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" ) type walletQueryRepoFake struct { org uuid.UUID spendable map[string]int64 expiring []repository.WalletExpiringAmount transactions []entities.WalletTransaction total int64 lots []entities.WalletLot filters []repository.WalletTransactionFilter } func (f *walletQueryRepoFake) GetCustomerOrganizationID(context.Context, uuid.UUID) (uuid.UUID, error) { if f.org == uuid.Nil { return uuid.Nil, repository.ErrWalletNotFound } return f.org, nil } func (f *walletQueryRepoFake) SpendableBalances(context.Context, uuid.UUID, time.Time) (map[string]int64, error) { return f.spendable, nil } func (f *walletQueryRepoFake) NearestExpiring(context.Context, uuid.UUID, time.Time) ([]repository.WalletExpiringAmount, error) { return f.expiring, nil } func (f *walletQueryRepoFake) ListTransactions(_ context.Context, filter repository.WalletTransactionFilter) ([]entities.WalletTransaction, int64, error) { f.filters = append(f.filters, filter) var out []entities.WalletTransaction for _, tx := range f.transactions { if filter.Currency == "" || tx.Currency == filter.Currency { out = append(out, tx) } } if len(out) > filter.Limit { out = out[:filter.Limit] } return out, f.total, nil } func (f *walletQueryRepoFake) ListLotsBySourceTransactions(_ context.Context, ids []uuid.UUID) ([]entities.WalletLot, error) { var out []entities.WalletLot for _, lot := range f.lots { for _, id := range ids { if lot.SourceTransactionID == id { out = append(out, lot) } } } return out, nil } type orgSettingsFake map[string]string func newWalletQueryTest(repo *walletQueryRepoFake, settings orgSettingsFake) *WalletQueryProcessor { p := NewWalletQueryProcessor(repo, NewLoyaltySettingsProcessor(&loyaltyRepoFake{orgValues: settings}, txRunnerFake{})) p.now = func() time.Time { return time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) } return p } func TestWalletQueryProcessor_SummaryShowsWhereEachRowCameFromOrWent(t *testing.T) { order, payment := uuid.New(), uuid.New() earnID, payID := uuid.New(), uuid.New() soon := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC) later := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC) created := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) repo := &walletQueryRepoFake{ org: uuid.New(), spendable: map[string]int64{constants.WalletCurrencyPoint: 250, constants.WalletCurrencyCoin: 8}, expiring: []repository.WalletExpiringAmount{ {Currency: constants.WalletCurrencyPoint, Date: "2026-07-01", Amount: 100}, }, transactions: []entities.WalletTransaction{ {ID: payID, Currency: constants.WalletCurrencyPoint, Type: constants.WalletTxTypePayment, Amount: -50, BalanceAfter: 250, ReferenceType: constants.WalletRefTypePayment, ReferenceID: payment, Description: "Bayar #ORD-1", CreatedAt: created.Add(time.Hour), CounterpartyCustomerID: ptr(uuid.New()), Metadata: entities.Metadata{"point_value": 100}}, {ID: earnID, Currency: constants.WalletCurrencyPoint, Type: constants.WalletTxTypeEarn, Amount: 300, BalanceAfter: 300, ReferenceType: constants.WalletRefTypeOrder, ReferenceID: order, Description: "Belanja #ORD-1", CreatedAt: created}, }, lots: []entities.WalletLot{ {SourceTransactionID: earnID, OriginalAmount: 200, RemainingAmount: 150, ExpiresAt: &later}, {SourceTransactionID: earnID, OriginalAmount: 100, RemainingAmount: 100, ExpiresAt: &soon}, }, } p := newWalletQueryTest(repo, orgSettingsFake{constants.LoyaltyPointValueKey: "100"}) data, err := p.Summary(context.Background(), uuid.New()) require.NoError(t, err) assert.Equal(t, int64(250), data.PointBalance) assert.Equal(t, int64(8), data.CoinBalance) assert.Equal(t, int64(100), data.PointValue) assert.Equal(t, int64(25000), data.PointDiscountValue) assert.Equal(t, &models.CustomerWalletExpiring{Amount: 100, Date: "2026-07-01"}, data.NearestExpiring.Point) assert.Nil(t, data.NearestExpiring.Coin) require.Len(t, data.RecentTransactions, 2) pay, earn := data.RecentTransactions[0], data.RecentTransactions[1] assert.Equal(t, &models.CustomerWalletTransactionRef{Type: constants.WalletRefTypePayment, ID: payment}, pay.Destination) assert.Nil(t, pay.Source) assert.Empty(t, pay.Lots) assert.Equal(t, &models.CustomerWalletTransactionRef{Type: constants.WalletRefTypeOrder, ID: order}, earn.Source) assert.Nil(t, earn.Destination) assert.Equal(t, &soon, earn.ExpiresAt, "earliest expiry among the lots the credit created") assert.Len(t, earn.Lots, 2) // The pre-wallet fields are still filled, from the wallet. assert.Equal(t, int64(250), data.TotalPoints) assert.Equal(t, int64(8), data.TotalTokens) require.Len(t, data.PointsHistory, 2) assert.Equal(t, int64(-50), data.PointsHistory[0].Points) assert.Equal(t, constants.WalletTxTypePayment, data.PointsHistory[0].Type) assert.Equal(t, created.Add(time.Hour), data.LastUpdated) } func TestWalletQueryProcessor_PointValueFallsBackToDefault(t *testing.T) { for name, settings := range map[string]orgSettingsFake{ "not set": {}, "garbage": {constants.LoyaltyPointValueKey: "abc"}, "zero": {constants.LoyaltyPointValueKey: "0"}, "negative": {constants.LoyaltyPointValueKey: "-5"}, } { t.Run(name, func(t *testing.T) { repo := &walletQueryRepoFake{org: uuid.New(), spendable: map[string]int64{constants.WalletCurrencyPoint: 40}} data, err := newWalletQueryTest(repo, settings).Summary(context.Background(), uuid.New()) require.NoError(t, err) assert.Equal(t, constants.LoyaltyPointValueDefault, data.PointValue) assert.Equal(t, int64(40), data.PointDiscountValue) }) } } func TestWalletQueryProcessor_SummaryForCustomerWithoutWallet(t *testing.T) { repo := &walletQueryRepoFake{org: uuid.New(), spendable: map[string]int64{}} data, err := newWalletQueryTest(repo, orgSettingsFake{}).Summary(context.Background(), uuid.New()) require.NoError(t, err) assert.Zero(t, data.PointBalance) assert.Zero(t, data.CoinBalance) assert.NotNil(t, data.RecentTransactions, "an empty list, not null, for the app") _, err = newWalletQueryTest(&walletQueryRepoFake{}, orgSettingsFake{}).Summary(context.Background(), uuid.New()) assert.ErrorIs(t, err, repository.ErrWalletNotFound) } func TestWalletQueryProcessor_LegacyEndpointsSplitByCurrency(t *testing.T) { repo := &walletQueryRepoFake{ org: uuid.New(), spendable: map[string]int64{constants.WalletCurrencyPoint: 100, constants.WalletCurrencyCoin: 8}, transactions: []entities.WalletTransaction{ {ID: uuid.New(), Currency: constants.WalletCurrencyCoin, Type: constants.WalletTxTypeMigration, Amount: 8}, {ID: uuid.New(), Currency: constants.WalletCurrencyPoint, Type: constants.WalletTxTypeMigration, Amount: 100}, }, } p := newWalletQueryTest(repo, orgSettingsFake{}) points, err := p.LegacyPoints(context.Background(), uuid.New()) require.NoError(t, err) assert.Equal(t, int64(100), points.TotalPoints) require.Len(t, points.PointsHistory, 1) assert.Equal(t, int64(100), points.PointsHistory[0].Points) tokens, err := p.LegacyTokens(context.Background(), uuid.New()) require.NoError(t, err) assert.Equal(t, int64(8), tokens.TotalTokens) require.Len(t, tokens.TokensHistory, 1) assert.Equal(t, int64(8), tokens.TokensHistory[0].Tokens) } func TestWalletQueryProcessor_TransactionsQuery(t *testing.T) { customer := uuid.New() jakarta := walletDisplayLocation repo := &walletQueryRepoFake{org: uuid.New(), total: 45} page, err := newWalletQueryTest(repo, orgSettingsFake{}).Transactions(context.Background(), customer, models.ListCustomerWalletTransactionsQuery{ Page: 3, Limit: 10, Currency: "point", Type: "earn, PAYMENT", From: "2026-05-01", To: "2026-05-31", }) require.NoError(t, err) assert.Equal(t, models.Pagination{Page: 3, Limit: 10, Total: 45, TotalPages: 5}, page.Pagination) assert.NotNil(t, page.Data) f := repo.filters[0] assert.Equal(t, customer, f.CustomerID) assert.Equal(t, 20, f.Offset) assert.Equal(t, 10, f.Limit) assert.Equal(t, constants.WalletCurrencyPoint, f.Currency) assert.Equal(t, []string{constants.WalletTxTypeEarn, constants.WalletTxTypePayment}, f.Types) assert.True(t, f.From.Equal(time.Date(2026, 5, 1, 0, 0, 0, 0, jakarta))) assert.True(t, f.To.Equal(time.Date(2026, 6, 1, 0, 0, 0, 0, jakarta)), "to covers the whole last day") // Defaults. repo = &walletQueryRepoFake{org: uuid.New()} page, err = newWalletQueryTest(repo, orgSettingsFake{}).Transactions(context.Background(), customer, models.ListCustomerWalletTransactionsQuery{}) require.NoError(t, err) assert.Equal(t, 1, page.Pagination.Page) assert.Equal(t, walletTransactionsPageLimit, repo.filters[0].Limit) assert.Zero(t, repo.filters[0].Offset) assert.Nil(t, repo.filters[0].From) assert.Empty(t, repo.filters[0].Types) } func TestWalletQueryProcessor_RejectsBadQueries(t *testing.T) { for name, q := range map[string]models.ListCustomerWalletTransactionsQuery{ "negative page": {Page: -1}, "limit too big": {Limit: walletTransactionsMaxLimit + 1}, "negative limit": {Limit: -1}, "unknown currency": {Currency: "GOLD"}, "unknown type": {Type: "EARN,BONUS"}, "bad from": {From: "01-05-2026"}, "bad to": {To: "2026-13-01"}, "from after to": {From: "2026-06-02", To: "2026-06-01"}, } { t.Run(name, func(t *testing.T) { repo := &walletQueryRepoFake{org: uuid.New()} _, err := newWalletQueryTest(repo, orgSettingsFake{}).Transactions(context.Background(), uuid.New(), q) assert.ErrorIs(t, err, ErrInvalidWalletQuery) assert.Empty(t, repo.filters, "nothing is queried") }) } // A single day is a valid range. repo := &walletQueryRepoFake{org: uuid.New()} _, err := newWalletQueryTest(repo, orgSettingsFake{}).Transactions(context.Background(), uuid.New(), models.ListCustomerWalletTransactionsQuery{From: "2026-06-01", To: "2026-06-01"}) assert.NoError(t, err) } func (f *walletQueryRepoFake) OrganizationOutstanding(context.Context, uuid.UUID) (int64, int64, error) { return 0, 0, nil } func (f *walletQueryRepoFake) ExpiringByDay(context.Context, uuid.UUID, time.Time) ([]repository.WalletExpiringAmount, error) { return f.expiring, nil } func TestWalletQueryProcessor_ExpiringGroupsByCurrencyAndDay(t *testing.T) { repo := &walletQueryRepoFake{org: uuid.New(), expiring: []repository.WalletExpiringAmount{ {Currency: "POINT", Date: "2026-10-31", Amount: 150}, {Currency: "COIN", Date: "2026-10-31", Amount: 4}, {Currency: "POINT", Date: "2026-12-31", Amount: 200}, }} got, err := newWalletQueryTest(repo, nil).Expiring(context.Background(), uuid.New()) require.NoError(t, err) assert.Equal(t, []models.CustomerWalletExpiring{{Amount: 150, Date: "2026-10-31"}, {Amount: 200, Date: "2026-12-31"}}, got.Point) assert.Equal(t, []models.CustomerWalletExpiring{{Amount: 4, Date: "2026-10-31"}}, got.Coin) empty, err := newWalletQueryTest(&walletQueryRepoFake{org: uuid.New()}, nil).Expiring(context.Background(), uuid.New()) require.NoError(t, err) assert.NotNil(t, empty.Point, "an empty list, not null") assert.NotNil(t, empty.Coin) }