153 lines
5.0 KiB
Go
153 lines
5.0 KiB
Go
package processor
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"sort"
|
||
|
|
"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/repository"
|
||
|
|
)
|
||
|
|
|
||
|
|
// walletExpiryRepoFake lists the fake wallet's due lots the way the query does, plus
|
||
|
|
// any extra lots a test wants listed.
|
||
|
|
type walletExpiryRepoFake struct {
|
||
|
|
wallet *walletRepoFake
|
||
|
|
extra []repository.DueLot
|
||
|
|
}
|
||
|
|
|
||
|
|
func (f *walletExpiryRepoFake) ListDueLots(_ context.Context, asOf time.Time, limit int) ([]repository.DueLot, error) {
|
||
|
|
descriptions := map[uuid.UUID]string{}
|
||
|
|
for _, tx := range f.wallet.transactions {
|
||
|
|
descriptions[tx.ID] = tx.Description
|
||
|
|
}
|
||
|
|
due := append([]repository.DueLot(nil), f.extra...)
|
||
|
|
for _, lot := range f.wallet.lots {
|
||
|
|
if lot.RemainingAmount > 0 && lot.ExpiresAt != nil && !lot.ExpiresAt.After(asOf) {
|
||
|
|
due = append(due, repository.DueLot{
|
||
|
|
ID: lot.ID, CustomerID: lot.CustomerID, Currency: lot.Currency, Remaining: lot.RemainingAmount,
|
||
|
|
ExpiresAt: *lot.ExpiresAt, SourceDescription: descriptions[lot.SourceTransactionID],
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sort.SliceStable(due, func(i, j int) bool { return due[i].ExpiresAt.Before(due[j].ExpiresAt) })
|
||
|
|
if len(due) > limit {
|
||
|
|
due = due[:limit]
|
||
|
|
}
|
||
|
|
return due, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *walletMoveEnv) expiry(notifier customerNotifier) (*WalletExpiryProcessor, *walletExpiryRepoFake) {
|
||
|
|
repo := &walletExpiryRepoFake{wallet: e.repo}
|
||
|
|
p := NewWalletExpiryProcessor(repo, e.p, txRunnerFake{}, notifier)
|
||
|
|
p.now = func() time.Time { return e.now }
|
||
|
|
return p, repo
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWalletExpiry_ExpiresWhatIsDueAndTellsTheCustomer(t *testing.T) {
|
||
|
|
e := newWalletMoveEnv(t)
|
||
|
|
a := e.member("Anita", "081200005678")
|
||
|
|
ord := earn(a, 150, e.at(-time.Hour))
|
||
|
|
ord.Description = "Belanja #ORD-0098"
|
||
|
|
due := e.credit(t, ord)
|
||
|
|
e.credit(t, earn(a, 50, e.at(-2*time.Hour)))
|
||
|
|
e.credit(t, earn(a, 70, e.at(time.Hour))) // not yet
|
||
|
|
e.credit(t, earn(a, 30, nil)) // never
|
||
|
|
e.earnCoins(t, a, 4, e.at(-time.Minute))
|
||
|
|
// A payment already used part of the first lot; only the rest expires.
|
||
|
|
_, err := e.p.Debit(e.ctx, WalletDebitInput{WalletEntry: pay(a, 20).WalletEntry, PreferredLotIDs: []uuid.UUID{due.Lots[0].ID}})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
notifier := ¬ifierFake{}
|
||
|
|
p, _ := e.expiry(notifier)
|
||
|
|
count, err := p.ExpireDue(e.ctx)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
assert.Equal(t, 3, count)
|
||
|
|
assert.Equal(t, int64(100), e.balance(t, a), "70 not due yet + 30 that never expires")
|
||
|
|
assert.Equal(t, int64(0), e.coinBalance(t, a))
|
||
|
|
|
||
|
|
var expire *entities.WalletTransaction
|
||
|
|
for _, tx := range e.repo.transactions {
|
||
|
|
if tx.Type == constants.WalletTxTypeExpire && tx.ReferenceID == due.Lots[0].ID {
|
||
|
|
expire = tx
|
||
|
|
}
|
||
|
|
}
|
||
|
|
require.NotNil(t, expire)
|
||
|
|
assert.Equal(t, int64(-130), expire.Amount)
|
||
|
|
assert.Equal(t, "Kedaluwarsa: 130 EnakPoint dari Belanja #ORD-0098", expire.Description)
|
||
|
|
assert.Equal(t, constants.WalletRefTypeLot, expire.ReferenceType)
|
||
|
|
assert.Equal(t, "expire:"+due.Lots[0].ID.String(), *expire.IdempotencyKey)
|
||
|
|
|
||
|
|
// One push per currency, with the total.
|
||
|
|
pushes := notifier.pushes[a]
|
||
|
|
require.Len(t, pushes, 2)
|
||
|
|
byCurrency := map[string]pushFake{}
|
||
|
|
for _, p := range pushes {
|
||
|
|
byCurrency[p.data["currency"]] = p
|
||
|
|
}
|
||
|
|
assert.Equal(t, "EnakPoint kedaluwarsa", byCurrency["POINT"].title)
|
||
|
|
assert.Equal(t, "180 EnakPoint kamu sudah kedaluwarsa.", byCurrency["POINT"].body)
|
||
|
|
assert.Equal(t, NotificationTypeWalletExpired, byCurrency["POINT"].data["type"])
|
||
|
|
assert.Equal(t, "4", byCurrency["COIN"].data["amount"])
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWalletExpiry_RunningAgainExpiresNothingMore(t *testing.T) {
|
||
|
|
e := newWalletMoveEnv(t)
|
||
|
|
a := e.member("Anita", "081200005678")
|
||
|
|
e.credit(t, earn(a, 150, e.at(-time.Hour)))
|
||
|
|
notifier := ¬ifierFake{}
|
||
|
|
|
||
|
|
// Two instances, one after the other.
|
||
|
|
first, _ := e.expiry(notifier)
|
||
|
|
second, _ := e.expiry(notifier)
|
||
|
|
n1, err := first.ExpireDue(e.ctx)
|
||
|
|
require.NoError(t, err)
|
||
|
|
n2, err := second.ExpireDue(e.ctx)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
assert.Equal(t, 1, n1)
|
||
|
|
assert.Equal(t, 0, n2)
|
||
|
|
assert.Len(t, notifier.pushes[a], 1)
|
||
|
|
var expires int
|
||
|
|
for _, tx := range e.repo.transactions {
|
||
|
|
if tx.Type == constants.WalletTxTypeExpire {
|
||
|
|
expires++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert.Equal(t, 1, expires)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWalletExpiry_OneFailingLotDoesNotStopTheOthers(t *testing.T) {
|
||
|
|
e := newWalletMoveEnv(t)
|
||
|
|
a := e.member("Anita", "081200005678")
|
||
|
|
e.credit(t, earn(a, 150, e.at(-time.Hour)))
|
||
|
|
p, repo := e.expiry(nil)
|
||
|
|
// A lot listed that ExpireLot cannot find.
|
||
|
|
repo.extra = []repository.DueLot{{ID: uuid.New(), CustomerID: a, Currency: "POINT", Remaining: 5, ExpiresAt: e.now.Add(-3 * time.Hour)}}
|
||
|
|
|
||
|
|
count, err := p.ExpireDue(e.ctx)
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.Equal(t, 1, count)
|
||
|
|
assert.Equal(t, int64(0), e.balance(t, a))
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWalletExpiry_NothingDue(t *testing.T) {
|
||
|
|
e := newWalletMoveEnv(t)
|
||
|
|
a := e.member("Anita", "081200005678")
|
||
|
|
e.credit(t, earn(a, 150, e.at(time.Hour)))
|
||
|
|
notifier := ¬ifierFake{}
|
||
|
|
p, _ := e.expiry(notifier)
|
||
|
|
|
||
|
|
count, err := p.ExpireDue(e.ctx)
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.Zero(t, count)
|
||
|
|
assert.Empty(t, notifier.pushes)
|
||
|
|
}
|