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>
This commit is contained in:
efrilm
2026-10-07 13:48:29 +07:00
co-authored by Claude Opus 5.5
parent 3ebc09f818
commit 2c9753fae7
87 changed files with 423 additions and 3071 deletions
+18 -22
View File
@@ -300,16 +300,16 @@ func earn(customerID uuid.UUID, amount int64, expiresAt *time.Time) WalletCredit
}
}
func pay(customerID uuid.UUID, amount int64) WalletDebitInput {
func redeem(customerID uuid.UUID, amount int64) WalletDebitInput {
return WalletDebitInput{WalletEntry: WalletEntry{
CustomerID: customerID,
Currency: constants.WalletCurrencyPoint,
Type: constants.WalletTxTypePayment,
Type: constants.WalletTxTypeRewardRedeem,
Amount: amount,
ReferenceType: constants.WalletRefTypePayment,
ReferenceType: constants.WalletRefTypeRewardRedemption,
ReferenceID: uuid.New(),
OutletID: ptr(uuid.New()),
Description: "Bayar",
Description: "Tukar voucher",
}}
}
@@ -385,7 +385,7 @@ func TestWalletProcessor_DebitAcrossSeveralLots(t *testing.T) {
second := e.credit(t, earn(c, 50, e.at(2*time.Hour))).Lots[0]
third := e.credit(t, earn(c, 40, e.at(3*time.Hour))).Lots[0]
res, err := e.p.Debit(e.ctx, pay(c, 70))
res, err := e.p.Debit(e.ctx, redeem(c, 70))
require.NoError(t, err)
assert.Equal(t, int64(-70), res.Transaction.Amount)
@@ -415,7 +415,7 @@ func TestWalletProcessor_DebitFollowsLotOrder(t *testing.T) {
var order []uuid.UUID
for i := 0; i < 4; i++ {
res, err := e.p.Debit(e.ctx, pay(c, 10))
res, err := e.p.Debit(e.ctx, redeem(c, 10))
require.NoError(t, err)
require.Len(t, res.Allocations, 1)
order = append(order, res.Allocations[0].LotID)
@@ -425,7 +425,7 @@ func TestWalletProcessor_DebitFollowsLotOrder(t *testing.T) {
// The expired lot still counts in the balance until the expiry job removes it,
// but it cannot be spent (§7.3).
assert.Equal(t, int64(10), e.balance(t, c))
_, err := e.p.Debit(e.ctx, pay(c, 10))
_, err := e.p.Debit(e.ctx, redeem(c, 10))
assert.ErrorIs(t, err, repository.ErrWalletInsufficientBalance)
}
@@ -434,14 +434,14 @@ func TestWalletProcessor_DebitOverBalanceChangesNothing(t *testing.T) {
c := e.customer()
e.credit(t, earn(c, 50, nil))
_, err := e.p.Debit(e.ctx, pay(c, 51))
_, err := e.p.Debit(e.ctx, redeem(c, 51))
assert.ErrorIs(t, err, repository.ErrWalletInsufficientBalance)
assert.Equal(t, int64(50), e.balance(t, c))
assert.Len(t, e.repo.transactions, 1)
assert.Empty(t, e.repo.allocations)
// A customer who never had a wallet has nothing to spend.
_, err = e.p.Debit(e.ctx, pay(e.customer(), 1))
_, err = e.p.Debit(e.ctx, redeem(e.customer(), 1))
assert.ErrorIs(t, err, repository.ErrWalletInsufficientBalance)
}
@@ -467,7 +467,7 @@ func TestWalletProcessor_DebitUpToWithShortfall(t *testing.T) {
e := newWalletTestEnv(t)
c := e.customer()
earned := e.credit(t, earn(c, 100, nil))
_, err := e.p.Debit(e.ctx, pay(c, 70))
_, err := e.p.Debit(e.ctx, redeem(c, 70))
require.NoError(t, err)
res, err := e.p.DebitUpTo(e.ctx, reversal(c, 100, earned))
@@ -510,7 +510,7 @@ func TestWalletProcessor_DebitRejectsSomeoneElsesLot(t *testing.T) {
e.credit(t, earn(a, 10, nil))
other := e.credit(t, earn(b, 10, nil))
in := pay(a, 5)
in := redeem(a, 5)
in.PreferredLotIDs = []uuid.UUID{other.Lots[0].ID}
_, err := e.p.Debit(e.ctx, in)
assert.ErrorIs(t, err, ErrWalletInvalidEntry)
@@ -567,7 +567,7 @@ func TestWalletProcessor_IdempotentDebit(t *testing.T) {
c := e.customer()
e.credit(t, earn(c, 30, e.at(time.Hour)))
e.credit(t, earn(c, 30, nil))
in := pay(c, 40)
in := redeem(c, 40)
in.IdempotencyKey = "pay:1"
first, err := e.p.Debit(e.ctx, in)
@@ -585,7 +585,7 @@ func TestWalletProcessor_IdempotentDebitUpToKeepsShortfall(t *testing.T) {
e := newWalletTestEnv(t)
c := e.customer()
earned := e.credit(t, earn(c, 100, nil))
_, err := e.p.Debit(e.ctx, pay(c, 60))
_, err := e.p.Debit(e.ctx, redeem(c, 60))
require.NoError(t, err)
in := reversal(c, 100, earned)
in.IdempotencyKey = "reverse:order-1"
@@ -614,7 +614,7 @@ func TestWalletProcessor_IdempotencyKeyReusedForAnotherOperation(t *testing.T) {
_, err := e.p.Credit(e.ctx, other)
assert.ErrorIs(t, err, ErrWalletIdempotencyConflict)
debit := pay(c, 100)
debit := redeem(c, 100)
debit.IdempotencyKey = "k"
_, err = e.p.Debit(e.ctx, debit)
assert.ErrorIs(t, err, ErrWalletIdempotencyConflict)
@@ -673,8 +673,8 @@ func TestWalletProcessor_RejectsEntriesThatBreakTheTypeRules(t *testing.T) {
credits := map[string]func(*WalletCreditInput){
"unknown type": func(in *WalletCreditInput) { in.Type = "BONUS" },
"debit-only type as credit": func(in *WalletCreditInput) {
in.Type = constants.WalletTxTypePayment
in.ReferenceType = constants.WalletRefTypePayment
in.Type = constants.WalletTxTypeRewardRedeem
in.ReferenceType = constants.WalletRefTypeRewardRedemption
},
"unknown currency": func(in *WalletCreditInput) { in.Currency = "GOLD" },
"zero amount": func(in *WalletCreditInput) { in.Amount = 0; in.Lots = nil },
@@ -701,10 +701,6 @@ func TestWalletProcessor_RejectsEntriesThatBreakTheTypeRules(t *testing.T) {
in.ReferenceType = constants.WalletRefTypeWalletTx
in.CounterpartyCustomerID = ptr(uuid.New())
},
"PAYMENT_REFUND without source": func(in *WalletCreditInput) {
in.Type = constants.WalletTxTypePaymentRefund
in.ReferenceType = constants.WalletRefTypePayment
},
"ADJUSTMENT without reason": func(in *WalletCreditInput) {
in.Type = constants.WalletTxTypeAdjustment
in.ReferenceType = constants.WalletRefTypeUser
@@ -744,7 +740,7 @@ func TestWalletProcessor_RejectsEntriesThatBreakTheTypeRules(t *testing.T) {
in.Type = constants.WalletTxTypeMigration
in.ReferenceType = constants.WalletRefTypeLegacyPoints
},
"PAYMENT in COIN": func(in *WalletDebitInput) { in.Currency = constants.WalletCurrencyCoin },
"REWARD_REDEEM in COIN": func(in *WalletDebitInput) { in.Currency = constants.WalletCurrencyCoin },
"GAME_SPEND in POINT": func(in *WalletDebitInput) {
in.Type = constants.WalletTxTypeGameSpend
in.ReferenceType = constants.WalletRefTypeGamePlay
@@ -772,7 +768,7 @@ func TestWalletProcessor_RejectsEntriesThatBreakTheTypeRules(t *testing.T) {
e := newWalletTestEnv(t)
e.repo.customers[c] = e.org
e.credit(t, earn(c, 100, nil))
in := pay(c, 10)
in := redeem(c, 10)
mutate(&in)
_, err := e.p.Debit(e.ctx, in)
assert.ErrorIs(t, err, ErrWalletInvalidEntry)