File size: 5,012 Bytes
1c4c66b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
136
137
package reconciler

import (
	"fmt"

	"github.com/samber/lo"

	"github.com/openmeterio/openmeter/openmeter/billing"
	"github.com/openmeterio/openmeter/openmeter/billing/charges"
	chargesmeta "github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
	chargesusagebased "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased"
	"github.com/openmeterio/openmeter/openmeter/billing/worker/subscriptionsync/service/persistedstate"
	"github.com/openmeterio/openmeter/openmeter/billing/worker/subscriptionsync/service/targetstate"
	"github.com/openmeterio/openmeter/openmeter/productcatalog"
	"github.com/openmeterio/openmeter/pkg/timeutil"
)

type usageBasedChargeCollection struct {
	chargePatchCollection
}

func newUsageBasedChargeCollection(preallocatedCapacity int) *usageBasedChargeCollection {
	return &usageBasedChargeCollection{
		chargePatchCollection: newChargePatchCollection(billing.LineEngineTypeChargeUsageBased, persistedstate.ItemTypeChargeUsageBased, preallocatedCapacity),
	}
}

func (c *usageBasedChargeCollection) AddCreate(target targetstate.StateItem) error {
	intent, err := newUsageBasedChargeIntent(target)
	if err != nil {
		return err
	}

	return c.addCreate(intent)
}

func (c *usageBasedChargeCollection) AddShrink(_ string, existing persistedstate.Item, target targetstate.StateItem) error {
	if _, ok := existing.(persistedstate.UsageBasedChargeGetter); !ok {
		return fmt.Errorf("existing item is not a usage based charge [item_type=%s,id=%s]", existing.Type(), existing.ID())
	}

	targetServicePeriod := target.GetServicePeriod()

	patch, err := chargesmeta.NewPatchShrink(chargesmeta.NewPatchShrinkInput{
		ChangeSource:           billing.ChangeSourceSystem,
		NewServicePeriodTo:     targetServicePeriod.To,
		NewFullServicePeriodTo: target.FullServicePeriod.To,
		NewBillingPeriodTo:     target.BillingPeriod.To,
		NewInvoiceAt:           target.GetInvoiceAt(),
	})
	if err != nil {
		return err
	}

	return c.addPatch(existing.ID().ID, patch)
}

func (c *usageBasedChargeCollection) AddExtend(existing persistedstate.Item, target targetstate.StateItem) error {
	if _, ok := existing.(persistedstate.UsageBasedChargeGetter); !ok {
		return fmt.Errorf("existing item is not a usage based charge [item_type=%s,id=%s]", existing.Type(), existing.ID())
	}

	targetServicePeriod := target.GetServicePeriod()

	patch, err := chargesmeta.NewPatchExtend(chargesmeta.NewPatchExtendInput{
		ChangeSource:           billing.ChangeSourceSystem,
		NewServicePeriodTo:     targetServicePeriod.To,
		NewFullServicePeriodTo: target.FullServicePeriod.To,
		NewBillingPeriodTo:     target.BillingPeriod.To,
		NewInvoiceAt:           target.GetInvoiceAt(),
	})
	if err != nil {
		return err
	}

	return c.addPatch(existing.ID().ID, patch)
}

func newUsageBasedChargeIntent(target targetstate.StateItem) (charges.ChargeIntent, error) {
	rateCardMeta := target.Spec.RateCard.AsMeta()
	price := rateCardMeta.Price
	if price == nil {
		return charges.ChargeIntent{}, fmt.Errorf("price is required for usage based charge")
	}

	// Copy unit_config too, not just price/discounts: if it is dropped here a
	// subscription-created charge silently rates the raw metered quantity. Clone so the
	// intent does not alias the spec.
	var unitConfig *productcatalog.UnitConfig
	if rateCardMeta.UnitConfig != nil {
		unitConfig = lo.ToPtr(rateCardMeta.UnitConfig.Clone())
	}

	annotations, err := target.SubscriptionItem.Annotations.Clone()
	if err != nil {
		return charges.ChargeIntent{}, err
	}

	return charges.NewChargeIntent(chargesusagebased.Intent{
		Intent: chargesmeta.Intent{
			ManagedBy:         billing.SubscriptionManagedLine,
			CustomerID:        target.Subscription.CustomerId,
			Annotations:       annotations,
			Currency:          target.CurrencyCalculator.Details().Code,
			UniqueReferenceID: &target.UniqueID,
			TaxConfig:         productcatalog.TaxCodeConfigFrom(rateCardMeta.TaxConfig),
			Subscription: &chargesmeta.SubscriptionReference{
				SubscriptionID: target.Subscription.ID,
				PhaseID:        target.PhaseID,
				ItemID:         target.SubscriptionItem.ID,
			},
		},
		IntentMutableFields: chargesusagebased.IntentMutableFields{
			IntentMutableFields: chargesmeta.IntentMutableFields{
				Name:          rateCardMeta.Name,
				Description:   rateCardMeta.Description,
				Metadata:      target.SubscriptionItem.Metadata.Clone(),
				ServicePeriod: target.GetServicePeriod(),
				FullServicePeriod: timeutil.ClosedPeriod{
					From: target.FullServicePeriod.From,
					To:   target.FullServicePeriod.To,
				},
				BillingPeriod: timeutil.ClosedPeriod{
					From: target.BillingPeriod.From,
					To:   target.BillingPeriod.To,
				},
			},
			InvoiceAt:  target.GetInvoiceAt(),
			Price:      *price,
			Discounts:  billing.DiscountsFromProductCatalog(rateCardMeta.Discounts).UpsertCorrelationIDs(),
			UnitConfig: unitConfig,
		},
		SettlementMode: target.Subscription.SettlementMode,
		FeatureKey:     lo.FromPtr(rateCardMeta.FeatureKey),
	}), nil
}