diff --git a/swatch-billable-usage/build.gradle b/swatch-billable-usage/build.gradle index bcbcd1c507..232093bb43 100644 --- a/swatch-billable-usage/build.gradle +++ b/swatch-billable-usage/build.gradle @@ -31,6 +31,8 @@ dependencies { implementation project(":swatch-model-billable-usage") annotationProcessor enforcedPlatform(libraries["quarkus-bom"]) annotationProcessor "org.hibernate.orm:hibernate-jpamodelgen" + implementation libraries["mapstruct"] + annotationProcessor libraries["mapstruct-processor"] testImplementation libraries["junit-jupiter"] testImplementation "org.apache.kafka:kafka-streams-test-utils" testImplementation 'io.smallrye.reactive:smallrye-reactive-messaging-in-memory' diff --git a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageController.java b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageController.java index 09ecf47c53..046d630026 100644 --- a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageController.java +++ b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageController.java @@ -27,9 +27,12 @@ import com.redhat.swatch.billable.usage.data.BillableUsageRemittanceRepository; import com.redhat.swatch.billable.usage.data.RemittanceErrorCode; import com.redhat.swatch.billable.usage.data.RemittanceSummaryProjection; +import com.redhat.swatch.billable.usage.model.RemittanceMapper; import com.redhat.swatch.billable.usage.openapi.model.MonthlyRemittance; +import com.redhat.swatch.billable.usage.openapi.model.TallyRemittance; import com.redhat.swatch.billable.usage.services.BillingProducer; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; import java.time.OffsetDateTime; import java.util.ArrayList; @@ -47,6 +50,7 @@ public class InternalBillableUsageController { private final BillableUsageRemittanceRepository remittanceRepository; private final BillingProducer billingProducer; + @Inject protected RemittanceMapper remittanceMapper; public List getRemittances(BillableUsageRemittanceFilter filter) { if (filter.getOrgId() == null) { @@ -71,6 +75,10 @@ public List getRemittances(BillableUsageRemittanceFilter filt return accountRemittanceList; } + public List getRemittancesByTally(BillableUsageRemittanceFilter filter) { + return remittanceMapper.map(remittanceRepository.find(filter)); + } + @Transactional public int resetBillableUsageRemittance( String productId, OffsetDateTime start, OffsetDateTime end, Set orgIds) { diff --git a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageResource.java b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageResource.java index 0841adfb38..6fd15d5088 100644 --- a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageResource.java +++ b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageResource.java @@ -25,6 +25,7 @@ import com.redhat.swatch.billable.usage.kafka.streams.FlushTopicService; import com.redhat.swatch.billable.usage.openapi.model.DefaultResponse; import com.redhat.swatch.billable.usage.openapi.model.MonthlyRemittance; +import com.redhat.swatch.billable.usage.openapi.model.TallyRemittance; import com.redhat.swatch.billable.usage.openapi.resource.DefaultApi; import com.redhat.swatch.billable.usage.services.EnabledOrgsProducer; import jakarta.enterprise.context.ApplicationScoped; @@ -35,6 +36,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.UUID; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.candlepin.clock.ApplicationClock; @@ -84,6 +86,18 @@ public List getRemittances( return billingController.getRemittances(filter); } + @Override + public List getRemittancesByTally(String tallyId) throws ProcessingException { + + BillableUsageRemittanceFilter filter = + BillableUsageRemittanceFilter.builder().tallyId(UUID.fromString(tallyId)).build(); + List tallyRemittances = billingController.getRemittancesByTally(filter); + if (tallyRemittances.isEmpty()) { + throw new BadRequestException("Tally id not found in billable usage remittance" + tallyId); + } + return tallyRemittances; + } + @Override public DefaultResponse resetBillableUsageRemittance( Set orgIds, String productId, OffsetDateTime start, OffsetDateTime end) { diff --git a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/data/BillableUsageRemittanceFilter.java b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/data/BillableUsageRemittanceFilter.java index 3ca27a3ec4..d5c680c397 100644 --- a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/data/BillableUsageRemittanceFilter.java +++ b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/data/BillableUsageRemittanceFilter.java @@ -22,6 +22,7 @@ import com.redhat.swatch.configuration.registry.MetricId; import java.time.OffsetDateTime; +import java.util.UUID; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -50,6 +51,7 @@ public class BillableUsageRemittanceFilter { private OffsetDateTime ending; private String accumulationPeriod; private boolean excludeFailures; + private UUID tallyId; public static BillableUsageRemittanceFilter fromUsage(BillableUsage usage) { return BillableUsageRemittanceFilter.builder() diff --git a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/data/BillableUsageRemittanceRepository.java b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/data/BillableUsageRemittanceRepository.java index 0a34bdb0cd..a5929fb45b 100644 --- a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/data/BillableUsageRemittanceRepository.java +++ b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/data/BillableUsageRemittanceRepository.java @@ -163,6 +163,9 @@ private Specification buildSearchSpecification( if (filter.isExcludeFailures()) { searchCriteria = searchCriteria.and(excludesFailures()); } + if (Objects.nonNull(filter.getTallyId())) { + searchCriteria = searchCriteria.and(matchingTallyId(filter.getTallyId())); + } return searchCriteria; } @@ -241,4 +244,9 @@ private static Specification matchingSla(String s return (root, query, builder) -> builder.equal(root.get(BillableUsageRemittanceEntity_.sla), sla); } + + private static Specification matchingTallyId(UUID tallyId) { + return (root, query, builder) -> + builder.equal(root.get(BillableUsageRemittanceEntity_.tallyId), tallyId); + } } diff --git a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/model/RemittanceMapper.java b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/model/RemittanceMapper.java new file mode 100644 index 0000000000..dfa0c87472 --- /dev/null +++ b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/model/RemittanceMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright Red Hat, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Red Hat trademarks are not licensed under GPLv3. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.swatch.billable.usage.model; + +import com.redhat.swatch.billable.usage.data.BillableUsageRemittanceEntity; +import com.redhat.swatch.billable.usage.openapi.model.TallyRemittance; +import java.util.List; +import org.mapstruct.Builder; +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; + +@Mapper( + componentModel = "cdi", + collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, + builder = @Builder(disableBuilder = true)) +public interface RemittanceMapper { + + TallyRemittance mapBillableUsageEntityToTallyRemittance( + BillableUsageRemittanceEntity billableUsageRemittanceEntity); + + List map(List billableUsageRemittanceEntities); +} diff --git a/swatch-billable-usage/src/main/resources/META-INF/openapi.yaml b/swatch-billable-usage/src/main/resources/META-INF/openapi.yaml index e282d8da69..ee5ed252d0 100644 --- a/swatch-billable-usage/src/main/resources/META-INF/openapi.yaml +++ b/swatch-billable-usage/src/main/resources/META-INF/openapi.yaml @@ -8,6 +8,30 @@ info: name: SWATCH Dev url: https://github.com/RedHatInsights/rhsm-subscriptions paths: + /api/swatch-billable-usage/internal/remittance/accountRemittances/{tally_id}: + get: + operationId: getRemittancesByTally + summary: Get Account Remittance by Tally ID + description: Returns a single account remittance by tally ID. + parameters: + - name: tally_id + in: path + required: true + schema: + type: string + responses: + '200': + description: "Remittance records by tally id." + content: + application/json: + schema: + $ref: "#/components/schemas/TallyRemittances" + '400': + $ref: "../../../../../spec/error-responses.yaml#/$defs/BadRequest" + '403': + $ref: "../../../../../spec/error-responses.yaml#/$defs/Forbidden" + '500': + $ref: "../../../../../spec/error-responses.yaml#/$defs/InternalServerError" /api/swatch-billable-usage/internal/remittance/accountRemittances: description: 'Operations to get specific account remittances' parameters: @@ -219,6 +243,37 @@ components: type: string remittanceErrorCode: type: string + TallyRemittances: + type: array + items: + $ref: "#/components/schemas/TallyRemittance" + TallyRemittance: + description: Encapsulates all Monthly remittance + properties: + orgId: + type: string + productId: + type: string + metricId: + type: string + billingProvider: + type: string + billingAccountId: + type: string + remittedPendingValue: + type: number + format: double + accumulationPeriod: + type: string + remittancePendingDate: + type: string + format: date-time + status: + type: string + errorCode: + type: string + tallyId: + type: string OrgIds: type: string properties: diff --git a/swatch-billable-usage/src/test/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageResourceTest.java b/swatch-billable-usage/src/test/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageResourceTest.java index 2fe12e1003..7933f036f5 100644 --- a/swatch-billable-usage/src/test/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageResourceTest.java +++ b/swatch-billable-usage/src/test/java/com/redhat/swatch/billable/usage/admin/api/InternalBillableUsageResourceTest.java @@ -36,6 +36,7 @@ import com.redhat.swatch.billable.usage.kafka.InMemoryMessageBrokerKafkaResource; import com.redhat.swatch.billable.usage.model.EnabledOrgsRequest; import com.redhat.swatch.billable.usage.openapi.model.MonthlyRemittance; +import com.redhat.swatch.billable.usage.openapi.model.TallyRemittance; import io.quarkus.test.InjectMock; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; @@ -47,6 +48,7 @@ import jakarta.transaction.Transactional; import java.time.Duration; import java.time.OffsetDateTime; +import java.util.UUID; import org.apache.http.HttpStatus; import org.awaitility.Awaitility; import org.candlepin.subscriptions.billable.usage.BillableUsage; @@ -123,6 +125,28 @@ void testGetRemittances() { assertEquals(RemittanceErrorCode.UNKNOWN.getValue(), remittances[0].getRemittanceErrorCode()); } + @Test + void testGetRemittancesReturnsBadRequestWhenWrongTallyId() { + String tallyId = "e404074d-626f-4272-aa05-b6d69d6de16c"; + given() + .get("/api/swatch-billable-usage/internal/remittance/accountRemittances/" + tallyId) + .then() + .statusCode(HttpStatus.SC_BAD_REQUEST); + } + + @Test + void testGetRemittancesByTallyId() { + String tallyId = "c204074d-626f-4272-aa05-b6d69d6de16a"; + givenRemittanceForTallyId(tallyId); + TallyRemittance[] remittances = + given() + .get("/api/swatch-billable-usage/internal/remittance/accountRemittances/" + tallyId) + .as(TallyRemittance[].class); + assertEquals(1, remittances.length); + assertEquals(ORG_ID, remittances[0].getOrgId()); + assertEquals(RemittanceStatus.FAILED.name(), remittances[0].getStatus()); + } + @Test void testResetBillableUsageRemittance() { givenRemittanceForOrgId(ORG_ID); @@ -143,7 +167,7 @@ void testProcessRetries() { .post("/api/swatch-billable-usage/internal/rpc/remittance/processRetries") .then() .statusCode(HttpStatus.SC_OK); - Awaitility.await().untilAsserted(() -> assertEquals(2, billableUsageSink.received().size())); + Awaitility.await().untilAsserted(() -> assertEquals(3, billableUsageSink.received().size())); assertNull(remittanceRepository.findById(entity.getUuid()).getRetryAfter()); } @@ -187,11 +211,34 @@ BillableUsageRemittanceEntity givenRemittanceForOrgId(String orgId) { .status(RemittanceStatus.FAILED) .errorCode(RemittanceErrorCode.UNKNOWN) .retryAfter(OffsetDateTime.now().minusDays(1)) + .tallyId(UUID.fromString("c204074d-626f-4272-aa05-b6d69d6de16a")) .build(); remittanceRepository.persist(entity); return entity; } + @Transactional + void givenRemittanceForTallyId(String tallyId) { + var entity = + BillableUsageRemittanceEntity.builder() + .usage(BillableUsage.Usage.PRODUCTION.value()) + .orgId(ORG_ID) + .billingProvider(BillableUsage.BillingProvider.AZURE.value()) + .billingAccountId(String.format("%s_%s_ba", ORG_ID, PRODUCT_ID)) + .productId(PRODUCT_ID) + .accumulationPeriod("mm-DD") + .sla(BillableUsage.Sla.PREMIUM.value()) + .metricId("Cores") + .remittancePendingDate(OffsetDateTime.now()) + .remittedPendingValue(2.0) + .status(RemittanceStatus.FAILED) + .errorCode(RemittanceErrorCode.UNKNOWN) + .retryAfter(OffsetDateTime.now().minusDays(1)) + .tallyId(UUID.fromString(tallyId)) + .build(); + remittanceRepository.persist(entity); + } + private static void whenPurgeRemittances() { given() .post("/api/swatch-billable-usage/internal/rpc/remittance/purge")