Skip to content

Commit 313ab35

Browse files
authored
feat: Update current-account domain for dimension-agnostic Amount (#1242)
* feat: Update current-account domain for dimension-agnostic Amount Replace Money with Amount throughout the current-account service to support multi-asset accounts (KWH, CARBON_CREDIT, GPU_HOUR, etc.). - Add Amount = sharedamount.Amount type alias to domain/quantity.go - Add NewAmountFromInstrument constructor re-export - Keep Money = Amount and ErrCurrencyMismatch as deprecated aliases - Update NewCurrentAccountWithDimension to use NewAmountFromInstrument - Replace Currency()/CurrencyCode() with InstrumentCode() throughout - Replace ErrCurrencyMismatch with ErrInstrumentMismatch in account.go - Update persistence layer to use NewAmountFromInstrument - Update service layer: grpc_mappers, lien_service, deposit/withdrawal - Update all tests: use InstrumentCode(), toMinorUnits() helper - Add multi-asset tests: KWH, CARBON_CREDIT account creation/deposit Closes asset-agnostic-accounts#16 * fix: Update balance_ownership_test.go for cadomain.Amount API Replace sharedmoney.Money with cadomain.Amount in MockCurrentAccountService: - Use cadomain.NewMoney() to convert legacy money.Money for Deposit/Withdraw - Use caBalance.ToMinorUnits() + money.NewFromMinorUnits() for GetBalance - Remove unused sharedmoney import EOF * fix: set SAGA_ASSET_DIR in E2E workflow for saga script loading The unified binary loads saga scripts at startup via loadSagaAsset(), which resolves paths relative to SAGA_ASSET_DIR env var or the executable directory. In the E2E workflow, the binary is at dist/meridian but saga files are in the repo checkout root, so assets were not found. Set SAGA_ASSET_DIR=$(pwd) so the binary finds saga scripts and handlers schema at services/reference-data/saga/ and shared/pkg/saga/schema/. * fix: add precision argument to NewCurrentAccountWithDimension test calls After rebasing with PR #1241 (instrument precision resolution), the NewCurrentAccountWithDimension function gained a required precision int parameter. Update test calls for non-CURRENCY dimensions (ENERGY, CARBON) to pass precision=0, as precision is not validated for non-currency instruments. * fix: restrict NewMoneyFromInstrument to CURRENCY dimension only NewMoneyFromInstrument hardcoded precision=2 and would silently mis-scale non-CURRENCY dimensions (ENERGY, CARBON) when called with incorrect dimension. Add an explicit CURRENCY dimension guard that returns ErrInvalidCurrency for non-CURRENCY inputs, preserving the money-only semantics of this deprecated compatibility API. --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 0ce4eef commit 313ab35

24 files changed

Lines changed: 328 additions & 169 deletions

.github/workflows/e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ jobs:
154154
run: |
155155
DATABASE_URL="postgres://root@localhost:26257/defaultdb?sslmode=disable" \
156156
LOCAL_DEV_MODE=true \
157+
SAGA_ASSET_DIR="$(pwd)" \
157158
./dist/meridian >/tmp/meridian.log 2>&1 &
158159
159160
- name: Seed dev tenant and apply manifest

services/current-account/adapters/persistence/lien_repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ func toLienEntity(lien *domain.Lien) *LienEntity {
332332
ID: lien.ID,
333333
AccountID: lien.AccountID,
334334
AmountCents: lien.Amount.ToMinorUnitsUnchecked(),
335-
Currency: string(lien.Amount.Currency()),
335+
Currency: lien.Amount.InstrumentCode(),
336336
BucketID: lien.BucketID,
337337
Status: string(lien.Status),
338338
PaymentOrderReference: lien.PaymentOrderReference,

services/current-account/adapters/persistence/lien_repository_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func TestLienRepository_Create(t *testing.T) {
8787
amountCents, err := retrieved.Amount.ToMinorUnits()
8888
require.NoError(t, err)
8989
assert.Equal(t, int64(10000), amountCents)
90-
assert.Equal(t, domain.CurrencyGBP, retrieved.Amount.Currency())
90+
assert.Equal(t, "GBP", retrieved.Amount.InstrumentCode())
9191
assert.Equal(t, "bucket-abc", retrieved.BucketID)
9292
assert.Equal(t, domain.LienStatusActive, retrieved.Status)
9393
assert.Equal(t, "PO-001", retrieved.PaymentOrderReference)

services/current-account/adapters/persistence/repository.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,11 +691,11 @@ func toDomain(entity *CurrentAccountEntity) (domain.CurrentAccount, error) {
691691
// Use entity's in-memory balance fields if populated (e.g., from recent save),
692692
// otherwise initialize with zero values.
693693
// The service layer should populate from Position Keeping for authoritative balance.
694-
balance, err := domain.NewMoneyFromInstrument(entity.InstrumentCode, entity.Dimension, entity.Balance)
694+
balance, err := domain.NewAmountFromInstrument(entity.InstrumentCode, entity.Dimension, 0, entity.Balance)
695695
if err != nil {
696696
return domain.CurrentAccount{}, fmt.Errorf("failed to create balance: %w", err)
697697
}
698-
availableBalance, err := domain.NewMoneyFromInstrument(entity.InstrumentCode, entity.Dimension, entity.AvailableBalance)
698+
availableBalance, err := domain.NewAmountFromInstrument(entity.InstrumentCode, entity.Dimension, 0, entity.AvailableBalance)
699699
if err != nil {
700700
return domain.CurrentAccount{}, fmt.Errorf("failed to create available balance: %w", err)
701701
}

services/current-account/adapters/persistence/withdrawal_repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func toWithdrawalEntity(withdrawal *domain.Withdrawal) *WithdrawalEntity {
174174
ID: withdrawal.ID,
175175
AccountID: withdrawal.AccountID,
176176
AmountCents: withdrawal.Amount.ToMinorUnitsUnchecked(),
177-
Currency: string(withdrawal.Amount.Currency()),
177+
Currency: withdrawal.Amount.InstrumentCode(),
178178
Status: string(withdrawal.Status),
179179
Reference: withdrawal.Reference,
180180
CreatedAt: withdrawal.CreatedAt,

services/current-account/adapters/persistence/withdrawal_repository_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func TestWithdrawalRepository_Create_Success(t *testing.T) {
124124
amountCents, err := retrieved.Amount.ToMinorUnits()
125125
require.NoError(t, err)
126126
assert.Equal(t, int64(10000), amountCents)
127-
assert.Equal(t, domain.CurrencyGBP, retrieved.Amount.Currency())
127+
assert.Equal(t, "GBP", retrieved.Amount.InstrumentCode())
128128
assert.Equal(t, domain.WithdrawalStatusPending, retrieved.Status)
129129
assert.Equal(t, "WD-001", retrieved.Reference)
130130
assert.Equal(t, 1, retrieved.Version)

services/current-account/domain/account.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,13 @@ func NewCurrentAccount(accountID, externalIdentifier, partyID, instrumentCode st
132132
//
133133
// For non-CURRENCY dimensions, precision is trusted as provided by the caller (validated at the
134134
// API boundary by the gRPC service layer using Reference Data).
135-
//
136-
// NOTE: Balance creation for non-CURRENCY dimensions is gated by the Money type which currently
137-
// only supports CURRENCY. Full non-currency balance support is handled in task 16.
138135
func NewCurrentAccountWithDimension(accountID, externalIdentifier, partyID, instrumentCode, dimension string, precision int, opts ...AccountOption) (CurrentAccount, error) {
139136
now := time.Now()
140137
normalizedDimension := strings.ToUpper(dimension)
141138

142139
// For CURRENCY, validate precision against canonical registry value.
143140
// If the currency code is not in the local registry (e.g. a currency only defined in
144-
// the Reference Data service), precision validation is deferred: NewMoneyFromInstrument
141+
// the Reference Data service), precision validation is deferred: NewAmountFromInstrument
145142
// will return ErrInvalidCurrency if the code is genuinely unknown.
146143
if normalizedDimension == quantity.DimensionCurrency {
147144
if inst, ok := currency.ByCode(strings.ToUpper(instrumentCode)); ok {
@@ -152,7 +149,7 @@ func NewCurrentAccountWithDimension(accountID, externalIdentifier, partyID, inst
152149
}
153150
}
154151

155-
zeroMoney, err := NewMoneyFromInstrument(instrumentCode, normalizedDimension, 0)
152+
zeroAmount, err := NewAmountFromInstrument(instrumentCode, normalizedDimension, precision, 0)
156153
if err != nil {
157154
return CurrentAccount{}, err
158155
}
@@ -164,8 +161,8 @@ func NewCurrentAccountWithDimension(accountID, externalIdentifier, partyID, inst
164161
instrumentCode: instrumentCode,
165162
dimension: normalizedDimension,
166163
partyID: partyID,
167-
balance: zeroMoney,
168-
availableBalance: zeroMoney,
164+
balance: zeroAmount,
165+
availableBalance: zeroAmount,
169166
status: AccountStatusActive,
170167
balanceUpdatedAt: now,
171168
version: 1,
@@ -200,8 +197,8 @@ func (a CurrentAccount) Deposit(amount Money) (CurrentAccount, error) {
200197
return CurrentAccount{}, ErrAccountClosed
201198
}
202199

203-
if amount.Currency() != a.balance.Currency() {
204-
return CurrentAccount{}, ErrCurrencyMismatch
200+
if amount.InstrumentCode() != a.balance.InstrumentCode() {
201+
return CurrentAccount{}, ErrInstrumentMismatch
205202
}
206203

207204
// Use immutable Add method
@@ -294,12 +291,12 @@ func (a CurrentAccount) PrepareForDebit(amount Money) (CurrentAccount, error) {
294291
return CurrentAccount{}, ErrAccountClosed
295292
}
296293

297-
if amount.Currency() != a.balance.Currency() {
298-
return CurrentAccount{}, ErrCurrencyMismatch
294+
if amount.InstrumentCode() != a.balance.InstrumentCode() {
295+
return CurrentAccount{}, ErrInstrumentMismatch
299296
}
300297

301298
// Check if sufficient funds (via availableBalance).
302-
// Currency match is already verified above, so Compare cannot return an error here.
299+
// Instrument match is already verified above, so Compare cannot return an error here.
303300
cmp, err := amount.Compare(a.availableBalance)
304301
if err != nil {
305302
return CurrentAccount{}, err
@@ -347,12 +344,12 @@ func (a CurrentAccount) Withdraw(amount Money) (CurrentAccount, error) {
347344
return CurrentAccount{}, ErrAccountClosed
348345
}
349346

350-
if amount.Currency() != a.balance.Currency() {
351-
return CurrentAccount{}, ErrCurrencyMismatch
347+
if amount.InstrumentCode() != a.balance.InstrumentCode() {
348+
return CurrentAccount{}, ErrInstrumentMismatch
352349
}
353350

354351
// Check if sufficient funds.
355-
// Currency match is already verified above, so Compare cannot return an error here.
352+
// Instrument match is already verified above, so Compare cannot return an error here.
356353
cmp, err := amount.Compare(a.availableBalance)
357354
if err != nil {
358355
return CurrentAccount{}, err

0 commit comments

Comments
 (0)