Skip to content

Commit

Permalink
SWATCH-2461: Add new field "currentTotal" into the snapshot model (#3269
Browse files Browse the repository at this point in the history
)

Jira issue: [SWATCH-2461](https://issues.redhat.com/browse/SWATCH-2461)

## Description
The billable usage and snapshot measurements models should contain a new
field for the "currentTotal" value. The snapshot measurement one is
calculated via SQL query and it's copied over the new billable usage
field.

## Testing
1.- podman-compose up
2.- start the tally service:

```
LOGGING_LEVEL_ORG_CANDLEPIN_SUBSCRIPTIONS_TALLY_BILLING=DEBUG \
  SUBSCRIPTION_USE_STUB=true \
  USER_USE_STUB=true \
  CONTRACT_USE_STUB=true \
  DEV_MODE=true \
  ./gradlew :bootRun
```

3.- send tally summary message into the topic
"platform.rhsm-subscriptions.tally":

```
{"org_id": "org123", "tally_snapshots": [{"billing_provider":"aws", "billing_account_id": "123", "snapshot_date": "2023-12-21T01:10:28Z", "product_id": "rosa", "sla": "Premium", "usage": "Production", "granularity": "Hourly", "tally_measurements": [{"hardware_measurement_type": "AWS", "metric_id": "Cores", "value": 12}]}]}
```

And you should see the following message in the tally service:

```
[DEBUG] [org.candlepin.subscriptions.tally.billing.BillingProducer] - Sending billable usage org.candlepin.subscriptions.json.BillableUsage@4471ce00[uuid=<null>,orgId=org123,id=<null>,billingProvider=aws,billingAccountId=123,snapshotDate=2024-04-18T08:29:36.237472693Z,productId=rosa,sla=Premium,usage=Production,status=<null>,errorCode=<null>,billedOn=<null>,uom=<null>,metricId=Cores,value=0.0,billingFactor=0.25,vendorProductCode=<null>,hardwareMeasurementType=AWS,currentTotal=0.0] to topic platform.rhsm-subscriptions.billable-usage
```

Note the new field **currentTotal**.
  • Loading branch information
Sgitario committed Apr 22, 2024
2 parents 52beb2c + 4cfedaa commit 6441529
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
package org.candlepin.subscriptions.tally;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.candlepin.clock.ApplicationClock;
import org.candlepin.subscriptions.db.TallySnapshotRepository;
import org.candlepin.subscriptions.db.model.Granularity;
import org.candlepin.subscriptions.db.model.TallyMeasurementKey;
import org.candlepin.subscriptions.db.model.TallySnapshot;
import org.candlepin.subscriptions.json.TallyMeasurement;
Expand All @@ -32,6 +34,14 @@
@Component
public class TallySummaryMapper {

private final TallySnapshotRepository snapshotRepository;
private final ApplicationClock clock;

public TallySummaryMapper(TallySnapshotRepository snapshotRepository, ApplicationClock clock) {
this.snapshotRepository = snapshotRepository;
this.clock = clock;
}

public TallySummary mapSnapshots(String orgId, List<TallySnapshot> snapshots) {
return createTallySummary(orgId, snapshots);
}
Expand Down Expand Up @@ -70,19 +80,34 @@ private org.candlepin.subscriptions.json.TallySnapshot mapTallySnapshot(
.withUsage(usage)
.withBillingProvider(billingProvider)
.withBillingAccountId(tallySnapshot.getBillingAccountId())
.withTallyMeasurements(mapMeasurements(tallySnapshot.getTallyMeasurements()));
.withTallyMeasurements(mapMeasurements(tallySnapshot));
}

private List<TallyMeasurement> mapMeasurements(
Map<TallyMeasurementKey, Double> tallyMeasurements) {
return tallyMeasurements.entrySet().stream()
private List<TallyMeasurement> mapMeasurements(TallySnapshot snapshot) {
return snapshot.getTallyMeasurements().entrySet().stream()
.map(
entry ->
new TallyMeasurement()
.withHardwareMeasurementType(entry.getKey().getMeasurementType().toString())
.withUom(entry.getKey().getMetricId())
.withMetricId(entry.getKey().getMetricId())
.withValue(entry.getValue()))
.collect(Collectors.toList());
.withValue(entry.getValue())
.withCurrentTotal(getCurrentlyMeasuredTotal(snapshot, entry.getKey())))
.toList();
}

private Double getCurrentlyMeasuredTotal(
TallySnapshot snapshot, TallyMeasurementKey measurementKey) {
return snapshotRepository.sumMeasurementValueForPeriod(
snapshot.getOrgId(),
snapshot.getProductId(),
Granularity.HOURLY,
snapshot.getServiceLevel(),
snapshot.getUsage(),
snapshot.getBillingProvider(),
snapshot.getBillingAccountId(),
clock.startOfMonth(snapshot.getSnapshotDate()),
snapshot.getSnapshotDate(),
measurementKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,18 @@ private BillableUsage produceMonthlyBillable(BillableUsage usage) {
usage.getSnapshotDate());
log.debug("Usage: {}", usage);

// For old messages, the total value for period is not set yet, so we need to keep using the
// getCurrentlyMeasuredTotal method
// After SWATCH-2368, we will fully use the provided total value for period from the usage and
// the getCurrentlyMeasuredTotal method is to be removed.
Double currentlyMeasuredTotal =
getCurrentlyMeasuredTotal(
usage, clock.startOfMonth(usage.getSnapshotDate()), usage.getSnapshotDate());
Optional.ofNullable(usage.getCurrentTotal())
.orElseGet(
() ->
getCurrentlyMeasuredTotal(
usage,
clock.startOfMonth(usage.getSnapshotDate()),
usage.getSnapshotDate()));

Optional<Double> contractValue = Optional.of(0.0);
if (SubscriptionDefinition.isContractEnabled(usage.getProductId())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import lombok.extern.slf4j.Slf4j;
import org.candlepin.subscriptions.db.model.HardwareMeasurementType;
import org.candlepin.subscriptions.json.BillableUsage;
import org.candlepin.subscriptions.json.TallyMeasurement;
import org.candlepin.subscriptions.json.TallySnapshot;
import org.candlepin.subscriptions.json.TallySnapshot.BillingProvider;
import org.candlepin.subscriptions.json.TallySnapshot.Granularity;
Expand Down Expand Up @@ -83,38 +84,41 @@ protected boolean isSnapshotPAYGEligible(TallySnapshot snapshot) {
return isSnapshotPAYGEligible;
}

public Stream<BillableUsage> fromTallySummary(TallySummary tallySummary) {
return tallySummary.getTallySnapshots().stream()
public Stream<BillableUsage> fromTallySummary(TallySummary summary) {
return summary.getTallySnapshots().stream()
.filter(this::isSnapshotPAYGEligible)
.filter(this::hasMeasurements)
.flatMap(
snapshot ->
snapshot.getTallyMeasurements().stream()
// Filter out any HardwareMeasurementType.TOTAL measurements to prevent
// duplicates
.filter(
measurement ->
!Objects.equals(
HardwareMeasurementType.TOTAL.toString(),
measurement.getHardwareMeasurementType()))
.map(
measurement ->
new BillableUsage()
.withOrgId(tallySummary.getOrgId())
.withId(snapshot.getId())
.withSnapshotDate(snapshot.getSnapshotDate())
.withProductId(snapshot.getProductId())
.withSla(BillableUsage.Sla.fromValue(snapshot.getSla().value()))
.withUsage(
BillableUsage.Usage.fromValue(snapshot.getUsage().value()))
.withBillingProvider(
BillableUsage.BillingProvider.fromValue(
snapshot.getBillingProvider().value()))
.withBillingAccountId(snapshot.getBillingAccountId())
.withMetricId(measurement.getMetricId())
.withValue(measurement.getValue())
.withHardwareMeasurementType(
measurement.getHardwareMeasurementType())));
.filter(this::isNotHardwareMeasurementTypeTotal)
.map(m -> toBillableUsage(m, summary, snapshot)));
}

private BillableUsage toBillableUsage(
TallyMeasurement measurement, TallySummary summary, TallySnapshot snapshot) {
return new BillableUsage()
.withOrgId(summary.getOrgId())
.withId(snapshot.getId())
.withSnapshotDate(snapshot.getSnapshotDate())
.withProductId(snapshot.getProductId())
.withSla(BillableUsage.Sla.fromValue(snapshot.getSla().value()))
.withUsage(BillableUsage.Usage.fromValue(snapshot.getUsage().value()))
.withBillingProvider(
BillableUsage.BillingProvider.fromValue(snapshot.getBillingProvider().value()))
.withBillingAccountId(snapshot.getBillingAccountId())
.withMetricId(measurement.getMetricId())
.withValue(measurement.getValue())
.withHardwareMeasurementType(measurement.getHardwareMeasurementType())
.withCurrentTotal(measurement.getCurrentTotal());
}

private boolean isNotHardwareMeasurementTypeTotal(TallyMeasurement measurement) {
return !HardwareMeasurementType.TOTAL
.toString()
.equals(measurement.getHardwareMeasurementType());
}

private boolean hasMeasurements(TallySnapshot tallySnapshot) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.candlepin.clock.ApplicationClock;
import org.candlepin.subscriptions.db.TallySnapshotRepository;
import org.candlepin.subscriptions.db.model.BillingProvider;
import org.candlepin.subscriptions.db.model.Granularity;
import org.candlepin.subscriptions.db.model.HardwareMeasurementType;
Expand All @@ -50,6 +52,7 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.kafka.core.KafkaTemplate;
Expand All @@ -59,6 +62,9 @@
class SnapshotSummaryProducerTest {

@Mock private KafkaTemplate<String, TallySummary> kafka;
@Mock TallySnapshotRepository snapshotRepository;
@Mock ApplicationClock clock;
@InjectMocks TallySummaryMapper tallySummaryMapper;

@Captor private ArgumentCaptor<TallySummary> summaryCaptor;

Expand All @@ -71,8 +77,7 @@ void setup() {
props = new TallySummaryProperties();
props.setTopic("summary-topic");
RetryTemplate retryTemplate = new RetryTemplate();
this.producer =
new SnapshotSummaryProducer(kafka, retryTemplate, props, new TallySummaryMapper());
this.producer = new SnapshotSummaryProducer(kafka, retryTemplate, props, tallySummaryMapper);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,20 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.redhat.swatch.configuration.util.MetricIdUtils;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.candlepin.clock.ApplicationClock;
import org.candlepin.subscriptions.db.TallySnapshotRepository;
import org.candlepin.subscriptions.db.model.BillingProvider;
import org.candlepin.subscriptions.db.model.Granularity;
import org.candlepin.subscriptions.db.model.HardwareMeasurementType;
Expand All @@ -38,9 +45,19 @@
import org.candlepin.subscriptions.db.model.Usage;
import org.candlepin.subscriptions.json.TallySummary;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class TallySummaryMapperTest {

@Mock TallySnapshotRepository snapshotRepository;
@Mock ApplicationClock clock;
@InjectMocks TallySummaryMapper mapper;
private Map<TallyMeasurementKey, Double> expectedTotalValues = new HashMap<>();

@Test
void testMapSnapshots() {
String org = "O1";
Expand All @@ -66,8 +83,6 @@ void testMapSnapshots() {
24);

var snapshots = List.of(rosa, rhel);

TallySummaryMapper mapper = new TallySummaryMapper();
TallySummary summary = mapper.mapSnapshots(org, snapshots);
assertEquals(org, summary.getOrgId());
List<org.candlepin.subscriptions.json.TallySnapshot> summarySnaps = summary.getTallySnapshots();
Expand Down Expand Up @@ -105,7 +120,21 @@ void assertMappedSnapshot(
assertTrue(expectedMeasurements.containsKey(key));
Double expectedValue = expectedMeasurements.get(key);
assertEquals(expectedValue, m.getValue());
assertEquals(expectedTotalValues.get(key), m.getCurrentTotal());
verify(snapshotRepository)
.sumMeasurementValueForPeriod(
eq(expected.getOrgId()),
eq(expected.getProductId()),
eq(Granularity.HOURLY),
eq(expected.getServiceLevel()),
eq(expected.getUsage()),
eq(expected.getBillingProvider()),
eq(expected.getBillingAccountId()),
any(),
eq(expected.getSnapshotDate()),
eq(key));
});
verify(clock, times(mappedMeasurements.size())).startOfMonth(expected.getSnapshotDate());
}

TallySnapshot buildSnapshot(
Expand All @@ -118,8 +147,8 @@ TallySnapshot buildSnapshot(
String metricId,
double val) {
Map<TallyMeasurementKey, Double> measurements = new HashMap<>();
measurements.put(new TallyMeasurementKey(HardwareMeasurementType.PHYSICAL, metricId), val);
measurements.put(new TallyMeasurementKey(HardwareMeasurementType.TOTAL, metricId), val);
buildMeasurement(measurements, HardwareMeasurementType.PHYSICAL, metricId, val);
buildMeasurement(measurements, HardwareMeasurementType.TOTAL, metricId, val);

return TallySnapshot.builder()
.orgId(orgId)
Expand All @@ -133,6 +162,19 @@ TallySnapshot buildSnapshot(
.build();
}

void buildMeasurement(
Map<TallyMeasurementKey, Double> measurements,
HardwareMeasurementType hardwareType,
String metricId,
double value) {
var measurementKey = new TallyMeasurementKey(hardwareType, metricId);
measurements.put(measurementKey, value);
expectedTotalValues.put(measurementKey, value * 2);
when(snapshotRepository.sumMeasurementValueForPeriod(
any(), any(), any(), any(), any(), any(), any(), any(), any(), eq(measurementKey)))
.thenReturn(value * 2);
}

Optional<org.candlepin.subscriptions.json.TallySnapshot> findSnapshot(
TallySummary summary, String product) {
return summary.getTallySnapshots().stream()
Expand Down
Loading

0 comments on commit 6441529

Please sign in to comment.