Skip to content

Commit 7ecad00

Browse files
fix: unittests
1 parent cf55f04 commit 7ecad00

23 files changed

Lines changed: 116 additions & 44 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ For TypeSpec-specific coding constraints, update `api/spec/AGENTS.md` instead of
227227
- Do not extract helper functions only to hide a couple of simple operations or short guard checks. If the helper would only wrap 2-4 lines and its name does not add meaningful domain or business intent, keep the code inline even when there is some duplication. Readers can inspect the function body to see what the code does; prefer function names that explain the domain reason for the call over names that merely restate the implementation steps. When you encounter a leftover pass-through wrapper that only calls another function without adding behavior, remove it and call the underlying function directly, even if it is outside the immediate change area.
228228
- Do not hide non-trivial branching or domain translation inside local inline functions. If a closure performs type switching, validation, persistence mapping, or meaningful domain conversion, make it a named helper near the code that uses it so it is discoverable, testable, and grep-friendly. Reserve inline closures for tiny callbacks where the surrounding API requires a function literal and the logic is obvious at the call site.
229229
- For `Validate() error` methods, prefer collecting all validation issues into `var errs []error` and returning `models.NewNillableGenericValidationError(errors.Join(errs...))` instead of returning on the first invalid field. Preserve field context with wrapped errors like `fmt.Errorf("field: %w", err)` and use plain `errors.New(...)` for simple local checks.
230+
- Credit-purchase charges can carry custom ledger currency codes even though standard invoice and settlement currencies stay fiat-only. When persisting credit-purchase charge metadata through shared helpers such as `chargemeta.Create` or `chargemeta.Update`, use the credit-purchase-specific intent validator instead of falling back to generic `meta.Intent.Validate`, which intentionally rejects non-fiat codes.
230231
- Do not introduce `context.Background()` or `context.TODO()` to sidestep missing context propagation in application code. Either propagate the caller's context through the full call path, or remove the unused `context.Context` parameter from the API if the operation is purely local and does not need cancellation, deadlines, or request-scoped values.
231232
- Never use `panic` in non-test code paths. If a new failure mode is possible, change the function signature to return an error and propagate it explicitly.
232233
- In production constructors and initialization, do not use `slog.Default()` as a fallback dependency. Require a `*slog.Logger` in config/provider inputs and inject it explicitly.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func (a *adapter) UpdateCharge(ctx context.Context, charge creditpurchase.Charge
4242
Intent: charge.Intent.Intent,
4343
IntentMutableFields: charge.Intent.IntentMutableFields.IntentMutableFields,
4444
Status: metaStatus,
45+
ValidateIntent: charge.Intent.Validate,
4546
})
4647
if err != nil {
4748
return creditpurchase.ChargeBase{}, err
@@ -85,6 +86,7 @@ func (a *adapter) CreateCharge(ctx context.Context, in creditpurchase.CreateChar
8586
Intent: in.Intent.Intent,
8687
IntentMutableFields: in.Intent.IntentMutableFields.IntentMutableFields,
8788
Status: metaStatus,
89+
ValidateIntent: in.Intent.Validate,
8890
})
8991
if err != nil {
9092
return creditpurchase.Charge{}, err

openmeter/billing/charges/creditpurchase/charge.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package creditpurchase
33
import (
44
"errors"
55
"fmt"
6+
"slices"
67
"time"
78

89
"github.com/alpacahq/alpacadecimal"
910
"github.com/samber/lo"
1011

12+
"github.com/openmeterio/openmeter/openmeter/billing"
1113
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
1214
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/ledgertransaction"
1315
"github.com/openmeterio/openmeter/openmeter/billing/charges/models/payment"
@@ -217,8 +219,30 @@ func (i Intent) CalculateEffectiveAt() time.Time {
217219
func (i Intent) Validate() error {
218220
var errs []error
219221

220-
if err := i.Intent.Validate(); err != nil {
221-
errs = append(errs, fmt.Errorf("intent meta: %w", err))
222+
if !slices.Contains(billing.InvoiceLineManagedBy("").Values(), string(i.ManagedBy)) {
223+
errs = append(errs, fmt.Errorf("intent meta: invalid managed by %s", i.ManagedBy))
224+
}
225+
226+
if i.CustomerID == "" {
227+
errs = append(errs, fmt.Errorf("intent meta: customer ID is required"))
228+
}
229+
230+
if err := i.Currency.ValidateFormat(); err != nil {
231+
errs = append(errs, fmt.Errorf("intent meta: currency: %w", err))
232+
}
233+
234+
if err := i.TaxConfig.Validate(); err != nil {
235+
errs = append(errs, fmt.Errorf("intent meta: tax config: %w", err))
236+
}
237+
238+
if i.Subscription != nil {
239+
if err := i.Subscription.Validate(); err != nil {
240+
errs = append(errs, fmt.Errorf("intent meta: subscription: %w", err))
241+
}
242+
}
243+
244+
if i.UniqueReferenceID != nil && *i.UniqueReferenceID == "" {
245+
errs = append(errs, fmt.Errorf("intent meta: unique reference ID cannot be empty"))
222246
}
223247

224248
if err := i.IntentMutableFields.Validate(); err != nil {

openmeter/billing/charges/creditpurchase/charge_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"github.com/stretchr/testify/require"
88

99
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
10+
"github.com/openmeterio/openmeter/openmeter/customer"
11+
"github.com/openmeterio/openmeter/pkg/currencyx"
1012
"github.com/openmeterio/openmeter/pkg/timeutil"
1113
)
1214

@@ -71,3 +73,18 @@ func TestFeatureFiltersValidateAsFeatureFilter(t *testing.T) {
7173
require.Error(t, FeatureFilters([]string{""}).ValidateAsFeatureFilter())
7274
})
7375
}
76+
77+
func TestListFundedCreditActivitiesInputValidateAllowsCustomCurrency(t *testing.T) {
78+
currency := currencyx.Code("CREDITS")
79+
80+
input := ListFundedCreditActivitiesInput{
81+
Customer: customer.CustomerID{
82+
Namespace: "ns",
83+
ID: "customer-id",
84+
},
85+
Limit: 1,
86+
Currency: &currency,
87+
}
88+
89+
require.NoError(t, input.Validate())
90+
}

openmeter/billing/charges/creditpurchase/funded_credit_activity.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (i ListFundedCreditActivitiesInput) Validate() error {
8888
}
8989

9090
if i.Currency != nil {
91-
if err := i.Currency.Validate(); err != nil {
91+
if err := i.Currency.ValidateFormat(); err != nil {
9292
errs = append(errs, fmt.Errorf("currency: %w", err))
9393
}
9494
}

openmeter/billing/charges/creditpurchase/settlement.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ type GenericSettlement struct {
4343
func (s GenericSettlement) Validate() error {
4444
var errs []error
4545

46-
if err := s.Currency.Validate(); err != nil {
46+
if err := s.Currency.ValidateFormat(); err != nil {
4747
errs = append(errs, fmt.Errorf("settlement currency: %w", err))
48-
} else if s.Currency.CurrencyType() != currencyx.CurrencyTypeFiat {
48+
} else if !s.Currency.IsKnownFiat() {
4949
errs = append(errs, fmt.Errorf("settlement currency must be a known fiat currency"))
5050
}
5151

openmeter/billing/charges/lineage/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func (i BackfillAdvanceLineageSegmentsInput) Validate() error {
110110
if i.CustomerID == "" {
111111
errs = append(errs, errors.New("customer id is required"))
112112
}
113-
if err := i.Currency.Validate(); err != nil {
113+
if err := i.Currency.ValidateFormat(); err != nil {
114114
errs = append(errs, fmt.Errorf("currency: %w", err))
115115
}
116116
if !i.Amount.IsPositive() {
@@ -138,7 +138,7 @@ func (i LoadLineagesByCustomerInput) Validate() error {
138138
if i.CustomerID == "" {
139139
errs = append(errs, errors.New("customer id is required"))
140140
}
141-
if err := i.Currency.Validate(); err != nil {
141+
if err := i.Currency.ValidateFormat(); err != nil {
142142
errs = append(errs, fmt.Errorf("currency: %w", err))
143143
}
144144

openmeter/billing/charges/models/chargemeta/mixin.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,9 @@ type CreateInput struct {
117117
Intent meta.Intent
118118
IntentMutableFields meta.IntentMutableFields
119119

120-
Status meta.ChargeStatus
121-
AdvanceAfter *time.Time
120+
Status meta.ChargeStatus
121+
AdvanceAfter *time.Time
122+
ValidateIntent func() error
122123
}
123124

124125
type Creator[T any] interface {
@@ -170,7 +171,11 @@ func Create[T Creator[T]](creator Creator[T], in CreateInput) (T, error) {
170171
in.IntentMutableFields = in.IntentMutableFields.Normalized()
171172
in.AdvanceAfter = meta.NormalizeOptionalTimestamp(in.AdvanceAfter)
172173

173-
if err := in.Intent.Validate(); err != nil {
174+
validateIntent := in.Intent.Validate
175+
if in.ValidateIntent != nil {
176+
validateIntent = in.ValidateIntent
177+
}
178+
if err := validateIntent(); err != nil {
174179
var empty T
175180
return empty, err
176181
}
@@ -223,8 +228,9 @@ type UpdateInput struct {
223228
Intent meta.Intent
224229
IntentMutableFields meta.IntentMutableFields
225230

226-
Status meta.ChargeStatus
227-
AdvanceAfter *time.Time
231+
Status meta.ChargeStatus
232+
AdvanceAfter *time.Time
233+
ValidateIntent func() error
228234
}
229235

230236
func Update[T Updater[T]](updater Updater[T], in UpdateInput) (T, error) {
@@ -236,7 +242,11 @@ func Update[T Updater[T]](updater Updater[T], in UpdateInput) (T, error) {
236242
return empty, err
237243
}
238244

239-
if err := in.Intent.Validate(); err != nil {
245+
validateIntent := in.Intent.Validate
246+
if in.ValidateIntent != nil {
247+
validateIntent = in.ValidateIntent
248+
}
249+
if err := validateIntent(); err != nil {
240250
var empty T
241251
return empty, err
242252
}

openmeter/billing/charges/service/creditpurchase_test.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,20 @@ func (s *CreditPurchaseTestSuite) TestCreditPurchaseRejectsCustomSettlementCurre
136136
s.Run(tc.name, func() {
137137
intent := charges.NewChargeIntent(creditpurchase.Intent{
138138
Intent: meta.Intent{
139-
Name: "Credit Purchase",
140-
ManagedBy: billing.ManuallyManagedLine,
141-
CustomerID: cust.ID,
142-
Currency: USD,
143-
ServicePeriod: servicePeriod,
144-
BillingPeriod: servicePeriod,
145-
FullServicePeriod: servicePeriod,
139+
ManagedBy: billing.ManuallyManagedLine,
140+
CustomerID: cust.ID,
141+
Currency: USD,
142+
},
143+
IntentMutableFields: creditpurchase.IntentMutableFields{
144+
IntentMutableFields: meta.IntentMutableFields{
145+
Name: "Credit Purchase",
146+
ServicePeriod: servicePeriod,
147+
BillingPeriod: servicePeriod,
148+
FullServicePeriod: servicePeriod,
149+
},
150+
CreditAmount: alpacadecimal.NewFromFloat(100),
151+
Settlement: tc.settlement,
146152
},
147-
CreditAmount: alpacadecimal.NewFromFloat(100),
148-
Settlement: tc.settlement,
149153
})
150154

151155
res, err := s.Charges.Create(ctx, charges.CreateInput{

openmeter/billing/charges/service/lineage_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,15 @@ func (s *CreditRealizationLineageTestSuite) TestBackfillAdvanceLineageSegmentsFi
238238

239239
ns := s.GetUniqueNamespace("charges-service-lineage-feature-backfill")
240240
customerID := ulid.Make().String()
241-
apiLineageID := s.createAdvanceLineageForBackfill(ctx, ns, customerID, []string{"api-calls"}, alpacadecimal.NewFromInt(40))
242-
storageLineageID := s.createAdvanceLineageForBackfill(ctx, ns, customerID, []string{"storage"}, alpacadecimal.NewFromInt(30))
241+
customCurrency := currencyx.Code("CREDITS")
242+
apiLineageID := s.createAdvanceLineageForBackfill(ctx, ns, customerID, customCurrency, []string{"api-calls"}, alpacadecimal.NewFromInt(40))
243+
storageLineageID := s.createAdvanceLineageForBackfill(ctx, ns, customerID, customCurrency, []string{"storage"}, alpacadecimal.NewFromInt(30))
243244
backingTransactionGroupID := ulid.Make().String()
244245

245246
err = service.BackfillAdvanceLineageSegments(ctx, lineage.BackfillAdvanceLineageSegmentsInput{
246247
Namespace: ns,
247248
CustomerID: customerID,
248-
Currency: currencyx.Code(currency.USD),
249+
Currency: customCurrency,
249250
Amount: alpacadecimal.NewFromInt(50),
250251
BackingTransactionGroupID: backingTransactionGroupID,
251252
FeatureFilters: []string{"api-calls"},
@@ -439,7 +440,7 @@ func (s *CreditRealizationLineageTestSuite) mustListLineages(namespace string, r
439440
return out
440441
}
441442

442-
func (s *CreditRealizationLineageTestSuite) createAdvanceLineageForBackfill(ctx context.Context, namespace string, customerID string, advanceFeatures []string, amount alpacadecimal.Decimal) string {
443+
func (s *CreditRealizationLineageTestSuite) createAdvanceLineageForBackfill(ctx context.Context, namespace string, customerID string, currencyCode currencyx.Code, advanceFeatures []string, amount alpacadecimal.Decimal) string {
443444
s.T().Helper()
444445

445446
chargeID := ulid.Make().String()
@@ -458,7 +459,7 @@ func (s *CreditRealizationLineageTestSuite) createAdvanceLineageForBackfill(ctx
458459
SetChargeID(chargeID).
459460
SetRootRealizationID(ulid.Make().String()).
460461
SetCustomerID(customerID).
461-
SetCurrency(currencyx.Code(currency.USD)).
462+
SetCurrency(currencyCode).
462463
SetOriginKind(creditrealization.LineageOriginKindAdvance).
463464
SetAdvanceFeatures(pq.StringArray(advanceFeatures)).
464465
Save(ctx)

0 commit comments

Comments
 (0)