Skip to content

Commit 1d58ed1

Browse files
committed
feat(customcurrencies): gate API with credits flag
1 parent 9a09c0a commit 1d58ed1

20 files changed

Lines changed: 66 additions & 219 deletions

File tree

.agents/skills/charges/SKILL.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,6 @@ Use these conventions for lifecycle tests:
643643
- for credit-only charges (usage-based or flat fee), handler callbacks must not return credit allocations above the requested amount; exact allocation paths must return allocations that sum to the requested amount
644644
- for flat fee credit-only tests, use `mustAdvanceFlatFeeCharges(...)` helper — it filters the advance result to flat fee charges only
645645
- for credit-purchase state-machine unit tests, use testify `mock.Mock` with `On(...).Run(...).Return(...).Once()` for expected handler callbacks so missing or unexpected calls fail; in service-suite tests, leave callbacks unset when validating that a flow fails before callbacks, because the shared `CreditPurchaseTestHandler` already errors if an unset callback is invoked
646-
- keep custom-currency support disabled on default charge service instances so tests continue covering the production unsupported boundary; when a test needs it enabled, use a semantic suite helper that constructs and enables the service instead of repeating `SetEnableCustomCurrency` interface assertions at call sites
647646
- when testing timestamp truncation, use sub-second fixtures and assert the persisted charge/run fields are second-aligned after create/advance
648647
- `time.Time` fields on domain models are value typed; use `s.False(ts.IsZero())` instead of `s.NotNil(ts)` when asserting they are populated
649648
- cover the temporary shrink/extend remap path as well; it synthesizes new intents and must normalize the replacement period ends before re-create

api/v3/handlers/currencies/create.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/openmeterio/openmeter/pkg/currencyx"
1414
"github.com/openmeterio/openmeter/pkg/framework/commonhttp"
1515
"github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
16+
"github.com/openmeterio/openmeter/pkg/models"
1617
)
1718

