Skip to content

Commit e66efd0

Browse files
committed
feat(customcurrencies): add configuration flag
1 parent 9a09c0a commit e66efd0

19 files changed

Lines changed: 120 additions & 48 deletions

File tree

.agents/skills/charges/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ 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
646+
- keep custom-currency support disabled on default charge service instances so tests continue covering the production unsupported boundary; production initialization flows through `credits.customCurrenciesEnabled` into each type-specific service config, while tests should use a semantic suite helper that constructs an enabled service instead of repeating `SetEnableCustomCurrency` interface assertions at call sites
647647
- when testing timestamp truncation, use sub-second fixtures and assert the persisted charge/run fields are second-aligned after create/advance
648648
- `time.Time` fields on domain models are value typed; use `s.False(ts.IsZero())` instead of `s.NotNil(ts)` when asserting they are populated
649649
- 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.customCurrenciesEnabled {
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.customCurrenciesEnabled {
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: 12 additions & 6 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
@@ -16,19 +19,22 @@ type Handler interface {
1619
}
1720

1821
type handler struct {
19-
resolveNamespace func(ctx context.Context) (string, error)
20-
options []httptransport.HandlerOption
21-
service currencies.Service
22+
resolveNamespace func(ctx context.Context) (string, error)
23+
options []httptransport.HandlerOption
24+
service currencies.Service
25+
customCurrenciesEnabled bool
2226
}
2327

2428
func New(
2529
resolveNamespace func(ctx context.Context) (string, error),
2630
currencyService currencies.Service,
31+
customCurrenciesEnabled bool,
2732
options ...httptransport.HandlerOption,
2833
) Handler {
2934
return &handler{
30-
resolveNamespace: resolveNamespace,
31-
options: options,
32-
service: currencyService,
35+
resolveNamespace: resolveNamespace,
36+
options: options,
37+
service: currencyService,
38+
customCurrenciesEnabled: customCurrenciesEnabled,
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 TestCustomCurrencyMutationsAreDisabledByDefault(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.CustomCurrenciesEnabled, httptransport.WithErrorHandler(config.ErrorHandler))
336336

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

app/common/charges.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,17 @@ func NewChargesFlatFeeService(
213213
locker *lockr.Locker,
214214
ratingService rating.Service,
215215
currenciesService currencies.Service,
216+
creditsConfig config.CreditsConfiguration,
216217
) (flatfee.Service, error) {
217218
flatFeeSvc, err := flatfeeservice.New(flatfeeservice.Config{
218-
Adapter: flatFeeAdapter,
219-
Handler: flatFeeHandler,
220-
Lineage: lineageService,
221-
MetaAdapter: metaAdapter,
222-
Locker: locker,
223-
RatingService: ratingService,
224-
Currencies: currenciesService,
219+
Adapter: flatFeeAdapter,
220+
Handler: flatFeeHandler,
221+
Lineage: lineageService,
222+
MetaAdapter: metaAdapter,
223+
Locker: locker,
224+
RatingService: ratingService,
225+
Currencies: currenciesService,
226+
CustomCurrenciesEnabled: creditsConfig.CustomCurrenciesEnabled,
225227
})
226228
if err != nil {
227229
return nil, fmt.Errorf("failed to create charges flat fee service: %w", err)
@@ -259,6 +261,7 @@ func NewChargesUsageBasedService(
259261
ratingService rating.Service,
260262
currenciesService currencies.Service,
261263
streamingConnector streaming.Connector,
264+
creditsConfig config.CreditsConfiguration,
262265
) (usagebased.Service, error) {
263266
usageBasedSvc, err := usagebasedservice.New(usagebasedservice.Config{
264267
Adapter: usageBasedAdapter,
@@ -272,6 +275,7 @@ func NewChargesUsageBasedService(
272275
RatingService: ratingService,
273276
Currencies: currenciesService,
274277
StreamingConnector: streamingConnector,
278+
CustomCurrenciesEnabled: creditsConfig.CustomCurrenciesEnabled,
275279
})
276280
if err != nil {
277281
return nil, fmt.Errorf("failed to create charges usage based service: %w", err)
@@ -317,12 +321,14 @@ func NewChargesCreditPurchaseService(
317321
creditPurchaseHandler creditpurchase.Handler,
318322
lineageService lineage.Service,
319323
metaAdapter meta.Adapter,
324+
creditsConfig config.CreditsConfiguration,
320325
) (creditpurchase.Service, error) {
321326
creditPurchaseSvc, err := creditpurchaseservice.New(creditpurchaseservice.Config{
322-
Adapter: creditPurchaseAdapter,
323-
Handler: creditPurchaseHandler,
324-
Lineage: lineageService,
325-
MetaAdapter: metaAdapter,
327+
Adapter: creditPurchaseAdapter,
328+
Handler: creditPurchaseHandler,
329+
Lineage: lineageService,
330+
MetaAdapter: metaAdapter,
331+
CustomCurrenciesEnabled: creditsConfig.CustomCurrenciesEnabled,
326332
})
327333
if err != nil {
328334
return nil, fmt.Errorf("failed to create charges credit purchase service: %w", err)
@@ -478,7 +484,7 @@ func newChargesRegistry(
478484
return nil, err
479485
}
480486

481-
flatFeeSvc, err := NewChargesFlatFeeService(flatFeeAdapter, flatFeeHandler, lineageService, metaAdapter, locker, ratingService, currenciesService)
487+
flatFeeSvc, err := NewChargesFlatFeeService(flatFeeAdapter, flatFeeHandler, lineageService, metaAdapter, locker, ratingService, currenciesService, creditsConfig)
482488
if err != nil {
483489
return nil, err
484490
}
@@ -509,6 +515,7 @@ func newChargesRegistry(
509515
ratingService,
510516
currenciesService,
511517
streamingConnector,
518+
creditsConfig,
512519
)
513520
if err != nil {
514521
return nil, err
@@ -523,7 +530,7 @@ func newChargesRegistry(
523530
return nil, err
524531
}
525532

526-
creditPurchaseSvc, err := NewChargesCreditPurchaseService(creditPurchaseAdapter, creditPurchaseHandler, lineageService, metaAdapter)
533+
creditPurchaseSvc, err := NewChargesCreditPurchaseService(creditPurchaseAdapter, creditPurchaseHandler, lineageService, metaAdapter, creditsConfig)
527534
if err != nil {
528535
return nil, err
529536
}

app/config/config_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ func TestComplete(t *testing.T) {
195195
Credits: CreditsConfiguration{
196196
Enabled: false,
197197
EnableCreditThenInvoice: false,
198+
CustomCurrenciesEnabled: false,
198199
},
199200
UnitConfig: UnitConfigConfiguration{
200201
Enabled: false,

app/config/credits.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
type CreditsConfiguration struct {
1010
Enabled bool `yaml:"enabled"`
1111
EnableCreditThenInvoice bool `yaml:"enableCreditThenInvoice"`
12+
CustomCurrenciesEnabled bool `yaml:"customCurrenciesEnabled"`
1213
}
1314

1415
func (c CreditsConfiguration) Validate() error {
@@ -24,4 +25,5 @@ func ConfigureCredits(v *viper.Viper, prefixes ...string) {
2425

2526
v.SetDefault(prefixer("enabled"), false)
2627
v.SetDefault(prefixer("enableCreditThenInvoice"), false)
28+
v.SetDefault(prefixer("customCurrenciesEnabled"), false)
2729
}

0 commit comments

Comments
 (0)