Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,9 @@ func TestOrgScopedMigrations_CockroachDB(t *testing.T) {
})

t.Run("IndexesCreated", func(t *testing.T) {
// Verify the three indexes were created
for _, indexName := range []string{
"idx_account_participant_syndicate",
"idx_account_syndicate_participants",
"idx_account_syndicate_scope_integrity",
} {
var count int64
err := gormDB.Raw(`
Expand All @@ -89,6 +87,20 @@ func TestOrgScopedMigrations_CockroachDB(t *testing.T) {
}
})

t.Run("ScopeIntegrityIndexDropped", func(t *testing.T) {
// Migration 20260416000001 dropped idx_account_syndicate_scope_integrity to allow
// multiple accounts per (party_id, org_party_id, instrument_code) — required by
// utility billing patterns (e.g. separate GBP accounts for electricity and gas
// from the same supplier).
var count int64
err := gormDB.Raw(`
SELECT COUNT(*) FROM pg_indexes
WHERE tablename = 'account' AND indexname = 'idx_account_syndicate_scope_integrity'
`).Scan(&count).Error
require.NoError(t, err)
assert.Equal(t, int64(0), count, "idx_account_syndicate_scope_integrity should have been dropped")
})

t.Run("CanCreateOrgScopedAccount", func(t *testing.T) {
partyID := uuid.New()
orgPartyID := uuid.New()
Expand Down Expand Up @@ -149,16 +161,18 @@ func TestOrgScopedMigrations_CockroachDB(t *testing.T) {
assert.Nil(t, retrieved.OrgPartyID, "Personal account should have NULL org_party_id")
})

t.Run("UniqueConstraintOnPartyOrgInstrumentCode", func(t *testing.T) {
t.Run("MultipleAccountsPerPartyOrgInstrumentAllowed", func(t *testing.T) {
// Utility billing case: a customer has separate GBP billing accounts for electricity
// and gas with the same supplier. After migration 20260416000001, both succeed and
// uniqueness is preserved only by account_identification.
partyID := uuid.New()
orgPartyID := uuid.New()
now := time.Now()

// First account: party + org + GBP
entity1 := &CurrentAccountEntity{
ID: uuid.New(),
AccountID: "ACC-UNIQ-001",
AccountIdentification: "GB82WEST33333333333333",
AccountIdentification: "PPM-ELEC-DEMO-001",
AccountType: "current",
InstrumentCode: "GBP",
Dimension: "CURRENCY",
Expand All @@ -172,13 +186,12 @@ func TestOrgScopedMigrations_CockroachDB(t *testing.T) {
UpdatedBy: "system",
}
err := gormDB.Create(entity1).Error
require.NoError(t, err, "First org-scoped account should succeed")
require.NoError(t, err, "First org-scoped GBP account should succeed")

// Duplicate: same party + org + instrument_code should fail
entity2 := &CurrentAccountEntity{
ID: uuid.New(),
AccountID: "ACC-UNIQ-002",
AccountIdentification: "GB82WEST44444444444444",
AccountIdentification: "PPM-GAS-DEMO-001",
AccountType: "current",
InstrumentCode: "GBP",
Dimension: "CURRENCY",
Expand All @@ -192,13 +205,12 @@ func TestOrgScopedMigrations_CockroachDB(t *testing.T) {
UpdatedBy: "system",
}
err = gormDB.Create(entity2).Error
assert.Error(t, err, "Duplicate (party_id, org_party_id, instrument_code) should be rejected by unique index")
assert.NoError(t, err, "Second GBP account for same (party, org) should succeed (multi-service billing)")

// Different instrument_code: same party + org + EUR should succeed
entity3 := &CurrentAccountEntity{
ID: uuid.New(),
AccountID: "ACC-UNIQ-003",
AccountIdentification: "GB82WEST55555555555555",
AccountIdentification: "PPM-EUR-DEMO-001",
AccountType: "current",
InstrumentCode: "EUR",
Dimension: "CURRENCY",
Expand All @@ -213,6 +225,28 @@ func TestOrgScopedMigrations_CockroachDB(t *testing.T) {
}
err = gormDB.Create(entity3).Error
assert.NoError(t, err, "Different instrument_code for same (party_id, org_party_id) should succeed")

// Account-identifier uniqueness is still enforced by idx_account_account_identification.
dup := &CurrentAccountEntity{
ID: uuid.New(),
AccountID: "ACC-UNIQ-004",
AccountIdentification: "PPM-ELEC-DEMO-001",
AccountType: "current",
InstrumentCode: "GBP",
Dimension: "CURRENCY",
Status: "active",
PartyID: partyID,
OrgPartyID: &orgPartyID,
OverdraftLimit: 0,
CreatedAt: now,
UpdatedAt: now,
CreatedBy: "system",
UpdatedBy: "system",
}
err = gormDB.Create(dup).Error
require.Error(t, err, "Duplicate account_identification must still be rejected")
assert.Contains(t, err.Error(), "duplicate key", "rejection should be a uniqueness violation")
assert.Contains(t, err.Error(), "account_identification", "rejection must reference idx_account_account_identification, not some other constraint")
})

t.Run("UniqueConstraintDoesNotAffectPersonalAccounts", func(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- Drop idx_account_syndicate_scope_integrity (party_id, org_party_id, instrument_code).
--
-- The original constraint (PRD-022) assumed one account per party per organisation per
-- currency, modelled on a syndicate where Alice holds a single GBP position in Venture Alpha.
-- That assumption does not hold for utility billing or any multi-service scenario where a
-- party legitimately holds multiple instrument-equivalent accounts under the same supplier:
-- a residential customer needs separate GBP billing accounts for electricity and gas at the
-- same supplier, and the same shape extends to multi-site customers (one billing account per
-- premise) and multi-product offerings.
--
-- Account-identifier uniqueness is preserved by idx_account_account_identification, so
-- dropping this index does not allow accidental account duplication; it only allows the
-- legitimate "Margaret has GBP-elec and GBP-gas with Utilita" pattern.

DROP INDEX IF EXISTS "idx_account_syndicate_scope_integrity";
3 changes: 2 additions & 1 deletion services/current-account/migrations/atlas.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
h1:PnPRafxLtbQAhHssRdEu02xkP/eQb96IF7HwUpVmYvE=
h1:C1bgEjVoS7bhttB5ntNjdmFCKz9yaBltHLl5kwGkbXk=
20251216000001_initial.sql h1:nyRf35O3eYNJqShAvOEhvliXKLIR6e/V8B9JhOHde9w=
20251216000002_audit_system.sql h1:X/0ZHt9cGTYmFbUUvKAkBgEiGw9fYEqfnbMB8I4SCR8=
20251217000001_fix_audit_status_constraint.sql h1:yQxsHxbUp5g+ehkKFjOhZs59SbfaQIPw4/sA2xE2rwU=
Expand Down Expand Up @@ -28,3 +28,4 @@ h1:PnPRafxLtbQAhHssRdEu02xkP/eQb96IF7HwUpVmYvE=
20260316000002_add_event_outbox_tenant_id_index.sql h1:hrEPnXoHQ1bnaMtvFFwZ9UzJVtryQrIe6zDSDqfoLbM=
20260323000001_align_audit_schema.sql h1:AmTi2tik7qBcMVH589i0pvv14DxTbPMbK0w24n0aw+g=
20260323000002_align_audit_schema_data.sql h1:5AqTImsCsnJIDldKPVAxFCwmb/8uabn7KZULF/PzQs8=
20260416000001_drop_syndicate_scope_integrity_index.sql h1:S9u8OUer3WusmW31j2zAsNGaZRwMV3v6+BxA5k0FxDk=
Loading