Files

244 lines
9.5 KiB
Go
Raw Permalink Normal View History

package processor
import (
"context"
"errors"
"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/models"
"apskel-pos-be/internal/repository"
)
type pushFake struct {
title, body string
data map[string]string
}
// notifierFake records the pushes each customer would get.
type notifierFake struct{ pushes map[uuid.UUID][]pushFake }
func (f *notifierFake) Notify(_ context.Context, customerID uuid.UUID, title, body string, data map[string]string) error {
if f.pushes == nil {
f.pushes = map[uuid.UUID][]pushFake{}
}
f.pushes[customerID] = append(f.pushes[customerID], pushFake{title: title, body: body, data: data})
return nil
}
func sendPoints(amount int64, phone string) models.WalletTransfer {
return models.WalletTransfer{Currency: constants.WalletCurrencyPoint, Amount: amount, RecipientPhone: phone}
}
func TestWalletTransfer_MovesBalanceWithItsExpiry(t *testing.T) {
e := newWalletMoveEnv(t)
a := e.member("Anita", "081200005678")
b := e.member("Budi Santoso", "081234561234")
dec, jan := e.at(30*24*time.Hour), e.at(60*24*time.Hour)
first := e.credit(t, earn(a, 100, dec))
second := e.credit(t, earn(a, 50, jan))
notifier := &notifierFake{}
// The example in §8: A sends 120, 100 from the lot expiring first and 20 from the next.
res, err := e.transfers(notifier).Transfer(e.ctx, a, sendPoints(120, "081234561234"), "482913", "key-1", models.CustomerPinRequestInfo{})
require.NoError(t, err)
assert.Equal(t, int64(30), res.Balance)
assert.Equal(t, models.WalletTransferRecipient{Name: "Bu*** Sa***", PhoneNumber: "08**-****-1234"}, res.Recipient)
assert.Equal(t, []models.WalletMovedLot{{Amount: 100, ExpiresAt: dec}, {Amount: 20, ExpiresAt: jan}}, res.Lots)
assert.Equal(t, int64(120), e.balance(t, b))
assert.Equal(t, []PinAction{PinActionTransfer}, e.pins.actions)
var bLots []uuid.UUID
for _, lot := range e.repo.lots {
if lot.CustomerID == b {
require.NotNil(t, lot.OriginLotID)
bLots = append(bLots, *lot.OriginLotID)
for _, origin := range e.repo.lots {
if origin.ID == *lot.OriginLotID {
assert.Equal(t, origin.ExpiresAt, lot.ExpiresAt, "the recipient's lot expires exactly when the sender's did")
}
}
}
}
assert.Equal(t, []uuid.UUID{first.Lots[0].ID, second.Lots[0].ID}, bLots)
out, in := e.repo.transactions[2], e.repo.transactions[3]
assert.Equal(t, constants.WalletTxTypeTransferOut, out.Type)
assert.Equal(t, b, *out.CounterpartyCustomerID)
assert.Equal(t, in.ID, out.ReferenceID)
assert.Equal(t, "Transfer ke Bu*** Sa*** (08**-****-1234)", out.Description)
assert.Equal(t, constants.WalletTxTypeTransferIn, in.Type)
assert.Equal(t, a, *in.CounterpartyCustomerID)
assert.Equal(t, out.ID, in.ReferenceID)
assert.Equal(t, *out.GroupID, *in.GroupID)
assert.Equal(t, "Transfer dari An*** (08**-****-5678)", in.Description)
assert.GreaterOrEqual(t, e.repo.locks[a], 1)
assert.GreaterOrEqual(t, e.repo.locks[b], 1)
assert.Equal(t, []pushFake{{
title: "EnakPoint masuk",
body: "Kamu menerima 120 EnakPoint dari An*** (08**-****-5678).",
data: map[string]string{
"type": NotificationTypeWalletTransferIn,
"transaction_id": in.ID.String(),
"group_id": in.GroupID.String(),
"currency": constants.WalletCurrencyPoint,
"amount": "120",
},
}}, notifier.pushes[b])
assert.Empty(t, notifier.pushes[a], "the sender gets no push")
}
func TestWalletTransfer_Coins(t *testing.T) {
e := newWalletMoveEnv(t)
a := e.member("Anita", "081200005678")
b := e.member("Budi", "081234561234")
e.earnCoins(t, a, 10, nil)
_, err := e.transfers(nil).Transfer(e.ctx, a, models.WalletTransfer{Currency: "coin", Amount: 4, RecipientPhone: "081234561234"}, "482913", "key-1", models.CustomerPinRequestInfo{})
require.NoError(t, err)
assert.Equal(t, int64(6), e.coinBalance(t, a))
assert.Equal(t, int64(4), e.coinBalance(t, b))
}
func TestWalletTransfer_RefusesRecipientsItMayNotSendTo(t *testing.T) {
e := newWalletMoveEnv(t)
a := e.member("Anita", "081200005678")
e.credit(t, earn(a, 100, nil))
walkIn := e.member("Walk-in", "081100000000")
e.customers.byID[walkIn].IsDefault = true
inactive := e.member("Old", "081100000001")
e.customers.byID[inactive].IsActive = false
elsewhere := e.member("Other Org", "081100000002")
e.customers.byID[elsewhere].OrganizationID = uuid.New()
for phone, want := range map[string]error{
"081200005678": ErrWalletMoveRejected, // herself
"081100000000": ErrWalletMoveRejected, // the walk-in customer
"081100000001": ErrWalletMoveRejected, // inactive
"081100000002": ErrWalletRecipientNotFound, // another organization looks like nobody
"081999999999": ErrWalletRecipientNotFound,
"": ErrWalletMoveRejected,
} {
_, err := e.transfers(nil).Recipient(e.ctx, a, phone)
assert.ErrorIs(t, err, want, phone)
_, err = e.transfers(nil).Transfer(e.ctx, a, sendPoints(10, phone), "482913", "key-"+phone, models.CustomerPinRequestInfo{})
assert.ErrorIs(t, err, want, phone)
}
assert.Empty(t, e.pins.actions, "refused before the PIN")
assert.Equal(t, int64(100), e.balance(t, a))
}
func TestWalletTransfer_RecipientIsMasked(t *testing.T) {
e := newWalletMoveEnv(t)
a := e.member("Anita", "081200005678")
e.member("Budi Santoso", "081234561234")
got, err := e.transfers(nil).Recipient(e.ctx, a, " 081234561234 ")
require.NoError(t, err)
assert.Equal(t, &models.WalletTransferRecipient{Name: "Bu*** Sa***", PhoneNumber: "08**-****-1234"}, got)
}
func TestWalletTransfer_OrganizationLimits(t *testing.T) {
e := newWalletMoveEnv(t)
a := e.member("Anita", "081200005678")
e.member("Budi", "081234561234")
e.credit(t, earn(a, 1000, nil))
e.settings.Transfer = models.LoyaltyTransferSettings{Enabled: true, MinAmount: 10, MaxPerTransaction: ptr(int64(300)), DailyLimit: ptr(int64(500))}
send := func(amount int64, key string) error {
_, err := e.transfers(nil).Transfer(e.ctx, a, sendPoints(amount, "081234561234"), "482913", key, models.CustomerPinRequestInfo{})
return err
}
assert.ErrorIs(t, send(9, "below-min"), ErrWalletMoveRejected)
assert.ErrorIs(t, send(301, "above-max"), ErrWalletMoveRejected)
require.NoError(t, send(300, "k1"))
require.NoError(t, send(200, "k2"))
// The daily limit is used up; a retry of a transfer already made still replays.
assert.ErrorIs(t, send(10, "k3"), ErrWalletMoveRejected)
require.NoError(t, send(200, "k2"))
// It starts over the next day in the customer's time zone.
e.now = startOfWalletDay(e.now).AddDate(0, 0, 1).Add(time.Minute)
e.repo.clock = e.now
require.NoError(t, send(10, "k4"))
assert.Equal(t, int64(490), e.balance(t, a))
e.settings.Transfer.Enabled = false
assert.ErrorIs(t, send(10, "k5"), ErrWalletMoveRejected)
}
func TestWalletTransfer_HeldAfterPinReset(t *testing.T) {
e := newWalletMoveEnv(t)
a := e.member("Anita", "081200005678")
e.member("Budi", "081234561234")
e.credit(t, earn(a, 100, nil))
until := e.now.Add(time.Hour)
e.pins.err = &PinError{Code: PinErrTransferBlocked, Until: &until}
_, err := e.transfers(nil).Transfer(e.ctx, a, sendPoints(10, "081234561234"), "482913", "key-1", models.CustomerPinRequestInfo{})
var pinErr *PinError
require.True(t, errors.As(err, &pinErr))
assert.Equal(t, PinErrTransferBlocked, pinErr.Code)
assert.Equal(t, int64(100), e.balance(t, a))
}
func TestWalletTransfer_NotEnoughBalance(t *testing.T) {
e := newWalletMoveEnv(t)
a := e.member("Anita", "081200005678")
b := e.member("Budi", "081234561234")
e.credit(t, earn(a, 100, nil))
// An expired lot cannot be sent even before the expiry job takes it.
e.credit(t, earn(a, 50, e.at(-time.Hour)))
_, err := e.transfers(nil).Transfer(e.ctx, a, sendPoints(120, "081234561234"), "482913", "key-1", models.CustomerPinRequestInfo{})
assert.ErrorIs(t, err, ErrWalletMoveRejected)
assert.Equal(t, int64(0), e.balance(t, b))
}
func TestWalletTransfer_RetryMovesNothingAndTellsNobodyAgain(t *testing.T) {
e := newWalletMoveEnv(t)
a := e.member("Anita", "081200005678")
b := e.member("Budi", "081234561234")
e.member("Citra", "081255550000")
e.credit(t, earn(a, 100, nil))
notifier := &notifierFake{}
first, err := e.transfers(notifier).Transfer(e.ctx, a, sendPoints(40, "081234561234"), "482913", "key-1", models.CustomerPinRequestInfo{})
require.NoError(t, err)
again, err := e.transfers(notifier).Transfer(e.ctx, a, sendPoints(40, "081234561234"), "482913", "key-1", models.CustomerPinRequestInfo{})
require.NoError(t, err)
assert.True(t, again.Replayed)
assert.Equal(t, first.GroupID, again.GroupID)
assert.Equal(t, int64(40), e.balance(t, b))
assert.Len(t, notifier.pushes[b], 1)
// The same key to someone else is not a retry.
_, err = e.transfers(notifier).Transfer(e.ctx, a, sendPoints(40, "081255550000"), "482913", "key-1", models.CustomerPinRequestInfo{})
assert.ErrorIs(t, err, ErrWalletIdempotencyConflict)
}
func TestWalletTransfer_UnknownSender(t *testing.T) {
e := newWalletMoveEnv(t)
_, err := e.transfers(nil).Transfer(e.ctx, uuid.New(), sendPoints(1, "081234561234"), "482913", "key-1", models.CustomerPinRequestInfo{})
assert.ErrorIs(t, err, repository.ErrWalletNotFound)
}
func TestMaskName(t *testing.T) {
assert.Equal(t, "Bu*** Sa***", maskName("Budi Santoso"))
assert.Equal(t, "An***", maskName("Anita"))
assert.Equal(t, "A*** Ra***", maskName("Al Rahman"))
assert.Equal(t, "***", maskName(" "))
assert.Equal(t, "08**-****-1234", maskPhoneNumber("081234561234"))
assert.Equal(t, "+6**-****-1234", maskPhoneNumber("+6281234561234"))
assert.Equal(t, "****", maskPhoneNumber("12345"))
}