Files
apskel-pos-backend/internal/processor/wallet_trace_processor_test.go
T
efrilmandClaude Opus 5.5 2c9753fae7 feat(loyalty): remove paying with EnakPoint
EnakPoint can only be redeemed for vouchers now: it can no longer pay for
orders and is never cashed out (docs/enakgame-prd.md §3.2, EG-001,
EG-002). No order was ever paid with EnakPoint, so there is no data to
move.

Removed:
- POST /customer/wallet/payment-code, POST /customer/orders/:id/pay-with-points
  and GET /orders/:id/point-payment/preview, with their processors,
  repositories, services, handlers and tests.
- The point payment method type: paying, splitting and refunding with it,
  the outlet filter on the method list, and the system-method guard.
- points and payment_code on CreatePayment; points_used and point_value
  on payments; accepts_point_payment on the customer outlets.
- The outlet point_payment settings. A PUT that still sends them is
  rejected as an unknown field.
- The EnakPoint split in the payment method analytics.
- PAYMENT and PAYMENT_REFUND from the wallet type rules. Tests that used
  them as a generic EnakPoint debit use REWARD_REDEEM.
- The EnakPoint-paid part from the earning basis, which is
  subtotal − discount again.

Migration 000102 drops the trigger, the point methods and their index,
the payments columns, and the outlet settings, and restores the method
type CHECK without point. payments.payment_method_id is ON DELETE
RESTRICT, so it fails rather than lose a payment made with EnakPoint.

The integration docs list the removed endpoints and fields, and the
EnakPoint & EnakCoin PRD and tasks note what is superseded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:48:29 +07:00

162 lines
5.6 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"
)
// 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)
}