Files
apskel-pos-backend/internal/processor/wallet_query_processor_test.go
T
2026-09-30 15:31:44 +07:00

267 lines
11 KiB
Go

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)
}