Skip to content

Commit 39c670b

Browse files
committed
feat(charges): ubp flatfee costbasis support
1 parent a058e68 commit 39c670b

91 files changed

Lines changed: 33695 additions & 12176 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/common/billing.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ func NewBillingRegistry(
173173
tracer trace.Tracer,
174174
taxCodeService taxcode.Service,
175175
currencyResolver currencies.CurrencyResolver,
176+
currenciesService currencies.Service,
176177
locker *lockr.Locker,
177178
ledgerService ledger.Ledger,
178179
balanceQuerier ledger.BalanceQuerier,
@@ -226,6 +227,7 @@ func NewBillingRegistry(
226227
breakageService,
227228
taxCodeService,
228229
currencyResolver,
230+
currenciesService,
229231
fsConfig.NamespaceLockdown,
230232
creditsConfig,
231233
featureGate,

app/common/charges.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ func NewChargesFlatFeeService(
212212
metaAdapter meta.Adapter,
213213
locker *lockr.Locker,
214214
ratingService rating.Service,
215+
currenciesService currencies.Service,
215216
) (flatfee.Service, error) {
216217
flatFeeSvc, err := flatfeeservice.New(flatfeeservice.Config{
217218
Adapter: flatFeeAdapter,
@@ -220,6 +221,7 @@ func NewChargesFlatFeeService(
220221
MetaAdapter: metaAdapter,
221222
Locker: locker,
222223
RatingService: ratingService,
224+
Currencies: currenciesService,
223225
})
224226
if err != nil {
225227
return nil, fmt.Errorf("failed to create charges flat fee service: %w", err)
@@ -414,6 +416,7 @@ func newChargesRegistry(
414416
breakageService ledgerbreakage.Service,
415417
taxCodeService taxcode.Service,
416418
currencyResolver currencies.CurrencyResolver,
419+
currenciesService currencies.Service,
417420
fsNamespaceLockdown []string,
418421
creditsConfig config.CreditsConfiguration,
419422
featureGate *featuregate.FeatureGateChecker,
@@ -473,7 +476,7 @@ func newChargesRegistry(
473476
return nil, err
474477
}
475478

476-
flatFeeSvc, err := NewChargesFlatFeeService(flatFeeAdapter, flatFeeHandler, lineageService, metaAdapter, locker, ratingService)
479+
flatFeeSvc, err := NewChargesFlatFeeService(flatFeeAdapter, flatFeeHandler, lineageService, metaAdapter, locker, ratingService, currenciesService)
477480
if err != nil {
478481
return nil, err
479482
}

cmd/billing-worker/wire_gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/jobs/internal/wire_gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/server/wire_gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openmeter/billing/charges/flatfee/adapter.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/alpacahq/alpacadecimal"
1010

1111
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
12+
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/costbasis"
1213
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/creditrealization"
1314
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/invoicedusage"
1415
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment"
@@ -24,10 +25,15 @@ type Adapter interface {
2425
ChargeRunAdapter
2526
ChargeInvoicedUsageAdapter
2627
ChargePaymentAdapter
28+
ChargeCostBasisAdapter
2729

2830
entutils.TxCreator
2931
}
3032

33+
type ChargeCostBasisAdapter interface {
34+
SetResolvedCostBasis(ctx context.Context, input costbasis.SetResolvedCostBasisInput) (costbasis.CostBasis, error)
35+
}
36+
3137
type ChargeAdapter interface {
3238
CreateCharges(ctx context.Context, charges CreateChargesInput) ([]Charge, error)
3339

@@ -141,6 +147,7 @@ type IntentWithInitialStatus struct {
141147
InitialAdvanceAfter *time.Time
142148
AmountAfterProration alpacadecimal.Decimal
143149
NoFiatTransactionRequired bool
150+
ResolvedCostBasis *costbasis.State
144151
}
145152

146153
func (i IntentWithInitialStatus) Validate() error {

openmeter/billing/charges/flatfee/adapter/charge.go

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import (
55
"fmt"
66
"time"
77

8+
"github.com/oklog/ulid/v2"
89
"github.com/samber/lo"
910

1011
"github.com/openmeterio/openmeter/openmeter/billing"
1112
"github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee"
1213
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
1314
metaadapter "github.com/openmeterio/openmeter/openmeter/billing/charges/meta/adapter"
1415
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/chargemeta"
16+
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/costbasis"
1517
"github.com/openmeterio/openmeter/openmeter/ent/db"
1618
dbchargeflatfee "github.com/openmeterio/openmeter/openmeter/ent/db/chargeflatfee"
1719
dbchargeflatfeeoverride "github.com/openmeterio/openmeter/openmeter/ent/db/chargeflatfeeoverride"
@@ -86,6 +88,10 @@ func (a *adapter) UpdateCharge(ctx context.Context, charge flatfee.ChargeBase) (
8688
return flatfee.ChargeBase{}, err
8789
}
8890

91+
if err := tx.loadCostBasisEdge(ctx, dbUpdatedChargeBase); err != nil {
92+
return flatfee.ChargeBase{}, err
93+
}
94+
8995
if overrideLayer := charge.Intent.GetOverrideLayerMutableFields(); overrideLayer != nil {
9096
intentOverride, err := tx.updateIntentOverride(ctx, charge.GetChargeID(), overrideLayer, intent.Currency)
9197
if err != nil {
@@ -132,6 +138,9 @@ func (a *adapter) UpdateSubscriptionItemID(ctx context.Context, charge flatfee.C
132138
}
133139

134140
updatedChargeBase.Edges.IntentOverride = override
141+
if err := tx.loadCostBasisEdge(ctx, updatedChargeBase); err != nil {
142+
return flatfee.Charge{}, err
143+
}
135144
mappedChargeBase, err := fromDBBaseWithCurrency(updatedChargeBase, charge.Intent.GetBaseIntent().Currency)
136145
if err != nil {
137146
return flatfee.Charge{}, err
@@ -208,19 +217,69 @@ func (a *adapter) CreateCharges(ctx context.Context, in flatfee.CreateChargesInp
208217
}
209218

210219
return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) ([]flatfee.Charge, error) {
211-
creates, err := slicesx.MapWithErr(in.Intents, func(intent flatfee.IntentWithInitialStatus) (*db.ChargeFlatFeeCreate, error) {
212-
return tx.buildCreateFlatFeeCharge(in.Namespace, intent)
220+
type preparedCreate struct {
221+
costBasis *db.ChargeFlatFeeCostBasisCreate
222+
charge *db.ChargeFlatFeeCreate
223+
}
224+
225+
preparedCreates := make([]preparedCreate, 0, len(in.Intents))
226+
for _, intent := range in.Intents {
227+
chargeCreate, err := tx.buildCreateFlatFeeCharge(in.Namespace, intent)
228+
if err != nil {
229+
return nil, err
230+
}
231+
232+
var costBasisCreate *db.ChargeFlatFeeCostBasisCreate
233+
if intent.Intent.CostBasis != nil {
234+
costBasisCreate, err = costbasis.Create(tx.db.ChargeFlatFeeCostBasis.Create(), costbasis.CreateInput{
235+
NamespacedID: models.NamespacedID{
236+
Namespace: in.Namespace,
237+
ID: ulid.Make().String(),
238+
},
239+
CurrencyID: intent.Intent.Currency.ID,
240+
Intent: *intent.Intent.CostBasis,
241+
State: intent.ResolvedCostBasis,
242+
})
243+
if err != nil {
244+
return nil, fmt.Errorf("building flat fee cost basis: %w", err)
245+
}
246+
}
247+
248+
preparedCreates = append(preparedCreates, preparedCreate{
249+
costBasis: costBasisCreate,
250+
charge: chargeCreate,
251+
})
252+
}
253+
254+
costBasisCreates := lo.Filter(preparedCreates, func(create preparedCreate, _ int) bool {
255+
return create.costBasis != nil
213256
})
214-
if err != nil {
215-
return nil, err
257+
258+
var createdCostBases []*db.ChargeFlatFeeCostBasis
259+
if len(costBasisCreates) > 0 {
260+
var err error
261+
createdCostBases, err = tx.db.ChargeFlatFeeCostBasis.CreateBulk(
262+
lo.Map(costBasisCreates, func(create preparedCreate, _ int) *db.ChargeFlatFeeCostBasisCreate {
263+
return create.costBasis
264+
})...,
265+
).Save(ctx)
266+
if err != nil {
267+
return nil, fmt.Errorf("creating flat fee cost bases: %w", err)
268+
}
269+
270+
lo.ForEach(costBasisCreates, func(create preparedCreate, idx int) {
271+
create.charge.SetCostBasisID(createdCostBases[idx].ID)
272+
})
216273
}
217274

218-
entities, err := tx.db.ChargeFlatFee.CreateBulk(creates...).Save(ctx)
275+
chargeCreates := lo.Map(preparedCreates, func(create preparedCreate, _ int) *db.ChargeFlatFeeCreate {
276+
return create.charge
277+
})
278+
entities, err := tx.db.ChargeFlatFee.CreateBulk(chargeCreates...).Save(ctx)
219279
if err != nil {
220280
return nil, metaadapter.MapChargeConstraintError(err)
221281
}
222282

223-
// Let's reserve the charge IDs
224283
err = tx.metaAdapter.RegisterCharges(ctx, meta.RegisterChargesInput{
225284
Namespace: in.Namespace,
226285
Type: meta.ChargeTypeFlatFee,
@@ -235,7 +294,20 @@ func (a *adapter) CreateCharges(ctx context.Context, in flatfee.CreateChargesInp
235294
return nil, err
236295
}
237296

297+
costBasisByID := lo.SliceToMap(createdCostBases, func(entity *db.ChargeFlatFeeCostBasis) (string, *db.ChargeFlatFeeCostBasis) {
298+
return entity.ID, entity
299+
})
300+
238301
return lo.MapErr(entities, func(entity *db.ChargeFlatFee, idx int) (flatfee.Charge, error) {
302+
if entity.CostBasisID != nil {
303+
createdCostBasis, ok := costBasisByID[*entity.CostBasisID]
304+
if !ok {
305+
return flatfee.Charge{}, fmt.Errorf("created flat fee cost basis %s not found", *entity.CostBasisID)
306+
}
307+
308+
entity.Edges.CostBasis = createdCostBasis
309+
}
310+
239311
return FromDBWithCurrency(entity, in.Intents[idx].Intent.Currency, meta.ExpandNone)
240312
})
241313
})
@@ -251,7 +323,8 @@ func (a *adapter) GetByIDs(ctx context.Context, input flatfee.GetByIDsInput) ([]
251323
Where(dbchargeflatfee.Namespace(input.Namespace)).
252324
Where(dbchargeflatfee.IDIn(input.IDs...)).
253325
WithIntentOverride().
254-
WithCustomCurrency()
326+
WithCustomCurrency().
327+
WithCostBasis()
255328

256329
if input.Expands.Has(meta.ExpandRealizations) {
257330
query = expandRealizations(query)
@@ -294,7 +367,8 @@ func (a *adapter) GetByID(ctx context.Context, input flatfee.GetByIDInput) (flat
294367
Where(dbchargeflatfee.Namespace(input.ChargeID.Namespace)).
295368
Where(dbchargeflatfee.ID(input.ChargeID.ID)).
296369
WithIntentOverride().
297-
WithCustomCurrency()
370+
WithCustomCurrency().
371+
WithCostBasis()
298372

299373
if input.Expands.Has(meta.ExpandRealizations) {
300374
query = expandRealizations(query)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package adapter
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee"
8+
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/costbasis"
9+
entdb "github.com/openmeterio/openmeter/openmeter/ent/db"
10+
dbchargeflatfeecostbasis "github.com/openmeterio/openmeter/openmeter/ent/db/chargeflatfeecostbasis"
11+
"github.com/openmeterio/openmeter/pkg/framework/entutils"
12+
"github.com/openmeterio/openmeter/pkg/models"
13+
)
14+
15+
var _ flatfee.ChargeCostBasisAdapter = (*adapter)(nil)
16+
17+
func (a *adapter) loadCostBasisEdge(ctx context.Context, entity *entdb.ChargeFlatFee) error {
18+
if entity.Edges.CostBasis != nil {
19+
return fmt.Errorf("flat fee cost basis edge is already loaded [charge_id=%s,edge_id=%s]", entity.ID, entity.Edges.CostBasis.ID)
20+
}
21+
22+
if entity.CostBasisID == nil {
23+
return nil
24+
}
25+
26+
costBasisEntity, err := a.db.ChargeFlatFeeCostBasis.Query().
27+
Where(
28+
dbchargeflatfeecostbasis.ID(*entity.CostBasisID),
29+
dbchargeflatfeecostbasis.Namespace(entity.Namespace),
30+
).
31+
Only(ctx)
32+
if err != nil {
33+
return fmt.Errorf("load flat fee cost basis edge [charge_id=%s,cost_basis_id=%s]: %w", entity.ID, *entity.CostBasisID, err)
34+
}
35+
36+
entity.Edges.CostBasis = costBasisEntity
37+
38+
return nil
39+
}
40+
41+
func (a *adapter) SetResolvedCostBasis(ctx context.Context, input costbasis.SetResolvedCostBasisInput) (costbasis.CostBasis, error) {
42+
if err := input.Validate(); err != nil {
43+
return costbasis.CostBasis{}, err
44+
}
45+
46+
return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (costbasis.CostBasis, error) {
47+
update := tx.db.ChargeFlatFeeCostBasis.UpdateOneID(input.ID).
48+
Where(
49+
dbchargeflatfeecostbasis.Namespace(input.Namespace),
50+
dbchargeflatfeecostbasis.ModeEQ(costbasis.ModeDynamic),
51+
dbchargeflatfeecostbasis.ResolvedCostBasisIDIsNil(),
52+
dbchargeflatfeecostbasis.ResolvedCostBasisIsNil(),
53+
dbchargeflatfeecostbasis.ResolvedAtIsNil(),
54+
)
55+
56+
update, err := costbasis.Set(update, input.State)
57+
if err != nil {
58+
return costbasis.CostBasis{}, err
59+
}
60+
61+
entity, err := update.Save(ctx)
62+
if entdb.IsNotFound(err) {
63+
existingEntity, getErr := tx.db.ChargeFlatFeeCostBasis.Query().
64+
Where(
65+
dbchargeflatfeecostbasis.ID(input.ID),
66+
dbchargeflatfeecostbasis.Namespace(input.Namespace),
67+
).
68+
Only(ctx)
69+
if entdb.IsNotFound(getErr) {
70+
return costbasis.CostBasis{}, models.NewGenericNotFoundError(
71+
fmt.Errorf("flat fee cost basis not found: %s", input.ID),
72+
)
73+
}
74+
if getErr != nil {
75+
return costbasis.CostBasis{}, fmt.Errorf("get flat fee cost basis after conditional resolution: %w", getErr)
76+
}
77+
78+
existing, getErr := costbasis.Get(existingEntity)
79+
if getErr != nil {
80+
return costbasis.CostBasis{}, fmt.Errorf("map flat fee cost basis after conditional resolution: %w", getErr)
81+
}
82+
83+
if existing.Intent.Kind() == costbasis.ModeDynamic && existing.State != nil {
84+
return existing, nil
85+
}
86+
87+
return costbasis.CostBasis{}, models.NewGenericValidationError(
88+
fmt.Errorf("flat fee cost basis is not an unresolved dynamic cost basis: %s", input.ID),
89+
)
90+
}
91+
if err != nil {
92+
return costbasis.CostBasis{}, fmt.Errorf("set resolved flat fee cost basis: %w", err)
93+
}
94+
95+
return costbasis.Get(entity)
96+
})
97+
}

openmeter/billing/charges/flatfee/adapter/intentoverride.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ func (a *adapter) CreateChargeOverride(ctx context.Context, charge flatfee.Charg
8383
}
8484

8585
dbCharge.Edges.IntentOverride = dbIntentOverride
86+
if err := tx.loadCostBasisEdge(ctx, dbCharge); err != nil {
87+
return flatfee.ChargeBase{}, err
88+
}
8689

8790
return fromDBBaseWithCurrency(dbCharge, charge.Intent.GetBaseIntent().Currency)
8891
})

0 commit comments

Comments
 (0)