From 2149eac74f283c58e470f39efc4b8e555080553a Mon Sep 17 00:00:00 2001 From: Olivier Fuxet Date: Tue, 19 Nov 2024 16:43:15 +0100 Subject: [PATCH 1/2] Implement getContributionEvents --- .../api/it/api/AbstractMarketplaceApiIT.java | 1 + .../api/it/api/ContributionsApiIT.java | 128 ++++++++++++++++++ .../resources/marketplace-api-contract.yaml | 77 ++++------- .../ReadContributionsApiPostgresAdapter.java | 103 ++++++++++---- 4 files changed, 226 insertions(+), 83 deletions(-) diff --git a/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/AbstractMarketplaceApiIT.java b/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/AbstractMarketplaceApiIT.java index 29fbc05a64..496ba8f7ce 100644 --- a/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/AbstractMarketplaceApiIT.java +++ b/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/AbstractMarketplaceApiIT.java @@ -81,6 +81,7 @@ public class AbstractMarketplaceApiIT { protected static final String BILLING_PROFILE_INVOICES_MANDATE = "/api/v1/billing-profiles/%s/invoices/mandate"; protected static final String CONTRIBUTIONS = "/api/v1/contributions"; protected static final String CONTRIBUTIONS_BY_ID = "/api/v1/contributions/%s"; + protected static final String CONTRIBUTIONS_BY_ID_EVENTS = "/api/v1/contributions/%s/events"; protected static final String PROJECTS_GET_CONTRIBUTION_BY_ID = "/api/v1/projects/%s/contributions/%s"; protected static final String PROJECTS_GET_BY_ID = "/api/v1/projects"; protected static final String PROJECTS_GET_BY_SLUG = "/api/v1/projects/slug"; diff --git a/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/ContributionsApiIT.java b/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/ContributionsApiIT.java index d3b51c618e..795f4fbb23 100644 --- a/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/ContributionsApiIT.java +++ b/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/ContributionsApiIT.java @@ -228,6 +228,68 @@ void should_get_pr_contribution() { """); } + @Test + void should_get_pr_contribution_events() { + // When + client.get() + .uri(getApiURI(CONTRIBUTIONS_BY_ID_EVENTS.formatted("f4db1d9b-4e1d-300c-9277-8d05824c804e"))) + // Then + .exchange() + .expectStatus() + .isOk() + .expectBody() + .json(""" + { + "events": [ + { + "timestamp": "2015-08-27T11:38:25Z", + "type": "PR_CREATED", + "assignee": null, + "mergedBy": null, + "linkedIssueContributionUuid": null + }, + { + "timestamp": "2015-08-30T19:18:14Z", + "type": "PR_MERGED", + "assignee": null, + "mergedBy": { + "githubUserId": 595505, + "login": "ofux", + "avatarUrl": "https://onlydust-app-images.s3.eu-west-1.amazonaws.com/5494259449694867225.webp", + "contacts": [ + { + "channel": "TWITTER", + "contact": "https://twitter.com/fuxeto", + "visibility": "private" + }, + { + "channel": "TELEGRAM", + "contact": "https://t.me/ofux", + "visibility": "private" + } + ] + }, + "linkedIssueContributionUuid": null + }, + { + "timestamp": "2023-10-29T01:01:47Z", + "type": "LINKED_ISSUE_CREATED", + "assignee": null, + "mergedBy": null, + "linkedIssueContributionUuid": "4782d22d-be45-3253-a3b4-5688045632f7" + }, + { + "timestamp": "2023-10-29T07:41:49Z", + "type": "LINKED_ISSUE_CLOSED", + "assignee": null, + "mergedBy": null, + "linkedIssueContributionUuid": "4782d22d-be45-3253-a3b4-5688045632f7" + } + ] + } + """); + } + @Test void should_get_issue_contribution() { // When @@ -342,6 +404,72 @@ void should_get_issue_contribution() { """); } + @Test + void should_get_issue_contribution_events() { + // When + client.get() + .uri(getApiURI(CONTRIBUTIONS_BY_ID_EVENTS.formatted("0f8d789f-fbbd-3171-ad03-9b2b6f8d9174"))) + // Then + .exchange() + .expectStatus() + .isOk() + .expectBody() + .json(""" + { + "events": [ + { + "timestamp": "2022-07-11T09:14:38Z", + "type": "ISSUE_CREATED", + "assignee": null, + "mergedBy": null, + "linkedIssueContributionUuid": null + }, + { + "timestamp": "2024-10-17T14:03:10.967909Z", + "type": "ISSUE_ASSIGNED", + "assignee": { + "githubUserId": 595505, + "login": "ofux", + "avatarUrl": "https://onlydust-app-images.s3.eu-west-1.amazonaws.com/5494259449694867225.webp", + "since": "2024-10-17T14:03:10.967909Z", + "assignedBy": { + "githubUserId": 43467246, + "login": "AnthonyBuisset", + "avatarUrl": "https://onlydust-app-images.s3.eu-west-1.amazonaws.com/11725380531262934574.webp", + "contacts": [ + { + "channel": "TELEGRAM", + "contact": "https://t.me/abuisset", + "visibility": "public" + }, + { + "channel": "TWITTER", + "contact": "https://twitter.com/abuisset", + "visibility": "public" + }, + { + "channel": "DISCORD", + "contact": "antho", + "visibility": "public" + } + ] + } + }, + "mergedBy": null, + "linkedIssueContributionUuid": null + }, + { + "timestamp": "2022-08-05T08:07:52Z", + "type": "ISSUE_CLOSED", + "assignee": null, + "mergedBy": null, + "linkedIssueContributionUuid": null + } + ] + } + """); + } + @Test void should_get_linked_issue_by_id() { // When diff --git a/marketplace-api-contract/src/main/resources/marketplace-api-contract.yaml b/marketplace-api-contract/src/main/resources/marketplace-api-contract.yaml index 657945940d..50caff2590 100644 --- a/marketplace-api-contract/src/main/resources/marketplace-api-contract.yaml +++ b/marketplace-api-contract/src/main/resources/marketplace-api-contract.yaml @@ -287,7 +287,6 @@ paths: required: true schema: $ref: '#/components/schemas/ContributionUuid' - responses: "200": description: 'Contribution' @@ -305,7 +304,7 @@ paths: $ref: '#/components/responses/InternalServerError' - /api/v1/contributions/{contributionId}/events: + /api/v1/contributions/{contributionUuid}/events: get: security: - bearerAuth: [ ] @@ -316,11 +315,11 @@ paths: description: | Get contribution events parameters: - - name: contributionId + - name: contributionUuid in: path required: true schema: - $ref: '#/components/schemas/ContributionId' + $ref: '#/components/schemas/ContributionUuid' responses: "200": description: 'Contribution event list' @@ -12608,63 +12607,35 @@ components: items: $ref: '#/components/schemas/ContributionEventResponse' + ContributionEventEnum: + type: string + enum: + - ISSUE_CREATED + - PR_CREATED + - ISSUE_ASSIGNED + - PR_MERGED + - ISSUE_CLOSED + - LINKED_ISSUE_CREATED + - LINKED_ISSUE_ASSIGNED + - LINKED_ISSUE_CLOSED + ContributionEventResponse: type: object required: - timestamp + - type properties: timestamp: type: string format: date-time - assigneeAdded: - type: object - required: - - assignee - properties: - assignee: - $ref: '#/components/schemas/GithubUserResponse' - assigneeRemoved: - type: object - required: - - assignee - properties: - assignee: - $ref: '#/components/schemas/GithubUserResponse' - opened: - type: object - required: - - by - properties: - by: - $ref: '#/components/schemas/GithubUserResponse' - merged: - type: object - required: - - by - properties: - by: - $ref: '#/components/schemas/GithubUserResponse' - closed: - type: object - required: - - by - properties: - by: - $ref: '#/components/schemas/GithubUserResponse' - issueLinked: - type: object - required: - - issue - properties: - issue: - $ref: '#/components/schemas/GithubIssueLinkResponse' - issueUnlinked: - type: object - required: - - issue - properties: - issue: - $ref: '#/components/schemas/GithubIssueLinkResponse' + type: + $ref: '#/components/schemas/ContributionEventEnum' + assignee: + $ref: '#/components/schemas/AssignedContributorResponse' + mergedBy: + $ref: '#/components/schemas/ContactableContributorResponse' + linkedIssueContributionUuid: + $ref: '#/components/schemas/ContributionUuid' IssuePatchRequest: type: object diff --git a/read-api/src/main/java/onlydust/com/marketplace/api/read/adapters/ReadContributionsApiPostgresAdapter.java b/read-api/src/main/java/onlydust/com/marketplace/api/read/adapters/ReadContributionsApiPostgresAdapter.java index ec91a65577..ea2b021d28 100644 --- a/read-api/src/main/java/onlydust/com/marketplace/api/read/adapters/ReadContributionsApiPostgresAdapter.java +++ b/read-api/src/main/java/onlydust/com/marketplace/api/read/adapters/ReadContributionsApiPostgresAdapter.java @@ -3,6 +3,7 @@ import lombok.AllArgsConstructor; import onlydust.com.marketplace.api.contract.ReadContributionsApi; import onlydust.com.marketplace.api.contract.model.*; +import onlydust.com.marketplace.api.read.entities.bi.ContributionReadEntity; import onlydust.com.marketplace.api.read.properties.Cache; import onlydust.com.marketplace.api.read.repositories.ContributionReadRepository; import onlydust.com.marketplace.api.rest.api.adapter.authentication.AuthenticatedAppUserService; @@ -11,10 +12,12 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; -import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; +import static onlydust.com.marketplace.api.contract.model.ContributionEventEnum.*; import static onlydust.com.marketplace.api.read.properties.Cache.XS; import static onlydust.com.marketplace.kernel.exception.OnlyDustException.notFound; import static onlydust.com.marketplace.kernel.pagination.PaginationHelper.hasMore; @@ -33,41 +36,21 @@ public class ReadContributionsApiPostgresAdapter implements ReadContributionsApi @Override public ResponseEntity getContributionById(UUID contributionUuid) { final var authenticatedUser = authenticatedAppUserService.tryGetAuthenticatedUser(); - final var page = contributionReadRepository.findAll(new ContributionsQueryParams() - .pageIndex(0) - .pageSize(1) - .ids(List.of(contributionUuid))); - - final var contribution = page.stream().findFirst() - .orElseThrow(() -> notFound("Contribution %s not found".formatted(contributionUuid))); + final var contribution = findContribution(contributionUuid); return ok() .cacheControl(cache.whenAnonymous(authenticatedUser, XS)) .body(contribution.toDto(authenticatedUser)); } - @Override - public ResponseEntity getContributionEvents(String contributionId) { - return ok(new ContributionEventListResponse() - .events(List.of( - new ContributionEventResponse() - .timestamp(ZonedDateTime.now().minusDays(2).minusHours(1).minusMinutes(5)) - .assigneeAdded(new ContributionEventResponseAssigneeAdded() - .assignee(new GithubUserResponse() - .avatarUrl("https://avatars.githubusercontent.com/u/43467246?v=4") - .githubUserId(1L) - .login("Antho") - )), - new ContributionEventResponse() - .timestamp(ZonedDateTime.now().minusDays(2).minusHours(1).minusMinutes(5)) - .opened(new ContributionEventResponseOpened() - .by(new GithubUserResponse() - .avatarUrl("https://avatars.githubusercontent.com/u/43467246?v=4") - .githubUserId(1L) - .login("Antho") - )) - ) - )); + private ContributionReadEntity findContribution(UUID contributionUuid) { + final var page = contributionReadRepository.findAll(new ContributionsQueryParams() + .pageIndex(0) + .pageSize(1) + .ids(List.of(contributionUuid))); + + return page.stream().findFirst() + .orElseThrow(() -> notFound("Contribution %s not found".formatted(contributionUuid))); } @Override @@ -84,4 +67,64 @@ public ResponseEntity getContributions(Contrib .totalItemNumber((int) page.getTotalElements()) .totalPageNumber(page.getTotalPages())); } + + @Override + public ResponseEntity getContributionEvents(UUID contributionUuid) { + final var contribution = findContribution(contributionUuid); + final List events = new ArrayList<>(); + + switch (contribution.contributionType()) { + case ISSUE -> { + events.add(new ContributionEventResponse() + .timestamp(contribution.createdAt()) + .type(ISSUE_CREATED)); + if (contribution.contributors() != null) { + contribution.contributors().forEach(a -> events.add(new ContributionEventResponse() + .timestamp(a.getSince()) + .type(ISSUE_ASSIGNED) + .assignee(a))); + } + events.add(new ContributionEventResponse() + .timestamp(contribution.completedAt()) + .type(ISSUE_CLOSED)); + } + case PULL_REQUEST -> { + events.add(new ContributionEventResponse() + .timestamp(contribution.createdAt()) + .type(PR_CREATED)); + Optional.ofNullable(contribution.mergedBy()).ifPresent(c -> events.add(new ContributionEventResponse() + .timestamp(contribution.completedAt()) + .type(PR_MERGED) + .mergedBy(c))); + if (contribution.linkedIssues() != null) { + contribution.linkedIssues().forEach(c -> { + final var linkedIssue = findContribution(c.getContributionUuid()); + events.add(new ContributionEventResponse() + .timestamp(linkedIssue.createdAt()) + .linkedIssueContributionUuid(linkedIssue.contributionUuid()) + .type(LINKED_ISSUE_CREATED)); + if (linkedIssue.contributors() != null) { + linkedIssue.contributors().forEach(a -> events.add(new ContributionEventResponse() + .timestamp(a.getSince()) + .linkedIssueContributionUuid(linkedIssue.contributionUuid()) + .type(LINKED_ISSUE_ASSIGNED) + .assignee(a))); + } + events.add(new ContributionEventResponse() + .timestamp(linkedIssue.completedAt()) + .linkedIssueContributionUuid(linkedIssue.contributionUuid()) + .type(LINKED_ISSUE_CLOSED)); + }); + } + } + case CODE_REVIEW -> { + return ok(new ContributionEventListResponse().events(List.of())); + } + } + + return ok() + .cacheControl(cache.forEverybody(XS)) + .body(new ContributionEventListResponse() + .events(events)); + } } From a4fc083e41f3713a9da4085a06220a031fe2e8d0 Mon Sep 17 00:00:00 2001 From: Olivier Fuxet Date: Tue, 19 Nov 2024 17:01:16 +0100 Subject: [PATCH 2/2] Remove zero totals from financial endpoint --- .../api/it/api/ReadBiFinancialStatsApiIT.java | 163 +----------------- .../BiFinancialMonthlyStatsReadEntity.java | 10 +- .../read/mapper/DetailedTotalMoneyMapper.java | 6 + 3 files changed, 14 insertions(+), 165 deletions(-) diff --git a/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/ReadBiFinancialStatsApiIT.java b/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/ReadBiFinancialStatsApiIT.java index 963a889875..fcb38ab028 100644 --- a/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/ReadBiFinancialStatsApiIT.java +++ b/bootstrap/src/test/java/onlydust/com/marketplace/api/it/api/ReadBiFinancialStatsApiIT.java @@ -218,144 +218,15 @@ void should_get_recipient_bi_financial_stats() { { "totalDeposited": { "totalUsdEquivalent": 0.00, - "totalPerCurrency": [ - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "3f6e1c98-8659-493a-b941-943a803bd91f", - "code": "BTC", - "name": "Bitcoin", - "logoUrl": null, - "decimals": 8 - }, - "usdEquivalent": null, - "usdConversionRate": null, - "ratio": null - }, - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "71bdfcf4-74ee-486b-8cfe-5d841dd93d5c", - "code": "ETH", - "name": "Ether", - "logoUrl": null, - "decimals": 18 - }, - "usdEquivalent": 0.00, - "usdConversionRate": 1781.983987, - "ratio": null - }, - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "562bbf65-8a71-4d30-ad63-520c0d68ba27", - "code": "USDC", - "name": "USD Coin", - "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/3408.png", - "decimals": 6 - }, - "usdEquivalent": 0.00, - "usdConversionRate": 1.010001, - "ratio": null - } - ] + "totalPerCurrency": [] }, "totalAllocated": { "totalUsdEquivalent": 0.00, - "totalPerCurrency": [ - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "3f6e1c98-8659-493a-b941-943a803bd91f", - "code": "BTC", - "name": "Bitcoin", - "logoUrl": null, - "decimals": 8 - }, - "usdEquivalent": null, - "usdConversionRate": null, - "ratio": null - }, - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "71bdfcf4-74ee-486b-8cfe-5d841dd93d5c", - "code": "ETH", - "name": "Ether", - "logoUrl": null, - "decimals": 18 - }, - "usdEquivalent": 0.00, - "usdConversionRate": 1781.983987, - "ratio": null - }, - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "562bbf65-8a71-4d30-ad63-520c0d68ba27", - "code": "USDC", - "name": "USD Coin", - "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/3408.png", - "decimals": 6 - }, - "usdEquivalent": 0.00, - "usdConversionRate": 1.010001, - "ratio": null - } - ] + "totalPerCurrency": [] }, "totalGranted": { "totalUsdEquivalent": 0.00, - "totalPerCurrency": [ - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "3f6e1c98-8659-493a-b941-943a803bd91f", - "code": "BTC", - "name": "Bitcoin", - "logoUrl": null, - "decimals": 8 - }, - "usdEquivalent": null, - "usdConversionRate": null, - "ratio": null - }, - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "71bdfcf4-74ee-486b-8cfe-5d841dd93d5c", - "code": "ETH", - "name": "Ether", - "logoUrl": null, - "decimals": 18 - }, - "usdEquivalent": 0.00, - "usdConversionRate": 1781.983987, - "ratio": null - }, - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "562bbf65-8a71-4d30-ad63-520c0d68ba27", - "code": "USDC", - "name": "USD Coin", - "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/3408.png", - "decimals": 6 - }, - "usdEquivalent": 0.00, - "usdConversionRate": 1.010001, - "ratio": null - } - ] + "totalPerCurrency": [] }, "totalRewarded": { "totalUsdEquivalent": 5850.95, @@ -407,20 +278,6 @@ void should_get_recipient_bi_financial_stats() { "totalPaid": { "totalUsdEquivalent": 404.00, "totalPerCurrency": [ - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "3f6e1c98-8659-493a-b941-943a803bd91f", - "code": "BTC", - "name": "Bitcoin", - "logoUrl": null, - "decimals": 8 - }, - "usdEquivalent": null, - "usdConversionRate": null, - "ratio": null - }, { "amount": 400, "prettyAmount": 400, @@ -434,20 +291,6 @@ void should_get_recipient_bi_financial_stats() { "usdEquivalent": 404.00, "usdConversionRate": 1.010001, "ratio": 100 - }, - { - "amount": 0, - "prettyAmount": 0, - "currency": { - "id": "71bdfcf4-74ee-486b-8cfe-5d841dd93d5c", - "code": "ETH", - "name": "Ether", - "logoUrl": null, - "decimals": 18 - }, - "usdEquivalent": 0.00, - "usdConversionRate": 1781.983987, - "ratio": 0 } ] }, diff --git a/read-api/src/main/java/onlydust/com/marketplace/api/read/entities/program/BiFinancialMonthlyStatsReadEntity.java b/read-api/src/main/java/onlydust/com/marketplace/api/read/entities/program/BiFinancialMonthlyStatsReadEntity.java index 369bd5f84d..5a1ceea8c8 100644 --- a/read-api/src/main/java/onlydust/com/marketplace/api/read/entities/program/BiFinancialMonthlyStatsReadEntity.java +++ b/read-api/src/main/java/onlydust/com/marketplace/api/read/entities/program/BiFinancialMonthlyStatsReadEntity.java @@ -122,11 +122,11 @@ public static BiFinancialsStatsListResponse toDto(List DetailedTotalMoney map(Collection stats, Function amountSupplier) { + return map(stats, amountSupplier, false); + } + + static DetailedTotalMoney map(Collection stats, Function amountSupplier, boolean removeZeroAmounts) { if (stats == null) return null; @@ -31,6 +36,7 @@ static DetailedTotalMoney map(Collection s .sorted(comparing(c -> c.currency().name())) .map(s -> s.toMoney(amountSupplier.apply(s), usdTotal)) .filter(Objects::nonNull) + .filter(s -> !removeZeroAmounts || s.getAmount().compareTo(ZERO) != 0) .sorted(comparing(DetailedTotalMoneyTotalPerCurrencyInner::getUsdEquivalent, nullsLast(naturalOrder())).reversed()) .toList()) .totalUsdEquivalent(prettyUsd(usdTotal));