Skip to content

Commit 7690598

Browse files
authored
fix: settle promotional credits (#4192)
1 parent 532db5c commit 7690598

3 files changed

Lines changed: 56 additions & 15 deletions

File tree

openmeter/ledger/chargeadapter/creditpurchase.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,30 @@ func (h *creditPurchaseHandler) issueCreditPurchase(ctx context.Context, charge
231231
})
232232
}
233233

234+
switch charge.Intent.Settlement.Type() {
235+
case chargecreditpurchase.SettlementTypePromotional:
236+
// Promotional grants settle immediately through wash so the credited FBO balance
237+
// does not leave an unsettled receivable behind.
238+
templates = append(templates,
239+
transactions.FundCustomerReceivableTemplate{
240+
At: charge.CreatedAt,
241+
Amount: charge.Intent.CreditAmount,
242+
Currency: charge.Intent.Currency,
243+
CostBasis: &costBasis,
244+
},
245+
transactions.SettleCustomerReceivablePaymentTemplate{
246+
At: charge.CreatedAt,
247+
Amount: charge.Intent.CreditAmount,
248+
Currency: charge.Intent.Currency,
249+
CostBasis: &costBasis,
250+
},
251+
)
252+
case chargecreditpurchase.SettlementTypeExternal, chargecreditpurchase.SettlementTypeInvoice:
253+
// Deferred settlement modes are handled by later lifecycle events.
254+
default:
255+
return ledgertransaction.GroupReference{}, fmt.Errorf("unsupported settlement type: %s", charge.Intent.Settlement.Type())
256+
}
257+
234258
if len(templates) == 0 {
235259
return ledgertransaction.GroupReference{}, nil
236260
}

openmeter/ledger/chargeadapter/creditpurchase_test.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,27 @@ func TestOnPromotionalCreditPurchase(t *testing.T) {
3535
)
3636

3737
require.True(t, env.sumBalance(t, env.fboSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.NewFromInt(100)))
38-
require.True(t, env.sumBalance(t, env.receivableSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.NewFromInt(-100)))
38+
require.True(t, env.sumBalance(t, env.receivableSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.Zero))
39+
require.True(t, env.sumBalance(t, env.authorizedReceivableSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.Zero))
40+
require.True(t, env.sumBalance(t, env.washSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.NewFromInt(-100)))
41+
}
42+
43+
func TestOnPromotionalCreditPurchase_BacksAdvanceBeforeTopUp(t *testing.T) {
44+
env := newCreditPurchaseHandlerTestEnv(t)
45+
env.createAdvanceExposure(t, alpacadecimal.NewFromInt(40))
46+
47+
charge := env.newPromotionalCharge(alpacadecimal.NewFromInt(100))
48+
ref, err := env.handler.OnPromotionalCreditPurchase(t.Context(), charge)
49+
require.NoError(t, err)
50+
require.NotEmpty(t, ref.TransactionGroupID)
51+
52+
require.True(t, env.sumBalance(t, env.receivableSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.Zero))
53+
require.True(t, env.sumBalance(t, env.authorizedReceivableSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.Zero))
54+
require.True(t, env.sumBalance(t, env.unknownReceivableSubAccount(t)).Equal(alpacadecimal.Zero))
55+
require.True(t, env.sumBalance(t, env.unknownAccruedSubAccount(t)).Equal(alpacadecimal.Zero))
56+
require.True(t, env.sumBalance(t, env.accruedSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.NewFromInt(40)))
57+
require.True(t, env.sumBalance(t, env.fboSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.NewFromInt(60)))
58+
require.True(t, env.sumBalance(t, env.washSubAccount(t, alpacadecimal.Zero)).Equal(alpacadecimal.NewFromInt(-100)))
3959
}
4060

