Skip to content

Commit 7854f8e

Browse files
committed
feat: add custom currency support to charges
1 parent f16570f commit 7854f8e

160 files changed

Lines changed: 5007 additions & 1244 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.

api/v3/handlers/customers/charges/convert.go

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func convertFlatFeeChargeToAPI(source flatfee.Charge) (api.BillingChargeFlatFee,
5959
AmountAfterProration: ConvertDecimalToCurrencyAmount(source.ChargeBase.State.AmountAfterProration),
6060
BillingPeriod: ConvertClosedPeriodToAPI(intent.BillingPeriod),
6161
CreatedAt: source.ChargeBase.ManagedResource.ManagedModel.CreatedAt,
62-
Currency: ConvertCurrencyCodeToAPI(source.ChargeBase.Intent.GetCurrency()),
62+
Currency: ConvertCurrencyCodeToAPI(source.ChargeBase.Intent.GetCurrency().GetCode()),
6363
Customer: ConvertCustomerIDToReference(source.ChargeBase.Intent.GetCustomerID()),
6464
DeletedAt: source.ChargeBase.ManagedResource.ManagedModel.DeletedAt,
6565
Description: intent.Description,
@@ -109,7 +109,7 @@ func convertUsageBasedChargeToAPI(source usagebased.Charge) (api.BillingChargeUs
109109
AdvanceAfter: source.State.AdvanceAfter,
110110
BillingPeriod: ConvertClosedPeriodToAPI(intent.BillingPeriod),
111111
CreatedAt: source.ChargeBase.ManagedResource.ManagedModel.CreatedAt,
112-
Currency: ConvertCurrencyCodeToAPI(source.ChargeBase.Intent.GetCurrency()),
112+
Currency: ConvertCurrencyCodeToAPI(source.ChargeBase.Intent.GetCurrency().GetCode()),
113113
Customer: ConvertCustomerIDToReference(source.ChargeBase.Intent.GetCustomerID()),
114114
DeletedAt: source.ChargeBase.ManagedResource.ManagedModel.DeletedAt,
115115
Description: intent.Description,
@@ -504,8 +504,8 @@ func convertAPIChargeStatus(s string) (meta.ChargeStatus, error) {
504504
}
505505
}
506506

507-
func convertFlatFeeChargeAPIToIntent(customerID string, flatFee api.CreateChargeFlatFeeRequest) (billingcharges.ChargeIntent, error) {
508-
var zero billingcharges.ChargeIntent
507+
func fromAPICreateChargeFlatFeeRequest(namespace, customerID string, flatFee api.CreateChargeFlatFeeRequest) (billingcharges.CreateCustomerChargeInput, error) {
508+
var zero billingcharges.CreateCustomerChargeInput
509509

510510
taxConfig, err := billingprofiles.FromAPIBillingTaxConfig(flatFee.TaxConfig)
511511
if err != nil {
@@ -549,37 +549,36 @@ func convertFlatFeeChargeAPIToIntent(customerID string, flatFee api.CreateCharge
549549
}
550550
}
551551

552-
return billingcharges.NewChargeIntent(flatfee.Intent{
553-
Intent: meta.Intent{
554-
ManagedBy: billing.ManuallyManagedLine,
555-
CustomerID: customerID,
556-
Currency: currencyx.Code(flatFee.Currency),
557-
TaxConfig: productcatalog.TaxCodeConfigFrom(taxConfig),
558-
UniqueReferenceID: flatFee.UniqueReferenceId,
559-
Subscription: nil,
560-
},
561-
IntentMutableFields: flatfee.IntentMutableFields{
562-
IntentMutableFields: meta.IntentMutableFields{
563-
Name: flatFee.Name,
564-
Description: flatFee.Description,
565-
Metadata: metadata,
566-
ServicePeriod: timeutil.ClosedPeriod(flatFee.ServicePeriod),
567-
FullServicePeriod: timeutil.ClosedPeriod(lo.FromPtrOr(flatFee.FullServicePeriod, flatFee.ServicePeriod)),
568-
BillingPeriod: timeutil.ClosedPeriod(lo.FromPtrOr(flatFee.BillingPeriod, flatFee.ServicePeriod)),
552+
return billingcharges.CreateCustomerChargeInput{
553+
Namespace: namespace,
554+
CustomerID: customerID,
555+
CurrencyCode: currencyx.Code(flatFee.Currency),
556+
TaxConfig: productcatalog.TaxCodeConfigFrom(taxConfig),
557+
UniqueReferenceID: flatFee.UniqueReferenceId,
558+
FlatFee: &billingcharges.CreateCustomerChargeFlatFeeInput{
559+
IntentMutableFields: flatfee.IntentMutableFields{
560+
IntentMutableFields: meta.IntentMutableFields{
561+
Name: flatFee.Name,
562+
Description: flatFee.Description,
563+
Metadata: metadata,
564+
ServicePeriod: timeutil.ClosedPeriod(flatFee.ServicePeriod),
565+
FullServicePeriod: timeutil.ClosedPeriod(lo.FromPtrOr(flatFee.FullServicePeriod, flatFee.ServicePeriod)),
566+
BillingPeriod: timeutil.ClosedPeriod(lo.FromPtrOr(flatFee.BillingPeriod, flatFee.ServicePeriod)),
567+
},
568+
InvoiceAt: flatFee.InvoiceAt,
569+
PaymentTerm: productcatalog.PaymentTermType(flatFee.PaymentTerm),
570+
PercentageDiscounts: discount,
571+
ProRating: proRating,
572+
AmountBeforeProration: amountBeforeProration,
569573
},
570-
InvoiceAt: flatFee.InvoiceAt,
571-
PaymentTerm: productcatalog.PaymentTermType(flatFee.PaymentTerm),
572-
PercentageDiscounts: discount,
573-
ProRating: proRating,
574-
AmountBeforeProration: amountBeforeProration,
574+
FeatureKey: flatFee.FeatureKey,
575+
SettlementMode: productcatalog.SettlementMode(flatFee.SettlementMode),
575576
},
576-
FeatureKey: flatFee.FeatureKey,
577-
SettlementMode: productcatalog.SettlementMode(flatFee.SettlementMode),
578-
}), nil
577+
}, nil
579578
}
580579

581-
func convertUsageBaseChargeAPIToIntent(customerID string, usageBasedFee api.CreateChargeUsageBasedRequest) (billingcharges.ChargeIntent, error) {
582-
var zero billingcharges.ChargeIntent
580+
func fromAPICreateChargeUsageBasedRequest(namespace, customerID string, usageBasedFee api.CreateChargeUsageBasedRequest) (billingcharges.CreateCustomerChargeInput, error) {
581+
var zero billingcharges.CreateCustomerChargeInput
583582

584583
taxConfig, err := billingprofiles.FromAPIBillingTaxConfig(usageBasedFee.TaxConfig)
585584
if err != nil {
@@ -618,29 +617,28 @@ func convertUsageBaseChargeAPIToIntent(customerID string, usageBasedFee api.Crea
618617
if err != nil {
619618
return zero, fmt.Errorf("invalid price: %w", err)
620619
}
621-
return billingcharges.NewChargeIntent(usagebased.Intent{
622-
Intent: meta.Intent{
623-
ManagedBy: billing.ManuallyManagedLine,
624-
CustomerID: customerID,
625-
Currency: currencyx.Code(usageBasedFee.Currency),
626-
TaxConfig: productcatalog.TaxCodeConfigFrom(taxConfig),
627-
UniqueReferenceID: usageBasedFee.UniqueReferenceId,
628-
Subscription: nil,
629-
},
630-
IntentMutableFields: usagebased.IntentMutableFields{
631-
IntentMutableFields: meta.IntentMutableFields{
632-
Name: usageBasedFee.Name,
633-
Description: usageBasedFee.Description,
634-
Metadata: metadata,
635-
ServicePeriod: timeutil.ClosedPeriod(usageBasedFee.ServicePeriod),
636-
FullServicePeriod: timeutil.ClosedPeriod(lo.FromPtrOr(usageBasedFee.FullServicePeriod, usageBasedFee.ServicePeriod)),
637-
BillingPeriod: timeutil.ClosedPeriod(lo.FromPtrOr(usageBasedFee.BillingPeriod, usageBasedFee.ServicePeriod)),
620+
return billingcharges.CreateCustomerChargeInput{
621+
Namespace: namespace,
622+
CustomerID: customerID,
623+
CurrencyCode: currencyx.Code(usageBasedFee.Currency),
624+
TaxConfig: productcatalog.TaxCodeConfigFrom(taxConfig),
625+
UniqueReferenceID: usageBasedFee.UniqueReferenceId,
626+
UsageBased: &billingcharges.CreateCustomerChargeUsageBasedInput{
627+
IntentMutableFields: usagebased.IntentMutableFields{
628+
IntentMutableFields: meta.IntentMutableFields{
629+
Name: usageBasedFee.Name,
630+
Description: usageBasedFee.Description,
631+
Metadata: metadata,
632+
ServicePeriod: timeutil.ClosedPeriod(usageBasedFee.ServicePeriod),
633+
FullServicePeriod: timeutil.ClosedPeriod(lo.FromPtrOr(usageBasedFee.FullServicePeriod, usageBasedFee.ServicePeriod)),
634+
BillingPeriod: timeutil.ClosedPeriod(lo.FromPtrOr(usageBasedFee.BillingPeriod, usageBasedFee.ServicePeriod)),
635+
},
636+
InvoiceAt: usageBasedFee.InvoiceAt,
637+
Price: *price,
638+
Discounts: discounts,
638639
},
639-
InvoiceAt: usageBasedFee.InvoiceAt,
640-
Price: *price,
641-
Discounts: discounts,
640+
FeatureKey: usageBasedFee.FeatureKey,
641+
SettlementMode: productcatalog.SettlementMode(usageBasedFee.SettlementMode),
642642
},
643-
FeatureKey: usageBasedFee.FeatureKey,
644-
SettlementMode: productcatalog.SettlementMode(usageBasedFee.SettlementMode),
645-
}), nil
643+
}, nil
646644
}

api/v3/handlers/customers/charges/convert_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
"github.com/openmeterio/openmeter/openmeter/billing"
1313
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
1414
"github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased"
15+
currenciestestutils "github.com/openmeterio/openmeter/openmeter/currencies/testutils/currency"
1516
"github.com/openmeterio/openmeter/openmeter/productcatalog"
16-
"github.com/openmeterio/openmeter/pkg/currencyx"
1717
"github.com/openmeterio/openmeter/pkg/timeutil"
1818
)
1919

@@ -80,7 +80,7 @@ func TestToAPIBillingChargeUsageBasedSystemIntentReturnsUnsupportedBasePriceErro
8080
Intent: meta.Intent{
8181
ManagedBy: billing.SubscriptionManagedLine,
8282
CustomerID: "customer-id",
83-
Currency: currencyx.Code("USD"),
83+
Currency: currenciestestutils.NewFiatCurrency(t, "USD"),
8484
},
8585
IntentMutableFields: usagebased.IntentMutableFields{
8686
IntentMutableFields: meta.IntentMutableFields{

api/v3/handlers/customers/charges/create.go

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package charges
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76
"net/http"
87

@@ -15,7 +14,7 @@ import (
1514
)
1615

1716
type (
18-
CreateCustomerChargesRequest = billingcharges.CreateInput
17+
CreateCustomerChargesRequest = billingcharges.CreateCustomerChargeInput
1918
CreateCustomerChargesResponse = api.BillingCharge
2019
CreateCustomerChargesParams struct {
2120
CustomerID api.ULID
@@ -31,10 +30,6 @@ func (h *handler) CreateCustomerCharge() CreateCustomerChargesHandler {
3130
return CreateCustomerChargesRequest{}, err
3231
}
3332

34-
input := billingcharges.CreateInput{
35-
Namespace: ns,
36-
}
37-
3833
body := api.CreateCustomerChargesJSONRequestBody{}
3934
if err := request.ParseBody(r, &body); err != nil {
4035
return CreateCustomerChargesRequest{}, err
@@ -51,6 +46,7 @@ func (h *handler) CreateCustomerCharge() CreateCustomerChargesHandler {
5146
})
5247
}
5348

49+
var input billingcharges.CreateCustomerChargeInput
5450
switch discriminator {
5551
case string(api.BillingChargeFlatFeeTypeFlatFee):
5652
flatFee, err := body.AsCreateChargeFlatFeeRequest()
@@ -64,7 +60,7 @@ func (h *handler) CreateCustomerCharge() CreateCustomerChargesHandler {
6460
})
6561
}
6662

67-
intent, err := convertFlatFeeChargeAPIToIntent(param.CustomerID, flatFee)
63+
input, err = fromAPICreateChargeFlatFeeRequest(ns, param.CustomerID, flatFee)
6864
if err != nil {
6965
return CreateCustomerChargesRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
7066
{
@@ -74,8 +70,6 @@ func (h *handler) CreateCustomerCharge() CreateCustomerChargesHandler {
7470
},
7571
})
7672
}
77-
78-
input.Intents = append(input.Intents, intent)
7973
case string(api.BillingChargeUsageBasedTypeUsageBased):
8074
usageBasedFee, err := body.AsCreateChargeUsageBasedRequest()
8175
if err != nil {
@@ -88,7 +82,7 @@ func (h *handler) CreateCustomerCharge() CreateCustomerChargesHandler {
8882
})
8983
}
9084

91-
intent, err := convertUsageBaseChargeAPIToIntent(param.CustomerID, usageBasedFee)
85+
input, err = fromAPICreateChargeUsageBasedRequest(ns, param.CustomerID, usageBasedFee)
9286
if err != nil {
9387
return CreateCustomerChargesRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
9488
{
@@ -98,8 +92,6 @@ func (h *handler) CreateCustomerCharge() CreateCustomerChargesHandler {
9892
},
9993
})
10094
}
101-
102-
input.Intents = append(input.Intents, intent)
10395
default:
10496
err := fmt.Errorf("invalid charge type: %s", discriminator)
10597
return CreateCustomerChargesRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
@@ -124,17 +116,12 @@ func (h *handler) CreateCustomerCharge() CreateCustomerChargesHandler {
124116
return input, nil
125117
},
126118
func(ctx context.Context, request CreateCustomerChargesRequest) (CreateCustomerChargesResponse, error) {
127-
res, err := h.service.Create(ctx, request)
119+
charge, err := h.service.CreateCustomerCharge(ctx, request)
128120
if err != nil {
129121
return CreateCustomerChargesResponse{}, err
130122
}
131-
if len(res) < 1 {
132-
return CreateCustomerChargesResponse{}, errors.New("no charge created")
133-
}
134-
if len(res) > 1 {
135-
return CreateCustomerChargesResponse{}, errors.New("too many results")
136-
}
137-
return convertChargeToAPI(res[0])
123+
124+
return convertChargeToAPI(charge)
138125
},
139126
commonhttp.JSONResponseEncoderWithStatus[CreateCustomerChargesResponse](http.StatusCreated),
140127
httptransport.AppendOptions(

api/v3/handlers/customers/charges/handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ type Handler interface {
1414

1515
type handler struct {
1616
resolveNamespace func(ctx context.Context) (string, error)
17-
service billingcharges.ChargeService
17+
service billingcharges.Service
1818
options []httptransport.HandlerOption
1919
}
2020

2121
func New(
2222
resolveNamespace func(ctx context.Context) (string, error),
23-
service billingcharges.ChargeService,
23+
service billingcharges.Service,
2424
options ...httptransport.HandlerOption,
2525
) Handler {
2626
return &handler{

api/v3/handlers/customers/credits/convert.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func toAPIBillingCreditGrant(charge creditpurchase.Charge) (api.BillingCreditGra
2929
Name: charge.Intent.Name,
3030
Description: charge.Intent.Description,
3131
Amount: charge.Intent.CreditAmount.String(),
32-
Currency: api.BillingCurrencyCode(charge.Intent.Currency),
32+
Currency: api.BillingCurrencyCode(charge.Intent.Currency.GetCode()),
3333
EffectiveAt: charge.Intent.EffectiveAt,
3434
ExpiresAt: charge.Intent.ExpiresAt,
3535
FundingMethod: toAPIBillingCreditFundingMethod(charge.Intent.Settlement),
@@ -103,13 +103,7 @@ func toAPICreditGrantPurchase(charge creditpurchase.Charge) (*api.BillingCreditG
103103
}
104104

105105
costBasis := inv.CostBasis.String()
106-
currency, err := currencyx.NewCurrencyBuilder(currencyx.CurrencyTypeFiat).
107-
WithCode(charge.Intent.Currency).
108-
Build()
109-
if err != nil {
110-
return nil, fmt.Errorf("getting currency calculator: %w", err)
111-
}
112-
purchaseAmount := currency.RoundToPrecision(charge.Intent.CreditAmount.Mul(inv.CostBasis))
106+
purchaseAmount := charge.Intent.Currency.RoundToPrecision(charge.Intent.CreditAmount.Mul(inv.CostBasis))
113107
settlementStatus := api.BillingCreditPurchasePaymentSettlementStatusPending
114108

115109
if charge.Realizations.InvoiceSettlement != nil {
@@ -130,13 +124,7 @@ func toAPICreditGrantPurchase(charge creditpurchase.Charge) (*api.BillingCreditG
130124
}
131125

132126
costBasis := ext.CostBasis.String()
133-
currency, err := currencyx.NewCurrencyBuilder(currencyx.CurrencyTypeFiat).
134-
WithCode(charge.Intent.Currency).
135-
Build()
136-
if err != nil {
137-
return nil, fmt.Errorf("getting currency calculator: %w", err)
138-
}
139-
purchaseAmount := currency.RoundToPrecision(charge.Intent.CreditAmount.Mul(ext.CostBasis))
127+
purchaseAmount := charge.Intent.Currency.RoundToPrecision(charge.Intent.CreditAmount.Mul(ext.CostBasis))
140128
availPolicy, err := toAPIBillingCreditAvailabilityPolicy(ext.InitialStatus)
141129
if err != nil {
142130
return nil, fmt.Errorf("converting availability policy: %w", err)

api/v3/handlers/customers/credits/convert_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import (
1111
api "github.com/openmeterio/openmeter/api/v3"
1212
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
1313
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
14+
currenciestestutils "github.com/openmeterio/openmeter/openmeter/currencies/testutils/currency"
1415
"github.com/openmeterio/openmeter/pkg/clock"
15-
"github.com/openmeterio/openmeter/pkg/currencyx"
1616
"github.com/openmeterio/openmeter/pkg/models"
1717
)
1818

@@ -34,7 +34,7 @@ func TestToAPIBillingCreditGrantPromotional(t *testing.T) {
3434
Intent: creditpurchase.Intent{
3535
Intent: meta.Intent{
3636
CustomerID: "cust-1",
37-
Currency: currencyx.Code("USD"),
37+
Currency: currenciestestutils.NewFiatCurrency(t, "USD"),
3838
},
3939
IntentMutableFields: creditpurchase.IntentMutableFields{
4040
IntentMutableFields: meta.IntentMutableFields{
@@ -79,7 +79,7 @@ func TestToAPIBillingCreditGrantStatusUsesExpiry(t *testing.T) {
7979
Intent: creditpurchase.Intent{
8080
Intent: meta.Intent{
8181
CustomerID: "cust-1",
82-
Currency: currencyx.Code("USD"),
82+
Currency: currenciestestutils.NewFiatCurrency(t, "USD"),
8383
},
8484
IntentMutableFields: creditpurchase.IntentMutableFields{
8585
CreditAmount: alpacadecimal.RequireFromString("25"),
@@ -151,7 +151,7 @@ func TestToAPIBillingCreditGrantKey(t *testing.T) {
151151
Intent: creditpurchase.Intent{
152152
Intent: meta.Intent{
153153
CustomerID: "cust-1",
154-
Currency: currencyx.Code("USD"),
154+
Currency: currenciestestutils.NewFiatCurrency(t, "USD"),
155155
},
156156
IntentMutableFields: creditpurchase.IntentMutableFields{
157157
IntentMutableFields: meta.IntentMutableFields{

api/v3/server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ type Config struct {
108108
SubscriptionWorkflowService subscriptionworkflow.Service
109109
TaxCodeService taxcode.Service
110110
CurrencyService currencies.Service
111-
ChargeService billingcharges.ChargeService
111+
ChargeService billingcharges.Service
112112
CostService cost.Service
113113
FeatureConnector feature.FeatureConnector
114114

app/common/billing.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
subscriptionsyncadapter "github.com/openmeterio/openmeter/openmeter/billing/worker/subscriptionsync/adapter"
3131
"github.com/openmeterio/openmeter/openmeter/billing/worker/subscriptionsync/reconciler"
3232
subscriptionsyncservice "github.com/openmeterio/openmeter/openmeter/billing/worker/subscriptionsync/service"
33+
"github.com/openmeterio/openmeter/openmeter/currencies"
3334
"github.com/openmeterio/openmeter/openmeter/customer"
3435
entdb "github.com/openmeterio/openmeter/openmeter/ent/db"
3536
"github.com/openmeterio/openmeter/openmeter/ledger"
@@ -171,6 +172,7 @@ func NewBillingRegistry(
171172
creditsConfig config.CreditsConfiguration,
172173
tracer trace.Tracer,
173174
taxCodeService taxcode.Service,
175+
currencyResolver currencies.CurrencyResolver,
174176
locker *lockr.Locker,
175177
ledgerService ledger.Ledger,
176178
balanceQuerier ledger.BalanceQuerier,
@@ -223,6 +225,7 @@ func NewBillingRegistry(
223225
accountService,
224226
breakageService,
225227
taxCodeService,
228+
currencyResolver,
226229
fsConfig.NamespaceLockdown,
227230
creditsConfig,
228231
featureGate,

0 commit comments

Comments
 (0)