diff options
7 files changed, 85 insertions, 18 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java index 300c1658c29..ee7679f54ca 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java @@ -29,6 +29,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { private final Map<Bill.Id, Bill.StatusHistory> statuses = new HashMap<>(); private final Map<Bill.Id, ZonedDateTime> startTimes = new HashMap<>(); private final Map<Bill.Id, ZonedDateTime> endTimes = new HashMap<>(); + private final Map<Bill.Id, String> exportedInvoiceIds = new HashMap<>(); private final ZonedDateTime startTime = LocalDate.of(2020, 4, 1).atStartOfDay(ZoneId.of("UTC")); private final ZonedDateTime endTime = LocalDate.of(2020, 5, 1).atStartOfDay(ZoneId.of("UTC")); @@ -74,7 +75,8 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { var status = statuses.getOrDefault(billId, Bill.StatusHistory.open(clock)); var start = startTimes.getOrDefault(billId, startTime); var end = endTimes.getOrDefault(billId, endTime); - return invoice.map(tenant -> new Bill(billId, tenant, status, lines, start, end)); + var exportedId = exportedInvoiceId(billId); + return invoice.map(tenant -> new Bill(billId, tenant, status, lines, start, end, exportedId)); } @Override @@ -157,7 +159,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { var status = statuses.get(invoiceId); var start = startTimes.get(invoiceId); var end = endTimes.get(invoiceId); - return new Bill(invoiceId, tenant, status, items, start, end); + return new Bill(invoiceId, tenant, status, items, start, end, exportedInvoiceId(invoiceId)); }) .toList(); } @@ -171,7 +173,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { var status = statuses.get(invoiceId); var start = startTimes.get(invoiceId); var end = endTimes.get(invoiceId); - return new Bill(invoiceId, tenant, status, items, start, end); + return new Bill(invoiceId, tenant, status, items, start, end, exportedInvoiceId(invoiceId)); }) .toList(); } @@ -180,9 +182,14 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { public void maintain() {} @Override - public void setExportedInvoiceId(Bill.Id billId, String invoiceId) { } + public void setExportedInvoiceId(Bill.Id billId, String invoiceId) { + exportedInvoiceIds.put(billId, invoiceId); + } @Override public void setExportedInvoiceItemId(String lineItemId, String invoiceItemId) { } + private String exportedInvoiceId(Bill.Id billId) { + return exportedInvoiceIds.getOrDefault(billId, null); + } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java index 93bca72f05f..9531745556f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java @@ -21,7 +21,7 @@ public class BillingReporterMock implements BillingReporter { @Override public InvoiceUpdate maintainInvoice(Bill bill) { - return InvoiceUpdate.empty(); + return new InvoiceUpdate(0,0,1); } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java index 20d0dac02b6..6ca3cf6ebb1 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java @@ -14,12 +14,6 @@ public record InvoiceUpdate(int itemsAdded, int itemsRemoved, int itemsModified) return new InvoiceUpdate(0, 0, 0); } - public InvoiceUpdate add(InvoiceUpdate other) { - return new InvoiceUpdate(itemsAdded + other.itemsAdded, - itemsRemoved + other.itemsRemoved, - itemsModified + other.itemsModified); - } - public static class Counter { private int itemsAdded = 0; private int itemsRemoved = 0; @@ -37,6 +31,12 @@ public record InvoiceUpdate(int itemsAdded, int itemsRemoved, int itemsModified) itemsModified++; } + public void add(InvoiceUpdate other) { + itemsAdded += other.itemsAdded; + itemsRemoved += other.itemsRemoved; + itemsModified += other.itemsModified; + } + public InvoiceUpdate finish() { return new InvoiceUpdate(itemsAdded, itemsRemoved, itemsModified); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java index 8ef14dd60ba..1b66fbb7bb4 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.billing; import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import java.math.BigDecimal; import java.time.Clock; @@ -22,6 +23,7 @@ import java.util.stream.Stream; public class MockBillingController implements BillingController { private final Clock clock; + private final BillingDatabaseClient dbClient; PlanId defaultPlan = PlanId.from("trial"); List<TenantName> tenants = new ArrayList<>(); @@ -32,8 +34,9 @@ public class MockBillingController implements BillingController { Map<TenantName, List<Bill.LineItem>> unusedLineItems = new HashMap<>(); Map<TenantName, CollectionMethod> collectionMethod = new HashMap<>(); - public MockBillingController(Clock clock) { + public MockBillingController(Clock clock, BillingDatabaseClient dbClient) { this.clock = clock; + this.dbClient = dbClient; } @Override @@ -203,6 +206,14 @@ public class MockBillingController implements BillingController { return count < limit; } + @Override + public String exportBill(Bill bill, String exportMethod, CloudTenant tenant) { + // Replace bill with a copy with exportedId set + var exportedId = "EXT-ID-123"; + dbClient.setExportedInvoiceId(bill.id(), exportedId); + return exportedId; + } + public void setTenants(List<TenantName> tenants) { this.tenants = tenants; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainer.java index 9cb45158bcb..15396649d2f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainer.java @@ -5,6 +5,7 @@ import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.LockedTenant; +import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClient; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingReporter; @@ -62,17 +63,17 @@ public class BillingReportMaintainer extends ControllerMaintainer { }); } - private InvoiceUpdate maintainInvoices() { + InvoiceUpdate maintainInvoices() { var billsNeedingMaintenance = databaseClient.readBills().stream() .filter(bill -> bill.getExportedId().isPresent()) .filter(exported -> ! exported.status().equals("ISSUED")) // TODO: This status does not yet exist. .toList(); - var updates = InvoiceUpdate.empty(); + var updates = new InvoiceUpdate.Counter(); for (var bill : billsNeedingMaintenance) { updates.add(reporter.maintainInvoice(bill)); } - return updates; + return updates.finish(); } private Map<TenantName, CloudTenant> cloudTenants() { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java index e8e3de697dc..67f77b9a872 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java @@ -89,7 +89,6 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg private final MockEnclaveAccessService mockAMIService = new MockEnclaveAccessService(); private final MockResourceTagger mockResourceTagger = new MockResourceTagger(); private final MockRoleService roleService = new MockRoleService(); - private final MockBillingController billingController = new MockBillingController(clock); private final ArtifactRegistryMock containerRegistry = new ArtifactRegistryMock(); private final NoopTenantSecretService tenantSecretService = new NoopTenantSecretService(); private final NoopEndpointSecretManager secretManager = new NoopEndpointSecretManager(); @@ -100,6 +99,7 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg private final PlanRegistry planRegistry = new PlanRegistryMock(); private final ResourceDatabaseClient resourceDb = new ResourceDatabaseClientMock(planRegistry); private final BillingDatabaseClient billingDb = new BillingDatabaseClientMock(clock, planRegistry); + private final MockBillingController billingController = new MockBillingController(clock, billingDb); private final RoleMaintainerMock roleMaintainer = new RoleMaintainerMock(); private final MockPricingController pricingController = new MockPricingController(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainerTest.java index 9e1aa64beae..bc6aefdd35c 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainerTest.java @@ -4,13 +4,20 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.hosted.controller.ControllerTester; +import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill; +import com.yahoo.vespa.hosted.controller.api.integration.billing.InvoiceUpdate; import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistryMock; import com.yahoo.vespa.hosted.controller.tenant.BillingReference; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import org.junit.jupiter.api.Test; import java.time.Duration; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.TreeMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -39,8 +46,49 @@ public class BillingReportMaintainerTest { assertNotNull(b1.orElseThrow().reference()); } + @Test + void only_bills_with_exported_id_are_maintained() { + var t1 = tester.createTenant("t1"); + var billingController = tester.controller().serviceRegistry().billingController(); + var billingDb = tester.controller().serviceRegistry().billingDatabase(); + + var start = LocalDate.of(2020, 5, 23).atStartOfDay(ZoneOffset.UTC); + var end = start.toLocalDate().plusDays(6).atStartOfDay(ZoneOffset.UTC); + var bill1 = billingDb.createBill(t1, start, end, "non-exported"); + var bill2 = billingDb.createBill(t1, start, end, "exported"); + + billingController.setPlan(t1, PlanRegistryMock.paidPlan.id(), false, true); + + + billingController.exportBill(billingDb.readBill(bill2).get(), "FOO", cloudTenant(t1)); + var updates = maintainer.maintainInvoices(); + assertEquals(new InvoiceUpdate(0, 0, 1), updates); + + var exportedBill = billingDb.readBill(bill2).get(); + assertEquals("EXT-ID-123", exportedBill.getExportedId().get()); + assertTrue(billingDb.readBill(bill1).get().getExportedId().isEmpty()); + } + + private CloudTenant cloudTenant(TenantName tenantName) { + return tester.controller().tenants().require(tenantName, CloudTenant.class); + } + private Optional<BillingReference> billingReference(TenantName tenantName) { - var t = tester.controller().tenants().require(tenantName, CloudTenant.class); - return t.billingReference(); + return cloudTenant(tenantName).billingReference(); } + + static Bill createBill(TenantName tenant) { + var start = LocalDate.of(2020, 5, 23).atStartOfDay(ZoneOffset.UTC); + var end = start.toLocalDate().plusDays(6).atStartOfDay(ZoneOffset.UTC); + var statusHistory = new Bill.StatusHistory(new TreeMap<>(Map.of(start, "OPEN"))); + return new Bill( + Bill.Id.of("bill-id-1"), + tenant, + statusHistory, + List.of(), + start, + end + ); + } + } |