summaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainer.java
blob: 7434fce31bf344835c37a5545107ccb73e137e66 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
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.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.BillStatus;
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;
import com.yahoo.vespa.hosted.controller.api.integration.billing.InvoiceUpdate;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

public class BillingReportMaintainer extends ControllerMaintainer {

    private final BillingReporter reporter;
    private final BillingController billing;
    private final BillingDatabaseClient databaseClient;

    private final PlanRegistry plans;

    public BillingReportMaintainer(Controller controller, Duration interval) {
        super(controller, interval, null, Set.of(SystemName.Public, SystemName.PublicCd));
        reporter = controller.serviceRegistry().billingReporter();
        billing = controller.serviceRegistry().billingController();
        databaseClient = controller.serviceRegistry().billingDatabase();
        plans = controller.serviceRegistry().planRegistry();
    }

    @Override
    protected double maintain() {
        maintainTenants();

        var updates = maintainInvoices();
        log.fine("Updated invoices: " + updates);

        return 0.0;
    }

    private void maintainTenants() {
        var tenants = cloudTenants();
        var tenantNames = List.copyOf(tenants.keySet());
        var billableTenants = billableTenants(tenantNames);

        billableTenants.forEach(tenant -> {
            controller().tenants().lockIfPresent(tenant, LockedTenant.Cloud.class, locked -> {
                var ref = reporter.maintainTenant(locked.get());
                if (locked.get().billingReference().isEmpty() || ! locked.get().billingReference().get().equals(ref)) {
                    controller().tenants().store(locked.with(ref));
                }
            });
        });
    }

    List<InvoiceUpdate> maintainInvoices() {
        var updates = new ArrayList<InvoiceUpdate>();

        var tenants = cloudTenants();
        var billsNeedingMaintenance = databaseClient.readBills().stream()
                .filter(bill -> bill.getExportedId().isPresent())
                .filter(exported -> ! exported.status().isFinal())
                .toList();

        for (var bill : billsNeedingMaintenance) {
            var exportedId = bill.getExportedId().orElseThrow();
            var update = reporter.maintainInvoice(tenants.get(bill.tenant()), bill);
            switch (update.type()) {
                case UNMODIFIED -> log.finer(() ->invoiceMessage(bill.id(), exportedId) + " was not modified");
                case MODIFIED -> log.fine(invoiceMessage(bill.id(), exportedId) + " was updated with " + update.itemsUpdate().get());
                case UNMODIFIABLE -> {
                    // This check is needed to avoid setting the status multiple times
                    if (bill.status() != BillStatus.FROZEN) {
                        log.fine(() -> invoiceMessage(bill.id(), exportedId) + " is now unmodifiable");
                        databaseClient.setStatus(bill.id(), "system", BillStatus.FROZEN);
                    }
                }
                case REMOVED -> {
                    log.fine(() -> invoiceMessage(bill.id(), exportedId) + " has been deleted in the external system");
                    // Reset the exportedId to null, so that we don't maintain it again
                    databaseClient.setExportedInvoiceId(bill.id(), null);
                }
                case PAID -> {
                    log.fine(() -> invoiceMessage(bill.id(), exportedId) + " has been paid in the external system");
                    databaseClient.setStatus(bill.id(), "system", BillStatus.SUCCESSFUL);
                }
                case VOIDED -> {
                    log.fine(() -> invoiceMessage(bill.id(), exportedId) + " has been voided in the external system");
                    databaseClient.setStatus(bill.id(), "system", BillStatus.VOID);
                }
            }
            updates.add(update);
        }
        return updates;
    }

    private String invoiceMessage(Bill.Id billId, String invoiceId) {
        return "Invoice '" + invoiceId + "' for bill '" + billId.value() + "'";
    }

    private Map<TenantName, CloudTenant> cloudTenants() {
        return controller().tenants().asList()
                .stream()
                .filter(CloudTenant.class::isInstance)
                .map(CloudTenant.class::cast)
                .collect(Collectors.toMap(
                        Tenant::name,
                        Function.identity()));
    }

    private List<Plan> billablePlans() {
        return plans.all().stream()
                .filter(Plan::isBilled)
                .toList();
    }

    private List<TenantName> billableTenants(List<TenantName> tenants) {
        return billablePlans().stream()
                .flatMap(p -> billing.tenantsWithPlan(tenants, p.id()).stream())
                .toList();
    }

}