refactor(vouchers): move voucher routes out of /enakgame
Vouchers are what EnakPoint is redeemed for, wherever it came from, so they are not part of EnakGame. Their only link to it is the budget attribution, which does not change. - Admin: /marketing/enakgame/vouchers... -> /marketing/vouchers... - Customer: /customer/enakgame/vouchers -> /customer/vouchers, /customer/enakgame/vouchers/:id/redeem -> /customer/vouchers/:id/redeem, /customer/enakgame/redemptions -> /customer/vouchers/redemptions Roles, handlers and logic stay the same. No client calls these endpoints yet. RFC §7.4 and §11 updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
18e87398bb
commit
1bb071aec6
@@ -285,7 +285,7 @@ func (h *EnakGameAdminHandler) EconomyAnalytics(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.EconomyAnalytics(ctx, appcontext.FromGinContext(ctx), q), method)
|
||||
}
|
||||
|
||||
// CreateVoucher is POST /marketing/enakgame/vouchers.
|
||||
// CreateVoucher is POST /marketing/vouchers.
|
||||
func (h *EnakGameAdminHandler) CreateVoucher(c *gin.Context) {
|
||||
const method = "EnakGameAdminHandler::CreateVoucher"
|
||||
body, ok := rawBody(c, method)
|
||||
@@ -296,7 +296,7 @@ func (h *EnakGameAdminHandler) CreateVoucher(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.CreateVoucher(ctx, appcontext.FromGinContext(ctx), body), method)
|
||||
}
|
||||
|
||||
// ListVouchers is GET /marketing/enakgame/vouchers?status=&search=&page=&limit=.
|
||||
// ListVouchers is GET /marketing/vouchers?status=&search=&page=&limit=.
|
||||
func (h *EnakGameAdminHandler) ListVouchers(c *gin.Context) {
|
||||
const method = "EnakGameAdminHandler::ListVouchers"
|
||||
var q models.VoucherListQuery
|
||||
@@ -307,7 +307,7 @@ func (h *EnakGameAdminHandler) ListVouchers(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.ListVouchers(ctx, appcontext.FromGinContext(ctx), q), method)
|
||||
}
|
||||
|
||||
// GetVoucher is GET /marketing/enakgame/vouchers/:id.
|
||||
// GetVoucher is GET /marketing/vouchers/:id.
|
||||
func (h *EnakGameAdminHandler) GetVoucher(c *gin.Context) {
|
||||
const method = "EnakGameAdminHandler::GetVoucher"
|
||||
id, ok := pathID(c, "id", method)
|
||||
@@ -318,7 +318,7 @@ func (h *EnakGameAdminHandler) GetVoucher(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.GetVoucher(ctx, appcontext.FromGinContext(ctx), id), method)
|
||||
}
|
||||
|
||||
// UpdateVoucher is PUT /marketing/enakgame/vouchers/:id.
|
||||
// UpdateVoucher is PUT /marketing/vouchers/:id.
|
||||
func (h *EnakGameAdminHandler) UpdateVoucher(c *gin.Context) {
|
||||
const method = "EnakGameAdminHandler::UpdateVoucher"
|
||||
id, ok := pathID(c, "id", method)
|
||||
@@ -333,7 +333,7 @@ func (h *EnakGameAdminHandler) UpdateVoucher(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.UpdateVoucher(ctx, appcontext.FromGinContext(ctx), id, body), method)
|
||||
}
|
||||
|
||||
// SetVoucherStatus is PUT /marketing/enakgame/vouchers/:id/status.
|
||||
// SetVoucherStatus is PUT /marketing/vouchers/:id/status.
|
||||
func (h *EnakGameAdminHandler) SetVoucherStatus(c *gin.Context) {
|
||||
const method = "EnakGameAdminHandler::SetVoucherStatus"
|
||||
id, ok := pathID(c, "id", method)
|
||||
@@ -348,7 +348,7 @@ func (h *EnakGameAdminHandler) SetVoucherStatus(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.SetVoucherStatus(ctx, appcontext.FromGinContext(ctx), id, body), method)
|
||||
}
|
||||
|
||||
// ImportVoucherCodes is POST /marketing/enakgame/vouchers/:id/codes, with the CSV as
|
||||
// ImportVoucherCodes is POST /marketing/vouchers/:id/codes, with the CSV as
|
||||
// a multipart file named "file" or as the request body.
|
||||
func (h *EnakGameAdminHandler) ImportVoucherCodes(c *gin.Context) {
|
||||
const method = "EnakGameAdminHandler::ImportVoucherCodes"
|
||||
@@ -379,7 +379,7 @@ func (h *EnakGameAdminHandler) ImportVoucherCodes(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.ImportVoucherCodes(ctx, appcontext.FromGinContext(ctx), id, data), method)
|
||||
}
|
||||
|
||||
// ListVoucherCodes is GET /marketing/enakgame/vouchers/:id/codes?status=&page=&limit=.
|
||||
// ListVoucherCodes is GET /marketing/vouchers/:id/codes?status=&page=&limit=.
|
||||
func (h *EnakGameAdminHandler) ListVoucherCodes(c *gin.Context) {
|
||||
const method = "EnakGameAdminHandler::ListVoucherCodes"
|
||||
id, ok := pathID(c, "id", method)
|
||||
|
||||
@@ -106,7 +106,7 @@ func (h *EnakGameCustomerHandler) CompleteSession(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.CompleteSession(c.Request.Context(), customerID, id, in), method)
|
||||
}
|
||||
|
||||
// ListVouchers is GET /customer/enakgame/vouchers.
|
||||
// ListVouchers is GET /customer/vouchers.
|
||||
func (h *EnakGameCustomerHandler) ListVouchers(c *gin.Context) {
|
||||
const method = "EnakGameCustomerHandler::ListVouchers"
|
||||
customerID, ok := customerIDFromGin(c, method)
|
||||
@@ -116,7 +116,7 @@ func (h *EnakGameCustomerHandler) ListVouchers(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.ListVouchers(c.Request.Context(), customerID), method)
|
||||
}
|
||||
|
||||
// RedeemVoucher is POST /customer/enakgame/vouchers/:id/redeem. It requires the PIN
|
||||
// RedeemVoucher is POST /customer/vouchers/:id/redeem. It requires the PIN
|
||||
// and Idempotency-Key; the body holds the PIN, so it is never logged.
|
||||
func (h *EnakGameCustomerHandler) RedeemVoucher(c *gin.Context) {
|
||||
const method = "EnakGameCustomerHandler::RedeemVoucher"
|
||||
@@ -135,7 +135,7 @@ func (h *EnakGameCustomerHandler) RedeemVoucher(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, h.service.RedeemVoucher(c.Request.Context(), customerID, id, &req, idempotencyKey(c), pinRequestInfo(c)), method)
|
||||
}
|
||||
|
||||
// ListRedemptions is GET /customer/enakgame/redemptions?page=&limit=.
|
||||
// ListRedemptions is GET /customer/vouchers/redemptions?page=&limit=.
|
||||
func (h *EnakGameCustomerHandler) ListRedemptions(c *gin.Context) {
|
||||
const method = "EnakGameCustomerHandler::ListRedemptions"
|
||||
customerID, ok := customerIDFromGin(c, method)
|
||||
|
||||
@@ -285,7 +285,7 @@ func TestEnakGameVoucherEndpoints_AgainstPostgres(t *testing.T) {
|
||||
router := gin.New()
|
||||
for prefix, role := range map[string]string{"/manager": "manager", "/purchasing": "purchasing"} {
|
||||
role := role
|
||||
g := router.Group(prefix+"/enakgame", func(c *gin.Context) {
|
||||
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)
|
||||
@@ -296,10 +296,10 @@ func TestEnakGameVoucherEndpoints_AgainstPostgres(t *testing.T) {
|
||||
g.POST("/vouchers/:id/codes", auth.RequireLoyaltyManager(), adminHandler.ImportVoucherCodes)
|
||||
g.GET("/vouchers/:id/codes", adminHandler.ListVoucherCodes)
|
||||
}
|
||||
c := router.Group("/customer/enakgame", func(c *gin.Context) { c.Set("customer_id", customer.String()) })
|
||||
c := router.Group("/customer", func(c *gin.Context) { c.Set("customer_id", customer.String()) })
|
||||
c.GET("/vouchers", customerHandler.ListVouchers)
|
||||
c.POST("/vouchers/:id/redeem", customerHandler.RedeemVoucher)
|
||||
c.GET("/redemptions", customerHandler.ListRedemptions)
|
||||
c.GET("/vouchers/redemptions", customerHandler.ListRedemptions)
|
||||
|
||||
send := func(req *http.Request) (int, map[string]any) {
|
||||
t.Helper()
|
||||
@@ -320,12 +320,12 @@ func TestEnakGameVoucherEndpoints_AgainstPostgres(t *testing.T) {
|
||||
data := func(body map[string]any) map[string]any { return body["data"].(map[string]any) }
|
||||
|
||||
voucher := `{"name": "Kopi", "voucher_type": "FREE_ITEM", "face_value": 20000, "point_cost": 15000, "stock_mode": "CODE_POOL", "status": "ACTIVE"}`
|
||||
status, _ := call(http.MethodPost, "/purchasing/enakgame/vouchers", voucher)
|
||||
status, _ := call(http.MethodPost, "/purchasing/vouchers", voucher)
|
||||
assert.Equal(t, http.StatusForbidden, status)
|
||||
status, body := call(http.MethodPost, "/manager/enakgame/vouchers", voucher)
|
||||
status, body := call(http.MethodPost, "/manager/vouchers", voucher)
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
id := data(body)["id"].(string)
|
||||
status, _ = call(http.MethodPut, "/manager/enakgame/vouchers/"+id, `{"stock_mode": "STATIC", "stock": 5}`)
|
||||
status, _ = call(http.MethodPut, "/manager/vouchers/"+id, `{"stock_mode": "STATIC", "stock": 5}`)
|
||||
assert.Equal(t, http.StatusBadRequest, status, "the stock mode stays")
|
||||
|
||||
// Codes as a multipart file, then the same as a plain body.
|
||||
@@ -335,25 +335,25 @@ func TestEnakGameVoucherEndpoints_AgainstPostgres(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
_, _ = part.Write([]byte("code\nKOPI-1\nKOPI-2\n"))
|
||||
require.NoError(t, w.Close())
|
||||
req := httptest.NewRequest(http.MethodPost, "/manager/enakgame/vouchers/"+id+"/codes", &form)
|
||||
req := httptest.NewRequest(http.MethodPost, "/manager/vouchers/"+id+"/codes", &form)
|
||||
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||
status, body = send(req)
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 2, data(body)["imported"])
|
||||
status, body = call(http.MethodPost, "/manager/enakgame/vouchers/"+id+"/codes", "KOPI-2\nKOPI-3\n", "Content-Type", "text/csv")
|
||||
status, body = call(http.MethodPost, "/manager/vouchers/"+id+"/codes", "KOPI-2\nKOPI-3\n", "Content-Type", "text/csv")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 1, data(body)["imported"])
|
||||
assert.Equal(t, []any{"KOPI-2"}, data(body)["duplicates"])
|
||||
status, body = call(http.MethodGet, "/manager/enakgame/vouchers/"+id+"/codes", "")
|
||||
status, body = call(http.MethodGet, "/manager/vouchers/"+id+"/codes", "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.Equal(t, map[string]any{"AVAILABLE": float64(3)}, data(body)["counts"])
|
||||
|
||||
status, body = call(http.MethodGet, "/customer/enakgame/vouchers", "")
|
||||
status, body = call(http.MethodGet, "/customer/vouchers", "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
require.Len(t, body["data"], 1)
|
||||
assert.EqualValues(t, 3, body["data"].([]any)[0].(map[string]any)["available"])
|
||||
|
||||
redeem := "/customer/enakgame/vouchers/" + id + "/redeem"
|
||||
redeem := "/customer/vouchers/" + id + "/redeem"
|
||||
status, body = call(http.MethodPost, redeem, `{"pin": "482913"}`)
|
||||
assert.Equal(t, http.StatusBadRequest, status, "Idempotency-Key is required")
|
||||
status, body = call(http.MethodPost, redeem, `{}`, "Idempotency-Key", "r1")
|
||||
@@ -377,10 +377,10 @@ func TestEnakGameVoucherEndpoints_AgainstPostgres(t *testing.T) {
|
||||
assert.EqualValues(t, 5_000, data(body)["point_balance"])
|
||||
status, _ = call(http.MethodPost, redeem, `{"pin": "482913"}`, "Idempotency-Key", "r5")
|
||||
assert.Equal(t, http.StatusBadRequest, status, "out of codes and of EnakPoint")
|
||||
status, _ = call(http.MethodPost, "/customer/enakgame/vouchers/"+uuid.NewString()+"/redeem", `{"pin": "482913"}`, "Idempotency-Key", "r4")
|
||||
status, _ = call(http.MethodPost, "/customer/vouchers/"+uuid.NewString()+"/redeem", `{"pin": "482913"}`, "Idempotency-Key", "r4")
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
|
||||
status, body = call(http.MethodGet, "/customer/enakgame/redemptions", "")
|
||||
status, body = call(http.MethodGet, "/customer/vouchers/redemptions", "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 3, data(body)["pagination"].(map[string]any)["total_count"])
|
||||
}
|
||||
@@ -477,7 +477,7 @@ func TestEnakGameEventEndpoints_AgainstPostgres(t *testing.T) {
|
||||
assert.NotContains(t, events[0], "budget_id", "the budget stays internal")
|
||||
}
|
||||
|
||||
// EG-1001: the admin steps of docs/enakgame-spin.md make a spin wheel, and a customer
|
||||
// EG-1001: the admin steps of docs/integration-backoffice.md §8.4 make a spin wheel, and a customer
|
||||
// plays it through /customer/enakgame/sessions, over HTTP down to Postgres.
|
||||
func TestEnakGameSpin_AgainstPostgres(t *testing.T) {
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
|
||||
Reference in New Issue
Block a user