1819
type (
@@ -24,6 +25,10 @@ type (
2425
func (h *handler) CreateCurrency() CreateCurrencyHandler {
2526
return httptransport.NewHandler(
2627
func(ctx context.Context, r *http.Request) (CreateCurrencyRequest, error) {
28+
if !h.creditsEnabled {
29+
return CreateCurrencyRequest{}, models.NewGenericValidationError(errCustomCurrenciesDisabled)
30+
}
31+
2732
ns, err := h.resolveNamespace(ctx)
2833
if err != nil {
2934
return CreateCurrencyRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)

api/v3/handlers/currencies/create_cost_basis.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/openmeterio/openmeter/pkg/currencyx"
1414
"github.com/openmeterio/openmeter/pkg/framework/commonhttp"
1515
"github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
16+
"github.com/openmeterio/openmeter/pkg/models"
1617
)
1718

1819
type (
@@ -24,6 +25,10 @@ type (
2425
func (h *handler) CreateCostBasis() CreateCostBasisHandler {
2526
return httptransport.NewHandlerWithArgs(
2627
func(ctx context.Context, r *http.Request, currencyID string) (CreateCostBasisRequest, error) {
28+
if !h.creditsEnabled {
29+
return CreateCostBasisRequest{}, models.NewGenericValidationError(errCustomCurrenciesDisabled)
30+
}
31+
2732
ns, err := h.resolveNamespace(ctx)
2833
if err != nil {
2934
return CreateCostBasisRequest{}, err

api/v3/handlers/currencies/handler.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package currencies
22

33
import (
44
"context"
5+
"errors"
56

67
"github.com/openmeterio/openmeter/openmeter/currencies"
78
"github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
89
)
910

11+
var errCustomCurrenciesDisabled = errors.New("custom currencies are not enabled on this deployment of OpenMeter")
12+
1013
type Handler interface {
1114
ListCurrencies() ListCurrenciesHandler
1215
CreateCurrency() CreateCurrencyHandler
@@ -19,16 +22,19 @@ type handler struct {
1922
resolveNamespace func(ctx context.Context) (string, error)
2023
options []httptransport.HandlerOption
2124
service currencies.Service
25+
creditsEnabled bool
2226
}
2327

2428
func New(
2529
resolveNamespace func(ctx context.Context) (string, error),
2630
currencyService currencies.Service,
31+
creditsEnabled bool,
2732
options ...httptransport.HandlerOption,
2833
) Handler {
2934
return &handler{
3035
resolveNamespace: resolveNamespace,
3136
options: options,
3237
service: currencyService,
38+
creditsEnabled: creditsEnabled,
3339
}
3440
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package currencies
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestCustomCurrencyMutationsRequireCredits(t *testing.T) {
13+
handler := New(func(context.Context) (string, error) {
14+
return "test", nil
15+
}, nil, false)
16+
17+
t.Run("create currency", func(t *testing.T) {
18+
request := httptest.NewRequest(http.MethodPost, "/api/v3/openmeter/currencies/custom", nil)
19+
response := httptest.NewRecorder()
20+
21+
handler.CreateCurrency().ServeHTTP(response, request)
22+
23+
require.Equal(t, http.StatusBadRequest, response.Code)
24+
})
25+
26+
t.Run("create cost basis", func(t *testing.T) {
27+
request := httptest.NewRequest(http.MethodPost, "/api/v3/openmeter/currencies/custom/currency-id/cost-bases", nil)
28+
response := httptest.NewRecorder()
29+
30+
handler.CreateCostBasis().With("currency-id").ServeHTTP(response, request)
31+
32+
require.Equal(t, http.StatusBadRequest, response.Code)
33+
})
34+
}

api/v3/handlers/currencies/list_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestListCurrenciesFilterByType(t *testing.T) {
5353
service := &listCurrenciesService{}
5454
handler := New(func(context.Context) (string, error) {
5555
return "test", nil
56-
}, service)
56+
}, service, true)
5757

5858
request := httptest.NewRequest(http.MethodGet, "/api/v3/currencies", nil)
5959
response := httptest.NewRecorder()

api/v3/server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ func NewServer(config *Config) (*Server, error) {
332332
plansHandler := planshandler.New(resolveNamespace, config.PlanService, config.UnitConfig.Enabled, httptransport.WithErrorHandler(config.ErrorHandler))
333333
planAddonsHandler := planaddonshandler.New(resolveNamespace, config.PlanService, config.PlanAddonService, httptransport.WithErrorHandler(config.ErrorHandler))
334334
taxcodesHandler := taxcodeshandler.New(resolveNamespace, config.TaxCodeService, httptransport.WithErrorHandler(config.ErrorHandler))
335-
currenciesHandler := currencieshandler.New(resolveNamespace, config.CurrencyService, httptransport.WithErrorHandler(config.ErrorHandler))
335+
currenciesHandler := currencieshandler.New(resolveNamespace, config.CurrencyService, config.Credits.Enabled, httptransport.WithErrorHandler(config.ErrorHandler))
336336

337337
var chargesH chargeshandler.Handler
338338
if config.ChargeService != nil {

openmeter/billing/charges/creditpurchase/service/create.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88

99
"github.com/openmeterio/openmeter/openmeter/billing"
1010
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
11-
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
1211
"github.com/openmeterio/openmeter/openmeter/productcatalog"
1312
"github.com/openmeterio/openmeter/pkg/framework/transaction"
1413
"github.com/openmeterio/openmeter/pkg/models"
@@ -20,10 +19,6 @@ func (s *service) Create(ctx context.Context, input creditpurchase.CreateInput)
2019
}
2120

2221
return transaction.Run(ctx, s.adapter, func(ctx context.Context) (creditpurchase.ChargeWithGatheringLine, error) {
23-
if input.Intent.Currency.IsCustom() && !s.enableCustomCurrency.Load() {
24-
return creditpurchase.ChargeWithGatheringLine{}, fmt.Errorf("custom currency %s is not supported for credit purchases: %w", input.Intent.Currency.GetCode(), meta.ErrCustomCurrencyNotSupported)
25-
}
26-
2722
input.Intent = input.Intent.Normalized()
2823

2924
// Let's create the credit purchase charge

openmeter/billing/charges/creditpurchase/service/service.go

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package service
33
import (
44
"errors"
55
"fmt"
6-
"sync/atomic"
7-
"testing"
86

97
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
108
creditpurchaserealizations "github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase/service/realizations"
@@ -65,20 +63,9 @@ func New(config Config) (creditpurchase.Service, error) {
6563
}
6664

6765
type service struct {
68-
adapter creditpurchase.Adapter
69-
metaAdapter meta.Adapter
70-
handler creditpurchase.Handler
71-
lineage lineage.Service
72-
realizations *creditpurchaserealizations.Service
73-
enableCustomCurrency atomic.Bool
74-
}
75-
76-
func (s *service) SetEnableCustomCurrency(t *testing.T, enabled bool) error {
77-
if t == nil {
78-
return errors.New("testing is nil")
79-
}
80-
81-
t.Helper()
82-
s.enableCustomCurrency.Store(enabled)
83-
return nil
66+
adapter creditpurchase.Adapter
67+
metaAdapter meta.Adapter
68+
handler creditpurchase.Handler
69+
lineage lineage.Service
70+
realizations *creditpurchaserealizations.Service
8471
}

openmeter/billing/charges/flatfee/service/create.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ func (s *service) Create(ctx context.Context, input flatfee.CreateInput) ([]flat
3333
now := clock.Now().UTC()
3434
// Let's create all the flat fee charges in bulk
3535
intentsWithStatus, err := slicesx.MapWithErr(input.Intents, func(intent flatfee.Intent) (flatfee.IntentWithInitialStatus, error) {
36-
if intent.Currency.IsCustom() && !s.enableCustomCurrency.Load() {
37-
return flatfee.IntentWithInitialStatus{}, fmt.Errorf("creating flat fee charge with custom currency %q: %w", intent.Currency.GetCode(), meta.ErrCustomCurrencyNotSupported)
38-
}
39-
4036
chargeIntent := intent.Normalized()
4137

4238
var resolvedCostBasis *costbasis.State

0 commit comments

Comments
 (0)