2026-09-30 15:31:44 +07:00
|
|
|
package processor
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"os"
|
|
|
|
|
"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/logger"
|
|
|
|
|
"apskel-pos-be/internal/models"
|
|
|
|
|
"apskel-pos-be/internal/repository"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
|
// Some processors log; the server sets the logger up at startup.
|
|
|
|
|
logger.Setup("fatal", "json")
|
|
|
|
|
os.Exit(m.Run())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// loyaltyRepoFake keeps settings in maps. It does not check for a transaction.
|
|
|
|
|
type loyaltyRepoFake struct {
|
|
|
|
|
outletOrg map[uuid.UUID]uuid.UUID
|
|
|
|
|
outletValues map[string]string
|
|
|
|
|
orgValues map[string]string
|
|
|
|
|
changes []entities.LoyaltySettingChange
|
|
|
|
|
locks []uuid.UUID
|
|
|
|
|
// Lots without an expiry per currency: how many and what they hold.
|
|
|
|
|
undated map[string][2]int64
|
|
|
|
|
dated map[string]time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) GetOutletValues(context.Context, uuid.UUID) (map[string]string, error) {
|
|
|
|
|
return copyStringMap(f.outletValues), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) GetOrganizationValues(context.Context, uuid.UUID) (map[string]string, error) {
|
|
|
|
|
return copyStringMap(f.orgValues), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) OutletOrganizationID(_ context.Context, outletID uuid.UUID) (uuid.UUID, error) {
|
|
|
|
|
org, ok := f.outletOrg[outletID]
|
|
|
|
|
if !ok {
|
|
|
|
|
return uuid.Nil, repository.ErrLoyaltyOutletNotFound
|
|
|
|
|
}
|
|
|
|
|
return org, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) LockScope(_ context.Context, id uuid.UUID) error {
|
|
|
|
|
f.locks = append(f.locks, id)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) SetOutletValue(_ context.Context, _ uuid.UUID, key string, value *string) error {
|
|
|
|
|
f.outletValues = setOptional(f.outletValues, key, value)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) SetOrganizationValue(_ context.Context, _ uuid.UUID, key string, value *string) error {
|
|
|
|
|
f.orgValues = setOptional(f.orgValues, key, value)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) InsertChanges(_ context.Context, changes []entities.LoyaltySettingChange) error {
|
|
|
|
|
f.changes = append(f.changes, changes...)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) ListChanges(context.Context, repository.LoyaltySettingChangeFilter) ([]entities.LoyaltySettingChange, int64, error) {
|
|
|
|
|
return f.changes, int64(len(f.changes)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func copyStringMap(m map[string]string) map[string]string {
|
|
|
|
|
out := make(map[string]string, len(m))
|
|
|
|
|
for k, v := range m {
|
|
|
|
|
out[k] = v
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setOptional(m map[string]string, key string, value *string) map[string]string {
|
|
|
|
|
if m == nil {
|
|
|
|
|
m = map[string]string{}
|
|
|
|
|
}
|
|
|
|
|
if value == nil {
|
|
|
|
|
delete(m, key)
|
|
|
|
|
} else {
|
|
|
|
|
m[key] = *value
|
|
|
|
|
}
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// txRunnerFake runs fn in place.
|
|
|
|
|
type txRunnerFake struct{}
|
|
|
|
|
|
|
|
|
|
func (txRunnerFake) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
|
|
|
|
|
return fn(ctx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoyaltySettings_OutletWithoutSettingsGetsEveryDefault(t *testing.T) {
|
|
|
|
|
p := NewLoyaltySettingsProcessor(&loyaltyRepoFake{}, txRunnerFake{})
|
|
|
|
|
s, err := p.Outlet(context.Background(), uuid.New())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, models.OutletLoyaltySettings{
|
2026-10-02 14:19:12 +07:00
|
|
|
Point: models.LoyaltyEarnSettings{Enabled: false, EarnMode: "PER_AMOUNT", EarnPerAmount: 100, EarnValue: 1, EarnPercent: 1, MinOrderAmount: 0, MaxPerOrder: nil},
|
|
|
|
|
Coin: models.LoyaltyEarnSettings{Enabled: false, EarnMode: "PER_AMOUNT", EarnPerAmount: 25000, EarnValue: 1, EarnPercent: 1, MinOrderAmount: 0, MaxPerOrder: nil},
|
2026-09-30 15:31:44 +07:00
|
|
|
PointPayment: models.LoyaltyPointPaymentSettings{AcceptPayment: false, MinPaymentPoints: 1, MaxPaymentPercent: 100},
|
|
|
|
|
}, *s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoyaltySettings_OrganizationWithoutSettingsGetsEveryDefault(t *testing.T) {
|
|
|
|
|
p := NewLoyaltySettingsProcessor(&loyaltyRepoFake{}, txRunnerFake{})
|
|
|
|
|
s, err := p.Organization(context.Background(), uuid.New())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
expiry := models.LoyaltyExpirySettings{Enabled: false, Mode: "FIXED_DATE", FixedDates: []string{"12-31"}, GraceMonths: 3, Period: 12, Unit: "MONTH", EndOfMonth: false, ReminderDays: 7}
|
|
|
|
|
assert.Equal(t, models.OrganizationLoyaltySettings{
|
|
|
|
|
PointValue: 1,
|
|
|
|
|
Exchange: models.LoyaltyExchangeSettings{CoinAmount: 1, PointAmount: 1},
|
|
|
|
|
Transfer: models.LoyaltyTransferSettings{Enabled: true, MinAmount: 1},
|
|
|
|
|
PointExpiry: expiry,
|
|
|
|
|
CoinExpiry: expiry,
|
|
|
|
|
}, *s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoyaltySettings_StoredValuesAreTyped(t *testing.T) {
|
|
|
|
|
repo := &loyaltyRepoFake{
|
|
|
|
|
outletValues: map[string]string{
|
|
|
|
|
constants.LoyaltyPointEnabledKey: "true",
|
|
|
|
|
constants.LoyaltyPointEarnPerAmountKey: " 1000 ",
|
|
|
|
|
constants.LoyaltyPointMaxPerOrderKey: "500",
|
|
|
|
|
constants.LoyaltyPointMaxPaymentPercentKey: "50",
|
|
|
|
|
"loyalty.unknown": "ignored",
|
|
|
|
|
},
|
|
|
|
|
orgValues: map[string]string{
|
|
|
|
|
constants.LoyaltyPointValueKey: "100",
|
|
|
|
|
constants.LoyaltyTransferDailyLimitKey: "10000",
|
|
|
|
|
"loyalty.coin.expiry_unit": "day",
|
|
|
|
|
"loyalty.point.expiry_fixed_dates": "12-31, 06-30,12-31",
|
|
|
|
|
"loyalty.coin.expiry_mode": "rolling",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
p := NewLoyaltySettingsProcessor(repo, txRunnerFake{})
|
|
|
|
|
|
|
|
|
|
outlet, err := p.Outlet(context.Background(), uuid.New())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.True(t, outlet.Point.Enabled)
|
|
|
|
|
assert.Equal(t, int64(1000), outlet.Point.EarnPerAmount)
|
|
|
|
|
assert.Equal(t, int64(500), *outlet.Point.MaxPerOrder)
|
|
|
|
|
assert.Equal(t, int64(50), outlet.PointPayment.MaxPaymentPercent)
|
|
|
|
|
|
|
|
|
|
org, err := p.Organization(context.Background(), uuid.New())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, int64(100), org.PointValue)
|
|
|
|
|
assert.Equal(t, int64(10000), *org.Transfer.DailyLimit)
|
|
|
|
|
assert.Equal(t, "DAY", org.CoinExpiry.Unit)
|
|
|
|
|
assert.Equal(t, "MONTH", org.PointExpiry.Unit)
|
|
|
|
|
assert.Equal(t, []string{"06-30", "12-31"}, org.PointExpiry.FixedDates)
|
|
|
|
|
assert.Equal(t, "ROLLING", org.CoinExpiry.Mode)
|
|
|
|
|
assert.Equal(t, "FIXED_DATE", org.PointExpiry.Mode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A bad row must never reach a calculation: earn_per_amount 0 would divide by zero.
|
|
|
|
|
func TestLoyaltySettings_UnusableStoredValuesFallBackToDefault(t *testing.T) {
|
|
|
|
|
repo := &loyaltyRepoFake{
|
|
|
|
|
outletValues: map[string]string{
|
|
|
|
|
constants.LoyaltyPointEnabledKey: "yes please",
|
|
|
|
|
constants.LoyaltyPointEarnPerAmountKey: "0",
|
|
|
|
|
constants.LoyaltyCoinEarnValueKey: "-1",
|
|
|
|
|
constants.LoyaltyPointMaxPerOrderKey: "abc",
|
|
|
|
|
constants.LoyaltyPointMaxPaymentPercentKey: "150",
|
|
|
|
|
},
|
|
|
|
|
orgValues: map[string]string{
|
|
|
|
|
constants.LoyaltyPointValueKey: "0",
|
|
|
|
|
"loyalty.point.expiry_unit": "YEAR",
|
|
|
|
|
"loyalty.point.expiry_period": "",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
p := NewLoyaltySettingsProcessor(repo, txRunnerFake{})
|
|
|
|
|
|
|
|
|
|
outlet, err := p.Outlet(context.Background(), uuid.New())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.False(t, outlet.Point.Enabled)
|
|
|
|
|
assert.Equal(t, int64(100), outlet.Point.EarnPerAmount)
|
|
|
|
|
assert.Equal(t, int64(1), outlet.Coin.EarnValue)
|
|
|
|
|
assert.Nil(t, outlet.Point.MaxPerOrder)
|
|
|
|
|
assert.Equal(t, int64(100), outlet.PointPayment.MaxPaymentPercent)
|
|
|
|
|
|
|
|
|
|
for name, raw := range map[string]string{"not set": "", "garbage": "abc", "zero": "0", "negative": "-5"} {
|
|
|
|
|
repo.orgValues = map[string]string{}
|
|
|
|
|
if raw != "" {
|
|
|
|
|
repo.orgValues[constants.LoyaltyPointValueKey] = raw
|
|
|
|
|
}
|
|
|
|
|
value, err := p.PointValue(context.Background(), uuid.New())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, constants.LoyaltyPointValueDefault, value, name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoyaltySettings_UpdateRecordsOnlyRealChanges(t *testing.T) {
|
|
|
|
|
org, outlet, admin := uuid.New(), uuid.New(), uuid.New()
|
|
|
|
|
repo := &loyaltyRepoFake{
|
|
|
|
|
outletOrg: map[uuid.UUID]uuid.UUID{outlet: org},
|
|
|
|
|
outletValues: map[string]string{constants.LoyaltyPointEarnPerAmountKey: "200"},
|
|
|
|
|
}
|
|
|
|
|
p := NewLoyaltySettingsProcessor(repo, txRunnerFake{})
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
settings, err := p.Outlet(ctx, outlet)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
settings.Point.Enabled = true // default false -> true
|
|
|
|
|
settings.Point.EarnPerAmount = 100 // stored 200 -> 100 (the default, but set explicitly)
|
|
|
|
|
settings.Point.MaxPerOrder = ptr(int64(0)) // unset -> 0
|
|
|
|
|
// Everything else stays on its default.
|
|
|
|
|
|
|
|
|
|
changes, err := p.UpdateOutlet(ctx, org, outlet, admin, *settings)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
byKey := map[string]models.LoyaltySettingChange{}
|
|
|
|
|
for _, c := range changes {
|
|
|
|
|
byKey[c.Key] = c
|
|
|
|
|
assert.Equal(t, admin, c.ChangedBy)
|
|
|
|
|
assert.Equal(t, org, c.OrganizationID)
|
|
|
|
|
assert.Equal(t, &outlet, c.OutletID)
|
|
|
|
|
}
|
|
|
|
|
require.Len(t, byKey, 3, "only the three keys that changed")
|
|
|
|
|
assert.Nil(t, byKey[constants.LoyaltyPointEnabledKey].OldValue, "was on its default")
|
|
|
|
|
assert.Equal(t, "true", *byKey[constants.LoyaltyPointEnabledKey].NewValue)
|
|
|
|
|
assert.Equal(t, "200", *byKey[constants.LoyaltyPointEarnPerAmountKey].OldValue)
|
|
|
|
|
assert.Equal(t, "100", *byKey[constants.LoyaltyPointEarnPerAmountKey].NewValue)
|
|
|
|
|
assert.Equal(t, "0", *byKey[constants.LoyaltyPointMaxPerOrderKey].NewValue)
|
|
|
|
|
assert.Len(t, repo.changes, 3)
|
|
|
|
|
assert.Equal(t, []uuid.UUID{outlet}, repo.locks)
|
|
|
|
|
|
|
|
|
|
after, err := p.Outlet(ctx, outlet)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, *settings, *after)
|
|
|
|
|
|
|
|
|
|
// Saving the same settings again changes nothing and records nothing.
|
|
|
|
|
changes, err = p.UpdateOutlet(ctx, org, outlet, admin, *after)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Empty(t, changes)
|
|
|
|
|
assert.Len(t, repo.changes, 3)
|
|
|
|
|
|
|
|
|
|
// Clearing a limit removes the stored value, recorded as a change to nothing.
|
|
|
|
|
after.Point.MaxPerOrder = nil
|
|
|
|
|
changes, err = p.UpdateOutlet(ctx, org, outlet, admin, *after)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, changes, 1)
|
|
|
|
|
assert.Equal(t, "0", *changes[0].OldValue)
|
|
|
|
|
assert.Nil(t, changes[0].NewValue)
|
|
|
|
|
assert.NotContains(t, repo.outletValues, constants.LoyaltyPointMaxPerOrderKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoyaltySettings_UpdateOrganization(t *testing.T) {
|
|
|
|
|
org, admin := uuid.New(), uuid.New()
|
|
|
|
|
repo := &loyaltyRepoFake{}
|
|
|
|
|
p := NewLoyaltySettingsProcessor(repo, txRunnerFake{})
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
settings, err := p.Organization(ctx, org)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
settings.PointValue = 100
|
|
|
|
|
settings.CoinExpiry.Enabled = true
|
|
|
|
|
settings.CoinExpiry.Unit = constants.LoyaltyExpiryUnitDay
|
|
|
|
|
|
|
|
|
|
changes, _, err := p.UpdateOrganization(ctx, org, admin, *settings)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
keys := []string{}
|
|
|
|
|
for _, c := range changes {
|
|
|
|
|
keys = append(keys, c.Key)
|
|
|
|
|
assert.Nil(t, c.OutletID, "organization settings have no outlet")
|
|
|
|
|
}
|
|
|
|
|
assert.ElementsMatch(t, []string{constants.LoyaltyPointValueKey, "loyalty.coin.expiry_enabled", "loyalty.coin.expiry_unit"}, keys)
|
|
|
|
|
|
|
|
|
|
value, err := p.PointValue(ctx, org)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, int64(100), value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoyaltySettings_UpdateRejectsInvalidValues(t *testing.T) {
|
|
|
|
|
org, outlet := uuid.New(), uuid.New()
|
|
|
|
|
repo := &loyaltyRepoFake{outletOrg: map[uuid.UUID]uuid.UUID{outlet: org}}
|
|
|
|
|
p := NewLoyaltySettingsProcessor(repo, txRunnerFake{})
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
for name, mutate := range map[string]func(*models.OutletLoyaltySettings){
|
|
|
|
|
"earn_per_amount 0": func(s *models.OutletLoyaltySettings) { s.Point.EarnPerAmount = 0 },
|
|
|
|
|
"negative earn_value": func(s *models.OutletLoyaltySettings) { s.Coin.EarnValue = -1 },
|
2026-10-02 14:19:12 +07:00
|
|
|
"earn mode unknown": func(s *models.OutletLoyaltySettings) { s.Point.EarnMode = "PERCENT" },
|
|
|
|
|
"negative earn_percent": func(s *models.OutletLoyaltySettings) { s.Point.EarnPercent = -1 },
|
|
|
|
|
"earn_percent over 100": func(s *models.OutletLoyaltySettings) { s.Coin.EarnPercent = 100.5 },
|
|
|
|
|
"earn_percent 3 decimals": func(s *models.OutletLoyaltySettings) { s.Point.EarnPercent = 1.125 },
|
2026-09-30 15:31:44 +07:00
|
|
|
"negative min_order": func(s *models.OutletLoyaltySettings) { s.Point.MinOrderAmount = -1 },
|
|
|
|
|
"negative max_per_order": func(s *models.OutletLoyaltySettings) { s.Coin.MaxPerOrder = ptr(int64(-1)) },
|
|
|
|
|
"payment percent over 100": func(s *models.OutletLoyaltySettings) { s.PointPayment.MaxPaymentPercent = 101 },
|
|
|
|
|
"negative payment percent": func(s *models.OutletLoyaltySettings) { s.PointPayment.MaxPaymentPercent = -1 },
|
|
|
|
|
"min payment points 0": func(s *models.OutletLoyaltySettings) { s.PointPayment.MinPaymentPoints = 0 },
|
|
|
|
|
} {
|
|
|
|
|
s, err := p.Outlet(ctx, outlet)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
mutate(s)
|
|
|
|
|
_, err = p.UpdateOutlet(ctx, org, outlet, uuid.New(), *s)
|
|
|
|
|
assert.ErrorIs(t, err, ErrInvalidLoyaltySettings, name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for name, mutate := range map[string]func(*models.OrganizationLoyaltySettings){
|
|
|
|
|
"point value 0": func(s *models.OrganizationLoyaltySettings) { s.PointValue = 0 },
|
|
|
|
|
"exchange coin amount 0": func(s *models.OrganizationLoyaltySettings) { s.Exchange.CoinAmount = 0 },
|
|
|
|
|
"transfer min 0": func(s *models.OrganizationLoyaltySettings) { s.Transfer.MinAmount = 0 },
|
|
|
|
|
"daily limit 0": func(s *models.OrganizationLoyaltySettings) { s.Transfer.DailyLimit = ptr(int64(0)) },
|
|
|
|
|
"expiry period 0": func(s *models.OrganizationLoyaltySettings) { s.PointExpiry.Period = 0 },
|
|
|
|
|
"expiry unit YEAR": func(s *models.OrganizationLoyaltySettings) { s.CoinExpiry.Unit = "YEAR" },
|
|
|
|
|
"negative reminder days": func(s *models.OrganizationLoyaltySettings) { s.CoinExpiry.ReminderDays = -1 },
|
|
|
|
|
"expiry mode YEARLY": func(s *models.OrganizationLoyaltySettings) { s.PointExpiry.Mode = "YEARLY" },
|
|
|
|
|
"no fixed dates": func(s *models.OrganizationLoyaltySettings) { s.PointExpiry.FixedDates = nil },
|
|
|
|
|
"fixed date 13-01": func(s *models.OrganizationLoyaltySettings) { s.PointExpiry.FixedDates = []string{"13-01"} },
|
|
|
|
|
"fixed date 02-29": func(s *models.OrganizationLoyaltySettings) { s.CoinExpiry.FixedDates = []string{"02-29"} },
|
|
|
|
|
"negative grace": func(s *models.OrganizationLoyaltySettings) { s.CoinExpiry.GraceMonths = -1 },
|
|
|
|
|
} {
|
|
|
|
|
s, err := p.Organization(ctx, org)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
mutate(s)
|
|
|
|
|
_, _, err = p.UpdateOrganization(ctx, org, uuid.New(), *s)
|
|
|
|
|
assert.ErrorIs(t, err, ErrInvalidLoyaltySettings, name)
|
|
|
|
|
}
|
|
|
|
|
assert.Empty(t, repo.changes, "nothing is written when a value is invalid")
|
|
|
|
|
assert.Empty(t, repo.locks, "invalid input is rejected before touching the database")
|
|
|
|
|
|
|
|
|
|
// An outlet of another organization.
|
|
|
|
|
s, err := p.Outlet(ctx, outlet)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
_, err = p.UpdateOutlet(ctx, uuid.New(), outlet, uuid.New(), *s)
|
|
|
|
|
assert.ErrorIs(t, err, repository.ErrLoyaltyOutletNotFound)
|
|
|
|
|
_, err = p.UpdateOutlet(ctx, org, uuid.New(), uuid.New(), *s)
|
|
|
|
|
assert.ErrorIs(t, err, repository.ErrLoyaltyOutletNotFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) UndatedBalance(_ context.Context, _ uuid.UUID, currency string) (int64, int64, error) {
|
|
|
|
|
u := f.undated[currency]
|
|
|
|
|
return u[0], u[1], nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *loyaltyRepoFake) DateUndatedLots(_ context.Context, _ uuid.UUID, currency string, expiresAt time.Time) (int64, int64, error) {
|
|
|
|
|
u := f.undated[currency]
|
|
|
|
|
if f.dated == nil {
|
|
|
|
|
f.dated = map[string]time.Time{}
|
|
|
|
|
}
|
|
|
|
|
f.dated[currency] = expiresAt
|
|
|
|
|
delete(f.undated, currency)
|
|
|
|
|
return u[0], u[1], nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Turning expiry on dates the balances that had none, a full cycle away; a dry run
|
|
|
|
|
// only counts them, and saving again dates nothing more.
|
|
|
|
|
func TestLoyaltySettings_TurningExpiryOnDatesExistingBalances(t *testing.T) {
|
|
|
|
|
org, admin := uuid.New(), uuid.New()
|
|
|
|
|
repo := &loyaltyRepoFake{undated: map[string][2]int64{"POINT": {3, 1500}, "COIN": {2, 40}}}
|
|
|
|
|
p := NewLoyaltySettingsProcessor(repo, txRunnerFake{})
|
|
|
|
|
p.now = func() time.Time { return time.Date(2026, 11, 20, 3, 0, 0, 0, time.UTC) }
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
settings, err := p.Organization(ctx, org)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
settings.PointExpiry.Enabled = true
|
|
|
|
|
|
|
|
|
|
_, preview, err := p.PreviewOrganization(ctx, org, *settings)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, preview, 1)
|
|
|
|
|
assert.Equal(t, "POINT", preview[0].Currency)
|
|
|
|
|
assert.Equal(t, int64(1500), preview[0].Amount)
|
|
|
|
|
assert.Empty(t, repo.dated, "a dry run dates nothing")
|
|
|
|
|
|
|
|
|
|
_, activations, err := p.UpdateOrganization(ctx, org, admin, *settings)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, activations, 1)
|
|
|
|
|
assert.Equal(t, int64(3), activations[0].Lots)
|
|
|
|
|
// Default FIXED_DATE on 31 Dec: the second one after 20 Nov 2026.
|
|
|
|
|
assert.Equal(t, "2027-12-31", activations[0].ExpiresAt.In(walletDisplayLocation).Format("2006-01-02"))
|
|
|
|
|
assert.Equal(t, activations[0].ExpiresAt, repo.dated["POINT"])
|
|
|
|
|
assert.NotContains(t, repo.dated, "COIN", "EnakCoin expiry stays off")
|
|
|
|
|
|
|
|
|
|
_, again, err := p.UpdateOrganization(ctx, org, admin, *settings)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Empty(t, again, "already on: nothing is activated twice")
|
|
|
|
|
}
|