package processor import ( "context" "sort" "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/repository" ) // walletExpiryRepoFake lists the fake wallet's due lots the way the query does, plus // any extra lots a test wants listed. type walletExpiryRepoFake struct { wallet *walletRepoFake extra []repository.DueLot } func (f *walletExpiryRepoFake) ListDueLots(_ context.Context, asOf time.Time, limit int) ([]repository.DueLot, error) { descriptions := map[uuid.UUID]string{} for _, tx := range f.wallet.transactions { descriptions[tx.ID] = tx.Description } due := append([]repository.DueLot(nil), f.extra...) for _, lot := range f.wallet.lots { if lot.RemainingAmount > 0 && lot.ExpiresAt != nil && !lot.ExpiresAt.After(asOf) { due = append(due, repository.DueLot{ ID: lot.ID, CustomerID: lot.CustomerID, Currency: lot.Currency, Remaining: lot.RemainingAmount, ExpiresAt: *lot.ExpiresAt, SourceDescription: descriptions[lot.SourceTransactionID], }) } } sort.SliceStable(due, func(i, j int) bool { return due[i].ExpiresAt.Before(due[j].ExpiresAt) }) if len(due) > limit { due = due[:limit] } return due, nil } func (e *walletMoveEnv) expiry(notifier customerNotifier) (*WalletExpiryProcessor, *walletExpiryRepoFake) { repo := &walletExpiryRepoFake{wallet: e.repo} p := NewWalletExpiryProcessor(repo, e.p, txRunnerFake{}, notifier) p.now = func() time.Time { return e.now } return p, repo } func TestWalletExpiry_ExpiresWhatIsDueAndTellsTheCustomer(t *testing.T) { e := newWalletMoveEnv(t) a := e.member("Anita", "081200005678") ord := earn(a, 150, e.at(-time.Hour)) ord.Description = "Belanja #ORD-0098" due := e.credit(t, ord) e.credit(t, earn(a, 50, e.at(-2*time.Hour))) e.credit(t, earn(a, 70, e.at(time.Hour))) // not yet e.credit(t, earn(a, 30, nil)) // never e.earnCoins(t, a, 4, e.at(-time.Minute)) // A payment already used part of the first lot; only the rest expires. _, err := e.p.Debit(e.ctx, WalletDebitInput{WalletEntry: pay(a, 20).WalletEntry, PreferredLotIDs: []uuid.UUID{due.Lots[0].ID}}) require.NoError(t, err) notifier := ¬ifierFake{} p, _ := e.expiry(notifier) count, err := p.ExpireDue(e.ctx) require.NoError(t, err) assert.Equal(t, 3, count) assert.Equal(t, int64(100), e.balance(t, a), "70 not due yet + 30 that never expires") assert.Equal(t, int64(0), e.coinBalance(t, a)) var expire *entities.WalletTransaction for _, tx := range e.repo.transactions { if tx.Type == constants.WalletTxTypeExpire && tx.ReferenceID == due.Lots[0].ID { expire = tx } } require.NotNil(t, expire) assert.Equal(t, int64(-130), expire.Amount) assert.Equal(t, "Kedaluwarsa: 130 EnakPoint dari Belanja #ORD-0098", expire.Description) assert.Equal(t, constants.WalletRefTypeLot, expire.ReferenceType) assert.Equal(t, "expire:"+due.Lots[0].ID.String(), *expire.IdempotencyKey) // One push per currency, with the total. pushes := notifier.pushes[a] require.Len(t, pushes, 2) byCurrency := map[string]pushFake{} for _, p := range pushes { byCurrency[p.data["currency"]] = p } assert.Equal(t, "EnakPoint kedaluwarsa", byCurrency["POINT"].title) assert.Equal(t, "180 EnakPoint kamu sudah kedaluwarsa.", byCurrency["POINT"].body) assert.Equal(t, NotificationTypeWalletExpired, byCurrency["POINT"].data["type"]) assert.Equal(t, "4", byCurrency["COIN"].data["amount"]) } func TestWalletExpiry_RunningAgainExpiresNothingMore(t *testing.T) { e := newWalletMoveEnv(t) a := e.member("Anita", "081200005678") e.credit(t, earn(a, 150, e.at(-time.Hour))) notifier := ¬ifierFake{} // Two instances, one after the other. first, _ := e.expiry(notifier) second, _ := e.expiry(notifier) n1, err := first.ExpireDue(e.ctx) require.NoError(t, err) n2, err := second.ExpireDue(e.ctx) require.NoError(t, err) assert.Equal(t, 1, n1) assert.Equal(t, 0, n2) assert.Len(t, notifier.pushes[a], 1) var expires int for _, tx := range e.repo.transactions { if tx.Type == constants.WalletTxTypeExpire { expires++ } } assert.Equal(t, 1, expires) } func TestWalletExpiry_OneFailingLotDoesNotStopTheOthers(t *testing.T) { e := newWalletMoveEnv(t) a := e.member("Anita", "081200005678") e.credit(t, earn(a, 150, e.at(-time.Hour))) p, repo := e.expiry(nil) // A lot listed that ExpireLot cannot find. repo.extra = []repository.DueLot{{ID: uuid.New(), CustomerID: a, Currency: "POINT", Remaining: 5, ExpiresAt: e.now.Add(-3 * time.Hour)}} count, err := p.ExpireDue(e.ctx) require.NoError(t, err) assert.Equal(t, 1, count) assert.Equal(t, int64(0), e.balance(t, a)) } func TestWalletExpiry_NothingDue(t *testing.T) { e := newWalletMoveEnv(t) a := e.member("Anita", "081200005678") e.credit(t, earn(a, 150, e.at(time.Hour))) notifier := ¬ifierFake{} p, _ := e.expiry(notifier) count, err := p.ExpireDue(e.ctx) require.NoError(t, err) assert.Zero(t, count) assert.Empty(t, notifier.pushes) }