150 lines
5.4 KiB
Go
150 lines
5.4 KiB
Go
package repository_test
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"os"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
"gorm.io/driver/postgres"
|
||
|
|
"gorm.io/gorm"
|
||
|
|
"gorm.io/gorm/logger"
|
||
|
|
|
||
|
|
"apskel-pos-be/internal/constants"
|
||
|
|
applogger "apskel-pos-be/internal/logger"
|
||
|
|
"apskel-pos-be/internal/processor"
|
||
|
|
"apskel-pos-be/internal/repository"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Reads and writes loyalty settings through the processor against Postgres. Needs
|
||
|
|
// TEST_DATABASE_URL pointing at a migrated database; see wallet_repository_test.go.
|
||
|
|
func TestLoyaltySettings_AgainstPostgres(t *testing.T) {
|
||
|
|
dsn := os.Getenv("TEST_DATABASE_URL")
|
||
|
|
if dsn == "" {
|
||
|
|
t.Skip("TEST_DATABASE_URL not set")
|
||
|
|
}
|
||
|
|
applogger.Setup("fatal", "json")
|
||
|
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||
|
|
require.NoError(t, err)
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
org, otherOrg, outlet, admin := uuid.New(), uuid.New(), uuid.New(), uuid.New()
|
||
|
|
exec := func(q string, args ...any) {
|
||
|
|
t.Helper()
|
||
|
|
require.NoError(t, db.Exec(q, args...).Error)
|
||
|
|
}
|
||
|
|
exec(`INSERT INTO organizations (id, name, plan_type) VALUES (?, 'loyalty test', 'basic'), (?, 'other', 'basic')`, org, otherOrg)
|
||
|
|
exec(`INSERT INTO outlets (id, organization_id, name) VALUES (?, ?, 'Kemang')`, outlet, org)
|
||
|
|
// A setting that is not a loyalty setting must be left alone.
|
||
|
|
exec(`INSERT INTO outlet_settings (outlet_id, key, value) VALUES (?, 'receipt.footer', 'Terima kasih')`, outlet)
|
||
|
|
t.Cleanup(func() {
|
||
|
|
db.Exec(`DELETE FROM loyalty_setting_changes WHERE organization_id IN ?`, []uuid.UUID{org, otherOrg})
|
||
|
|
db.Exec(`DELETE FROM outlet_settings WHERE outlet_id = ?`, outlet)
|
||
|
|
db.Exec(`DELETE FROM organization_settings WHERE organization_id IN ?`, []uuid.UUID{org, otherOrg})
|
||
|
|
db.Exec(`DELETE FROM outlets WHERE id = ?`, outlet)
|
||
|
|
db.Exec(`DELETE FROM organizations WHERE id IN ?`, []uuid.UUID{org, otherOrg})
|
||
|
|
})
|
||
|
|
|
||
|
|
repo := repository.NewLoyaltySettingsRepository(db)
|
||
|
|
p := processor.NewLoyaltySettingsProcessor(repo, repository.NewTxManager(db))
|
||
|
|
|
||
|
|
// No settings: every PRD default.
|
||
|
|
settings, err := p.Outlet(ctx, outlet)
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.False(t, settings.Point.Enabled)
|
||
|
|
assert.Equal(t, int64(100), settings.Point.EarnPerAmount)
|
||
|
|
assert.Equal(t, int64(25000), settings.Coin.EarnPerAmount)
|
||
|
|
assert.Nil(t, settings.Point.MaxPerOrder)
|
||
|
|
assert.Equal(t, int64(100), settings.PointPayment.MaxPaymentPercent)
|
||
|
|
|
||
|
|
// Change three keys.
|
||
|
|
settings.Point.Enabled = true
|
||
|
|
settings.Point.EarnPerAmount = 1000
|
||
|
|
settings.Coin.MaxPerOrder = ptrInt64(5)
|
||
|
|
changes, err := p.UpdateOutlet(ctx, org, outlet, admin, *settings)
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.Len(t, changes, 3)
|
||
|
|
|
||
|
|
var stored []struct{ Key, Value string }
|
||
|
|
require.NoError(t, db.Raw(`SELECT key, value FROM outlet_settings WHERE outlet_id = ? ORDER BY key`, outlet).Scan(&stored).Error)
|
||
|
|
assert.Equal(t, []struct{ Key, Value string }{
|
||
|
|
{constants.LoyaltyCoinMaxPerOrderKey, "5"},
|
||
|
|
{constants.LoyaltyPointEarnPerAmountKey, "1000"},
|
||
|
|
{constants.LoyaltyPointEnabledKey, "true"},
|
||
|
|
{"receipt.footer", "Terima kasih"},
|
||
|
|
}, stored, "only the changed keys are stored, next to the untouched non-loyalty key")
|
||
|
|
|
||
|
|
history, err := p.ListChanges(ctx, org, &outlet, 1, 20)
|
||
|
|
require.NoError(t, err)
|
||
|
|
require.Len(t, history.Data, 3)
|
||
|
|
for _, c := range history.Data {
|
||
|
|
assert.Equal(t, admin, c.ChangedBy)
|
||
|
|
assert.Nil(t, c.OldValue, "%s was on its default", c.Key)
|
||
|
|
assert.NotNil(t, c.NewValue)
|
||
|
|
}
|
||
|
|
|
||
|
|
// A second change records the old value.
|
||
|
|
settings.Point.EarnPerAmount = 500
|
||
|
|
changes, err = p.UpdateOutlet(ctx, org, outlet, admin, *settings)
|
||
|
|
require.NoError(t, err)
|
||
|
|
require.Len(t, changes, 1)
|
||
|
|
assert.Equal(t, "1000", *changes[0].OldValue)
|
||
|
|
assert.Equal(t, "500", *changes[0].NewValue)
|
||
|
|
|
||
|
|
// Another organization cannot change this outlet.
|
||
|
|
_, err = p.UpdateOutlet(ctx, otherOrg, outlet, admin, *settings)
|
||
|
|
assert.ErrorIs(t, err, repository.ErrLoyaltyOutletNotFound)
|
||
|
|
|
||
|
|
// Writing outside a transaction is refused.
|
||
|
|
assert.Error(t, repo.SetOutletValue(ctx, outlet, constants.LoyaltyPointEnabledKey, nil))
|
||
|
|
|
||
|
|
// A save must wait while another save of the same organization holds the lock, so
|
||
|
|
// two admins saving at once cannot both compute their change from the same old
|
||
|
|
// value. Hold the lock in another transaction and check the save blocks until it
|
||
|
|
// is released.
|
||
|
|
txm := repository.NewTxManager(db)
|
||
|
|
holding, release, holderDone := make(chan struct{}), make(chan struct{}), make(chan error, 1)
|
||
|
|
go func() {
|
||
|
|
holderDone <- txm.WithTransaction(ctx, func(ctx context.Context) error {
|
||
|
|
if err := repo.LockScope(ctx, org); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
close(holding)
|
||
|
|
<-release
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
}()
|
||
|
|
<-holding
|
||
|
|
|
||
|
|
orgSettings, err := p.Organization(ctx, org)
|
||
|
|
require.NoError(t, err)
|
||
|
|
orgSettings.PointValue = 200
|
||
|
|
saved := make(chan error, 1)
|
||
|
|
go func() {
|
||
|
|
_, err := p.UpdateOrganization(ctx, org, admin, *orgSettings)
|
||
|
|
saved <- err
|
||
|
|
}()
|
||
|
|
select {
|
||
|
|
case err := <-saved:
|
||
|
|
t.Fatalf("the save finished while another transaction held the lock (err: %v)", err)
|
||
|
|
case <-time.After(300 * time.Millisecond):
|
||
|
|
}
|
||
|
|
close(release)
|
||
|
|
require.NoError(t, <-holderDone)
|
||
|
|
require.NoError(t, <-saved)
|
||
|
|
|
||
|
|
value, err := p.PointValue(ctx, org)
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.Equal(t, int64(200), value)
|
||
|
|
|
||
|
|
// The whole organization's history includes the outlet's changes.
|
||
|
|
all, err := p.ListChanges(ctx, org, nil, 1, 100)
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.EqualValues(t, 5, all.Pagination.Total, "four outlet changes and one organization change")
|
||
|
|
}
|
||
|
|
|
||
|
|
func ptrInt64(v int64) *int64 { return &v }
|