package processor import ( "context" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" ) // walletTraceRepoFake reads the fake wallet the way the trace repository reads the // tables. type walletTraceRepoFake struct{ e *walletMoveEnv } func (f walletTraceRepoFake) GetTransaction(_ context.Context, organizationID, id uuid.UUID) (*entities.WalletTransaction, error) { for _, tx := range f.e.repo.transactions { if tx.ID == id && tx.OrganizationID == organizationID { c := *tx return &c, nil } } return nil, repository.ErrWalletTransactionNotFound } func (f walletTraceRepoFake) GetTransactions(_ context.Context, ids []uuid.UUID) ([]entities.WalletTransaction, error) { var out []entities.WalletTransaction for _, tx := range f.e.repo.transactions { if containsID(ids, tx.ID) { out = append(out, *tx) } } return out, nil } func (f walletTraceRepoFake) ListAllocations(ctx context.Context, id uuid.UUID) ([]entities.WalletLotAllocation, error) { return f.e.repo.ListAllocationsByTransaction(ctx, id) } func (f walletTraceRepoFake) ListLotsBySource(ctx context.Context, id uuid.UUID) ([]entities.WalletLot, error) { return f.e.repo.ListLotsBySourceTransaction(ctx, id) } func (f walletTraceRepoFake) GetLots(ctx context.Context, ids []uuid.UUID) ([]entities.WalletLot, error) { return f.e.repo.GetLotsByIDs(ctx, ids) } func (f walletTraceRepoFake) CustomerNames(_ context.Context, ids []uuid.UUID) (map[uuid.UUID]string, error) { names := map[uuid.UUID]string{} for _, id := range ids { if c, ok := f.e.customers.byID[id]; ok { names[id] = c.Name } } return names, nil } func containsID(ids []uuid.UUID, id uuid.UUID) bool { for _, v := range ids { if v == id { return true } } return false } func findRow(t *testing.T, e *walletMoveEnv, customerID uuid.UUID, txType string) *entities.WalletTransaction { t.Helper() for _, tx := range e.repo.transactions { if tx.CustomerID == customerID && tx.Type == txType { return tx } } t.Fatalf("no %s row for %s", txType, customerID) return nil } // The example of ยง8: A has 100 from #ORD-1 and 50 from #ORD-2, sends 120 to B, and B // redeems 30. Tracing B's redemption leads to A's order #ORD-1. func TestWalletTrace_RedemptionLeadsBackToTheSendersOrder(t *testing.T) { e := newWalletMoveEnv(t) a := e.member("Anita", "081200005678") b := e.member("Budi Santoso", "081234561234") ord1 := earn(a, 100, e.at(30*24*time.Hour)) ord1.Description = "Belanja #ORD-1" ord2 := earn(a, 50, e.at(60*24*time.Hour)) ord2.Description = "Belanja #ORD-2" e.credit(t, ord1) e.credit(t, ord2) _, err := e.transfers(nil).Transfer(e.ctx, a, sendPoints(120, "081234561234"), "482913", "key-1", models.CustomerPinRequestInfo{}) require.NoError(t, err) payment, err := e.p.Debit(e.ctx, redeem(b, 30)) require.NoError(t, err) p := NewWalletTraceProcessor(walletTraceRepoFake{e}) trace, err := p.Trace(e.ctx, e.org, payment.Transaction.ID) require.NoError(t, err) assert.Equal(t, constants.WalletTxTypeRewardRedeem, trace.Transaction.Type) assert.Equal(t, "Budi Santoso", trace.Transaction.Customer.Name) require.Len(t, trace.Lots, 1) assert.Equal(t, int64(30), trace.Lots[0].Amount) chain := trace.Lots[0].Chain require.Len(t, chain, 2) assert.Equal(t, constants.WalletTxTypeTransferIn, chain[0].Source.Type) assert.Equal(t, b, chain[0].Source.Customer.ID) assert.Equal(t, constants.WalletTxTypeEarn, chain[1].Source.Type) assert.Equal(t, "Anita", chain[1].Source.Customer.Name) assert.Equal(t, "Belanja #ORD-1", chain[1].Source.Description) assert.Equal(t, constants.WalletRefTypeOrder, chain[1].Source.ReferenceType) assert.Equal(t, ord1.ReferenceID, chain[1].Source.ReferenceID) assert.Nil(t, chain[1].Lot.OriginLotID, "the chain ends at the lot the order created") assert.Equal(t, *chain[0].Lot.OriginLotID, chain[1].Lot.ID) } func TestWalletTrace_DebitAndCreditOfATransfer(t *testing.T) { e := newWalletMoveEnv(t) a := e.member("Anita", "081200005678") b := e.member("Budi", "081234561234") e.credit(t, earn(a, 100, e.at(time.Hour))) e.credit(t, earn(a, 50, nil)) _, err := e.transfers(nil).Transfer(e.ctx, a, sendPoints(120, "081234561234"), "482913", "key-1", models.CustomerPinRequestInfo{}) require.NoError(t, err) p := NewWalletTraceProcessor(walletTraceRepoFake{e}) // The sending side took from both of A's lots, which were A's own earnings. out, err := p.Trace(e.ctx, e.org, findRow(t, e, a, constants.WalletTxTypeTransferOut).ID) require.NoError(t, err) require.Len(t, out.Lots, 2) assert.Equal(t, []int64{100, 20}, []int64{out.Lots[0].Amount, out.Lots[1].Amount}) for _, lot := range out.Lots { require.Len(t, lot.Chain, 1) assert.Equal(t, constants.WalletTxTypeEarn, lot.Chain[0].Source.Type) } // The receiving side created two lots, each carried over from one of A's. in, err := p.Trace(e.ctx, e.org, findRow(t, e, b, constants.WalletTxTypeTransferIn).ID) require.NoError(t, err) require.Len(t, in.Lots, 2) for _, lot := range in.Lots { require.Len(t, lot.Chain, 2) assert.Equal(t, b, lot.Chain[0].Source.Customer.ID) assert.Equal(t, a, lot.Chain[1].Source.Customer.ID) assert.Equal(t, lot.Chain[1].Lot.ExpiresAt, lot.Chain[0].Lot.ExpiresAt) } } func TestWalletTrace_OtherOrganizationsRowsAreNotFound(t *testing.T) { e := newWalletMoveEnv(t) a := e.member("Anita", "081200005678") res := e.credit(t, earn(a, 10, nil)) _, err := NewWalletTraceProcessor(walletTraceRepoFake{e}).Trace(e.ctx, uuid.New(), res.Transaction.ID) assert.ErrorIs(t, err, repository.ErrWalletTransactionNotFound) }