181 lines
7.8 KiB
Go
181 lines
7.8 KiB
Go
package handler
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"os"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
"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/appcontext"
|
||
|
|
applogger "apskel-pos-be/internal/logger"
|
||
|
|
"apskel-pos-be/internal/middleware"
|
||
|
|
"apskel-pos-be/internal/processor"
|
||
|
|
"apskel-pos-be/internal/repository"
|
||
|
|
"apskel-pos-be/internal/service"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Drives GET/PUT /marketing/loyalty-settings and its history over HTTP down to
|
||
|
|
// Postgres. Needs TEST_DATABASE_URL pointing at a migrated database; see
|
||
|
|
// internal/repository/wallet_repository_test.go.
|
||
|
|
func TestOrganizationLoyaltySettingsEndpoints_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)
|
||
|
|
|
||
|
|
org, admin, customer := 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 (?, 'A', 'basic')`, org)
|
||
|
|
exec(`INSERT INTO customers (id, organization_id, name) VALUES (?, ?, 'Budi')`, customer, org)
|
||
|
|
t.Cleanup(func() {
|
||
|
|
db.Exec(`DELETE FROM wallet_lots WHERE customer_id = ?`, customer)
|
||
|
|
db.Exec(`DELETE FROM wallet_transactions WHERE customer_id = ?`, customer)
|
||
|
|
db.Exec(`DELETE FROM customer_wallets WHERE customer_id = ?`, customer)
|
||
|
|
db.Exec(`DELETE FROM loyalty_setting_changes WHERE organization_id = ?`, org)
|
||
|
|
db.Exec(`DELETE FROM organization_settings WHERE organization_id = ?`, org)
|
||
|
|
db.Exec(`DELETE FROM customers WHERE id = ?`, customer)
|
||
|
|
db.Exec(`DELETE FROM organizations WHERE id = ?`, org)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Balances in circulation: 50.000 EnakPoint and 105 EnakCoin.
|
||
|
|
txm := repository.NewTxManager(db)
|
||
|
|
wallet := processor.NewWalletProcessor(repository.NewWalletRepository(db))
|
||
|
|
for _, c := range []struct {
|
||
|
|
currency, ref string
|
||
|
|
amount int64
|
||
|
|
}{{"POINT", "LEGACY_POINTS", 50000}, {"COIN", "LEGACY_TOKENS", 105}} {
|
||
|
|
require.NoError(t, txm.WithTransaction(context.Background(), func(ctx context.Context) error {
|
||
|
|
_, err := wallet.Credit(ctx, processor.WalletCreditInput{WalletEntry: processor.WalletEntry{
|
||
|
|
CustomerID: customer, Currency: c.currency, Type: "MIGRATION", Amount: c.amount,
|
||
|
|
ReferenceType: c.ref, ReferenceID: uuid.New(), Description: "Saldo awal",
|
||
|
|
Metadata: map[string]interface{}{"frozen": true}}})
|
||
|
|
return err
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
settings := processor.NewLoyaltySettingsProcessor(repository.NewLoyaltySettingsRepository(db), txm)
|
||
|
|
h := NewLoyaltySettingsHandler(service.NewLoyaltySettingsService(settings, repository.NewWalletQueryRepository(db)))
|
||
|
|
auth := middleware.NewAuthMiddleware(nil)
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
router := gin.New()
|
||
|
|
for prefix, role := range map[string]string{"/manager": "manager", "/purchasing": "purchasing"} {
|
||
|
|
role := role
|
||
|
|
g := router.Group(prefix, func(c *gin.Context) {
|
||
|
|
ctx := context.WithValue(c.Request.Context(), appcontext.OrganizationIDKey, org.String())
|
||
|
|
ctx = context.WithValue(ctx, appcontext.UserIDKey, admin.String())
|
||
|
|
ctx = context.WithValue(ctx, appcontext.UserRoleKey, role)
|
||
|
|
c.Request = c.Request.WithContext(ctx)
|
||
|
|
})
|
||
|
|
g.GET("/loyalty-settings", h.GetOrganizationSettings)
|
||
|
|
g.PUT("/loyalty-settings", auth.RequireLoyaltyManager(), h.UpdateOrganizationSettings)
|
||
|
|
g.GET("/loyalty-settings/history", h.ListHistory)
|
||
|
|
}
|
||
|
|
call := func(method, path, body string) (int, map[string]any) {
|
||
|
|
t.Helper()
|
||
|
|
req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
router.ServeHTTP(rec, req)
|
||
|
|
var out map[string]any
|
||
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out), rec.Body.String())
|
||
|
|
return rec.Code, out
|
||
|
|
}
|
||
|
|
data := func(body map[string]any) map[string]any { return body["data"].(map[string]any) }
|
||
|
|
|
||
|
|
status, body := call(http.MethodGet, "/manager/loyalty-settings", "")
|
||
|
|
require.Equal(t, http.StatusOK, status, body)
|
||
|
|
got := data(body)
|
||
|
|
assert.EqualValues(t, 1, got["point_value"])
|
||
|
|
assert.Equal(t, map[string]any{"coin_amount": float64(1), "point_amount": float64(1)}, got["exchange"])
|
||
|
|
assert.Equal(t, true, got["transfer"].(map[string]any)["enabled"])
|
||
|
|
assert.Equal(t, "MONTH", got["point_expiry"].(map[string]any)["unit"])
|
||
|
|
impact := got["impact"].(map[string]any)
|
||
|
|
assert.EqualValues(t, 50000, impact["outstanding_points"])
|
||
|
|
assert.EqualValues(t, impact["point_rupiah_before"], impact["point_rupiah_after"])
|
||
|
|
|
||
|
|
// A dry run shows what the balances in circulation would be worth, and saves nothing.
|
||
|
|
change := `{"point_value": 100, "exchange": {"coin_amount": 10, "point_amount": 1}}`
|
||
|
|
status, body = call(http.MethodPut, "/manager/loyalty-settings?dry_run=true", change)
|
||
|
|
require.Equal(t, http.StatusOK, status, body)
|
||
|
|
got = data(body)
|
||
|
|
assert.Equal(t, true, got["dry_run"])
|
||
|
|
impact = got["impact"].(map[string]any)
|
||
|
|
assert.EqualValues(t, 50000, impact["point_rupiah_before"])
|
||
|
|
assert.EqualValues(t, 5000000, impact["point_rupiah_after"])
|
||
|
|
assert.EqualValues(t, 105, impact["coins_as_points_before"])
|
||
|
|
assert.EqualValues(t, 10, impact["coins_as_points_after"])
|
||
|
|
assert.Len(t, got["changes"], 2)
|
||
|
|
status, body = call(http.MethodGet, "/manager/loyalty-settings", "")
|
||
|
|
require.Equal(t, http.StatusOK, status, body)
|
||
|
|
assert.EqualValues(t, 1, data(body)["point_value"], "a dry run saves nothing")
|
||
|
|
status, body = call(http.MethodGet, "/manager/loyalty-settings/history", "")
|
||
|
|
require.Equal(t, http.StatusOK, status, body)
|
||
|
|
assert.EqualValues(t, 0, data(body)["pagination"].(map[string]any)["total_count"])
|
||
|
|
|
||
|
|
// Saving records every change with who made it.
|
||
|
|
status, body = call(http.MethodPut, "/manager/loyalty-settings", change)
|
||
|
|
require.Equal(t, http.StatusOK, status, body)
|
||
|
|
assert.Equal(t, false, data(body)["dry_run"])
|
||
|
|
assert.EqualValues(t, 100, data(body)["point_value"])
|
||
|
|
status, body = call(http.MethodGet, "/manager/loyalty-settings/history", "")
|
||
|
|
require.Equal(t, http.StatusOK, status, body)
|
||
|
|
history := data(body)["data"].([]any)
|
||
|
|
require.Len(t, history, 2)
|
||
|
|
var keys []string
|
||
|
|
for _, raw := range history {
|
||
|
|
row := raw.(map[string]any)
|
||
|
|
keys = append(keys, row["key"].(string))
|
||
|
|
assert.Equal(t, admin.String(), row["changed_by"])
|
||
|
|
assert.Nil(t, row["old_value"], "was on its default")
|
||
|
|
assert.Nil(t, row["outlet_id"])
|
||
|
|
}
|
||
|
|
assert.ElementsMatch(t, []string{"loyalty.point.value", "loyalty.exchange.coin_amount"}, keys)
|
||
|
|
|
||
|
|
// Nothing already written changes: balances and ledger rows keep their numbers.
|
||
|
|
var rows []struct {
|
||
|
|
Amount int64
|
||
|
|
Metadata string
|
||
|
|
}
|
||
|
|
require.NoError(t, db.Raw(`SELECT amount, metadata::text AS metadata FROM wallet_transactions WHERE customer_id = ? ORDER BY currency`, customer).Scan(&rows).Error)
|
||
|
|
require.Len(t, rows, 2)
|
||
|
|
assert.Equal(t, int64(105), rows[0].Amount)
|
||
|
|
assert.Equal(t, int64(50000), rows[1].Amount)
|
||
|
|
for _, r := range rows {
|
||
|
|
assert.Contains(t, r.Metadata, `"frozen": true`)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Out of bounds, unknown fields and the wrong role are refused.
|
||
|
|
for name, bad := range map[string]string{
|
||
|
|
"point value 0": `{"point_value": 0}`,
|
||
|
|
"exchange 0": `{"exchange": {"coin_amount": 0}}`,
|
||
|
|
"expiry unit YEAR": `{"coin_expiry": {"unit": "YEAR"}}`,
|
||
|
|
"unknown field": `{"point_valeu": 5}`,
|
||
|
|
} {
|
||
|
|
status, _ = call(http.MethodPut, "/manager/loyalty-settings", bad)
|
||
|
|
assert.Equal(t, http.StatusBadRequest, status, name)
|
||
|
|
status, _ = call(http.MethodPut, "/manager/loyalty-settings?dry_run=true", bad)
|
||
|
|
assert.Equal(t, http.StatusBadRequest, status, name+" (dry run)")
|
||
|
|
}
|
||
|
|
status, _ = call(http.MethodPut, "/purchasing/loyalty-settings", `{"point_value": 5}`)
|
||
|
|
assert.Equal(t, http.StatusForbidden, status)
|
||
|
|
status, body = call(http.MethodGet, "/manager/loyalty-settings", "")
|
||
|
|
require.Equal(t, http.StatusOK, status, body)
|
||
|
|
assert.EqualValues(t, 100, data(body)["point_value"])
|
||
|
|
}
|