Skip to content

Commit b2357da

Browse files
committed
fix: grants service should not bypass charges service for now
1 parent c842d91 commit b2357da

5 files changed

Lines changed: 378 additions & 4 deletions

File tree

.agents/skills/charges/SKILL.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,24 @@ Current expected behavior:
214214
- ledger handlers may still defensively tolerate zero and return `ledgertransaction.GroupReference{}`
215215
- when a service proceeds with non-zero invoice accrual, it must require a non-empty transaction group reference before storing accrued usage
216216

217+
## HTTP/API Conversion
218+
219+
Credit-purchase charges have an API/domain enum mismatch for promotional grants.
220+
221+
Rules:
222+
223+
- in the billing domain, promotional credit grants are represented as `creditpurchase.SettlementTypePromotional`
224+
- in the v3 customer credits API, the same case is represented as `funding_method=none`
225+
- v3 API responses for promotional grants must omit the `purchase` block entirely
226+
- conversion code in `api/v3/handlers/customers/credits` must map this case explicitly instead of treating `promotional` as an unsupported settlement type
227+
228+
Important files:
229+
230+
- `api/v3/handlers/customers/credits/convert.go`
231+
- `openmeter/billing/charges/creditpurchase/settlement.go`
232+
- `openmeter/billing/creditgrant/service/service.go`
233+
- `api/spec/packages/aip/src/customers/credits/grant.tsp`
234+
217235
## Realization Helper Subpackages
218236

219237
Use small type-specific realization helper subpackages to keep charge services and state machines from becoming kitchen-sink orchestration layers.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ func toAPICreditGrantPurchase(charge creditpurchase.Charge) (*creditGrantPurchas
142142
SettlementStatus: &settlementStatus,
143143
}, nil
144144

145+
case creditpurchase.SettlementTypePromotional:
146+
return nil, nil
147+
145148
default:
146149
return nil, fmt.Errorf("invalid settlement type: %s", settlement.Type())
147150
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package customerscredits
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/alpacahq/alpacadecimal"
8+
"github.com/stretchr/testify/require"
9+
10+
api "github.com/openmeterio/openmeter/api/v3"
11+
"github.com/openmeterio/openmeter/openmeter/billing/charges/creditpurchase"
12+
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
13+
"github.com/openmeterio/openmeter/pkg/currencyx"
14+
"github.com/openmeterio/openmeter/pkg/models"
15+
)
16+
17+
func TestToAPIBillingCreditGrantPromotional(t *testing.T) {
18+
now := time.Date(2026, time.April, 17, 10, 0, 0, 0, time.UTC)
19+
20+
charge := creditpurchase.Charge{
21+
ChargeBase: creditpurchase.ChargeBase{
22+
ManagedResource: meta.ManagedResource{
23+
NamespacedModel: models.NamespacedModel{
24+
Namespace: "ns",
25+
},
26+
ManagedModel: models.ManagedModel{
27+
CreatedAt: now,
28+
UpdatedAt: now,
29+
},
30+
ID: "grant-1",
31+
},
32+
Intent: creditpurchase.Intent{
33+
Intent: meta.Intent{
34+
Name: "Promo credits",
35+
CustomerID: "cust-1",
36+
Currency: currencyx.Code("USD"),
37+
},
38+
CreditAmount: alpacadecimal.RequireFromString("25"),
39+
Settlement: creditpurchase.NewSettlement(creditpurchase.PromotionalSettlement{}),
40+
},
41+
Status: creditpurchase.StatusActive,
42+
},
43+
}
44+
45+
grant, err := toAPIBillingCreditGrant(charge)
46+
require.NoError(t, err)
47+
require.Equal(t, api.BillingCreditFundingMethodNone, grant.FundingMethod)
48+
require.Nil(t, grant.Purchase)
49+
require.Equal(t, "25", grant.Amount)
50+
}

openmeter/billing/creditgrant/service/service.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,37 @@ func (s *service) Create(ctx context.Context, input creditgrant.CreateInput) (cr
8484
// Build the credit purchase intent
8585
intent := toIntent(input)
8686

87-
result, err := s.creditPurchaseService.Create(ctx, creditpurchase.CreateInput{
87+
result, err := s.chargesService.Create(ctx, charges.CreateInput{
8888
Namespace: input.Namespace,
89-
Intent: intent,
89+
Intents: charges.ChargeIntents{charges.NewChargeIntent(intent)},
9090
})
9191
if err != nil {
92-
return creditpurchase.Charge{}, fmt.Errorf("create credit purchase charge: %w", err)
92+
return creditpurchase.Charge{}, fmt.Errorf("create credit grant charge: %w", err)
9393
}
9494

95-
return result.Charge, nil
95+
if len(result) != 1 {
96+
return creditpurchase.Charge{}, fmt.Errorf("expected 1 created charge, got %d", len(result))
97+
}
98+
99+
createdChargeID, err := result[0].GetChargeID()
100+
if err != nil {
101+
return creditpurchase.Charge{}, fmt.Errorf("get created charge id: %w", err)
102+
}
103+
104+
charge, err := s.chargesService.GetByID(ctx, charges.GetByIDInput{
105+
ChargeID: createdChargeID,
106+
Expands: meta.Expands{meta.ExpandRealizations},
107+
})
108+
if err != nil {
109+
return creditpurchase.Charge{}, fmt.Errorf("get created credit grant charge: %w", err)
110+
}
111+
112+
cpCharge, err := charge.AsCreditPurchaseCharge()
113+
if err != nil {
114+
return creditpurchase.Charge{}, fmt.Errorf("charge is not a credit purchase: %w", err)
115+
}
116+
117+
return cpCharge, nil
96118
}
97119

98120
func (s *service) Get(ctx context.Context, input creditgrant.GetInput) (creditpurchase.Charge, error) {

0 commit comments

Comments
 (0)