2026-09-30 09:15:26 +07:00
|
|
|
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 {
|
2026-09-30 10:23:08 +07:00
|
|
|
p := NewWalletQueryProcessor(repo, NewLoyaltySettingsProcessor(&loyaltyRepoFake{orgValues: settings}, txRunnerFake{}))
|
2026-09-30 09:15:26 +07:00
|
|
|
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)
|
|
|
|
|
}
|
2026-09-30 11:26:16 +07:00
|
|
|
|
|
|
|
|
func (f *walletQueryRepoFake) OrganizationOutstanding(context.Context, uuid.UUID) (int64, int64, error) {
|
|
|
|
|
return 0, 0, nil
|
|
|
|
|
}
|