4161
func TestOnCreditPurchaseInitiated(t *testing.T) {

test/credits/sanity_test.go

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -886,17 +886,14 @@ func (s *CreditsTestSuite) TestUsageBasedCreditThenInvoicePaymentLifecycle() {
886886
s.NotNil(updatedCharge.Realizations[0].InvoiceUsage)
887887
s.Equal(float64(7.5), updatedCharge.Realizations[0].InvoiceUsage.Totals.Total.InexactFloat64())
888888

889-
// Aggregate open receivable still includes the promotional grant route.
890-
// At this point:
891-
// - promotional grant receivable (cost basis 0) is -5
892-
// - invoice-backed receivable (cost basis 1) is -7.5
893-
// so the aggregate open receivable is -12.5.
894-
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&promoCostBasis), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-5)))
889+
// Promotional grants settle immediately through wash, so only the
890+
// invoice-backed receivable remains open at this point.
891+
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&promoCostBasis), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.Zero))
895892
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&invoiceCostBasis), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-7.5)))
896-
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-12.5)))
893+
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-7.5)))
897894
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusAuthorized).Equal(alpacadecimal.Zero))
898895
s.True(s.mustCustomerAccruedBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal]()).Equal(alpacadecimal.NewFromFloat(12.5)))
899-
s.True(s.mustWashBalance(ns, USD, mo.None[*alpacadecimal.Decimal]()).Equal(alpacadecimal.Zero))
896+
s.True(s.mustWashBalance(ns, USD, mo.None[*alpacadecimal.Decimal]()).Equal(alpacadecimal.NewFromFloat(-5)))
900897
})
901898

902899
s.Run("the payment is authorized", func() {
@@ -914,12 +911,12 @@ func (s *CreditsTestSuite) TestUsageBasedCreditThenInvoicePaymentLifecycle() {
914911
s.NotNil(updatedCharge.Realizations[0].Payment.Authorized)
915912
s.Nil(updatedCharge.Realizations[0].Payment.Settled)
916913

917-
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&promoCostBasis), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-5)))
914+
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&promoCostBasis), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.Zero))
918915
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&invoiceCostBasis), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-7.5)))
919-
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-12.5)))
916+
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-7.5)))
920917
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&invoiceCostBasis), ledger.TransactionAuthorizationStatusAuthorized).Equal(alpacadecimal.NewFromFloat(7.5)))
921918
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusAuthorized).Equal(alpacadecimal.NewFromFloat(7.5)))
922-
s.True(s.mustWashBalance(ns, USD, mo.None[*alpacadecimal.Decimal]()).Equal(alpacadecimal.NewFromFloat(-7.5)))
919+
s.True(s.mustWashBalance(ns, USD, mo.None[*alpacadecimal.Decimal]()).Equal(alpacadecimal.NewFromFloat(-12.5)))
923920
})
924921

925922
s.Run("the payment is settled and the charge reaches final", func() {
@@ -939,12 +936,12 @@ func (s *CreditsTestSuite) TestUsageBasedCreditThenInvoicePaymentLifecycle() {
939936
s.Equal(payment.StatusSettled, updatedCharge.Realizations[0].Payment.Status)
940937
s.NotNil(updatedCharge.Realizations[0].Payment.Settled)
941938

942-
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&promoCostBasis), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-5)))
939+
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&promoCostBasis), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.Zero))
943940
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&invoiceCostBasis), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.Zero))
944-
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.NewFromFloat(-5)))
941+
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusOpen).Equal(alpacadecimal.Zero))
945942
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.Some(&invoiceCostBasis), ledger.TransactionAuthorizationStatusAuthorized).Equal(alpacadecimal.Zero))
946943
s.True(s.mustCustomerReceivableBalance(cust.GetID(), USD, mo.None[*alpacadecimal.Decimal](), ledger.TransactionAuthorizationStatusAuthorized).Equal(alpacadecimal.Zero))
947-
s.True(s.mustWashBalance(ns, USD, mo.None[*alpacadecimal.Decimal]()).Equal(alpacadecimal.NewFromFloat(-7.5)))
944+
s.True(s.mustWashBalance(ns, USD, mo.None[*alpacadecimal.Decimal]()).Equal(alpacadecimal.NewFromFloat(-12.5)))
948945
})
949946
}
950947

0 commit comments

Comments
 (0)