feat(loyalty): remove paying with EnakPoint
EnakPoint can only be redeemed for vouchers now: it can no longer pay for orders and is never cashed out (docs/enakgame-prd.md §3.2, EG-001, EG-002). No order was ever paid with EnakPoint, so there is no data to move. Removed: - POST /customer/wallet/payment-code, POST /customer/orders/:id/pay-with-points and GET /orders/:id/point-payment/preview, with their processors, repositories, services, handlers and tests. - The point payment method type: paying, splitting and refunding with it, the outlet filter on the method list, and the system-method guard. - points and payment_code on CreatePayment; points_used and point_value on payments; accepts_point_payment on the customer outlets. - The outlet point_payment settings. A PUT that still sends them is rejected as an unknown field. - The EnakPoint split in the payment method analytics. - PAYMENT and PAYMENT_REFUND from the wallet type rules. Tests that used them as a generic EnakPoint debit use REWARD_REDEEM. - The EnakPoint-paid part from the earning basis, which is subtotal − discount again. Migration 000102 drops the trigger, the point methods and their index, the payments columns, and the outlet settings, and restores the method type CHECK without point. payments.payment_method_id is ON DELETE RESTRICT, so it fails rather than lose a payment made with EnakPoint. The integration docs list the removed endpoints and fields, and the EnakPoint & EnakCoin PRD and tasks note what is superseded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3ebc09f818
commit
2c9753fae7
@@ -1,35 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
)
|
||||
|
||||
// CustomerOrderPaymentHandler serves POST /customer/orders/:id/pay-with-points
|
||||
// (docs/prd-point-coin.md F9). The body holds the PIN, so it is never logged.
|
||||
type CustomerOrderPaymentHandler struct {
|
||||
payments service.CustomerOrderPaymentService
|
||||
}
|
||||
|
||||
func NewCustomerOrderPaymentHandler(payments service.CustomerOrderPaymentService) *CustomerOrderPaymentHandler {
|
||||
return &CustomerOrderPaymentHandler{payments: payments}
|
||||
}
|
||||
|
||||
func (h *CustomerOrderPaymentHandler) PayWithPoints(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerOrderPaymentHandler::PayWithPoints")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
orderID, ok := parseUUIDParam(c, "id", "CustomerOrderPaymentHandler::PayWithPoints")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.PayWithPointsRequest
|
||||
if !bindPinRequest(c, &req, "CustomerOrderPaymentHandler::PayWithPoints") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.payments.PayWithPoints(c.Request.Context(), customerID, orderID, &req, pinRequestInfo(c)), "CustomerOrderPaymentHandler::PayWithPoints")
|
||||
}
|
||||
@@ -136,16 +136,3 @@ func customerIDFromGin(c *gin.Context, method string) (uuid.UUID, bool) {
|
||||
func pinRequestInfo(c *gin.Context) models.CustomerPinRequestInfo {
|
||||
return models.CustomerPinRequestInfo{IPAddress: c.ClientIP(), UserAgent: c.Request.UserAgent()}
|
||||
}
|
||||
|
||||
// IssuePaymentCode is POST /customer/wallet/payment-code.
|
||||
func (h *CustomerPinHandler) IssuePaymentCode(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerPinHandler::IssuePaymentCode")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.IssuePaymentCodeRequest
|
||||
if !bindPinRequest(c, &req, "CustomerPinHandler::IssuePaymentCode") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.pinService.IssuePaymentCode(c.Request.Context(), customerID, &req, pinRequestInfo(c)), "CustomerPinHandler::IssuePaymentCode")
|
||||
}
|
||||
|
||||
@@ -82,9 +82,9 @@ func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
||||
return err
|
||||
}
|
||||
if _, err := wallet.Debit(ctx, processor.WalletDebitInput{WalletEntry: processor.WalletEntry{
|
||||
CustomerID: customer, Currency: constants.WalletCurrencyPoint, Type: constants.WalletTxTypePayment,
|
||||
Amount: 50, ReferenceType: constants.WalletRefTypePayment, ReferenceID: payment, OutletID: &outlet,
|
||||
Description: "Bayar #ORD-2"}}); err != nil {
|
||||
CustomerID: customer, Currency: constants.WalletCurrencyPoint, Type: constants.WalletTxTypeRewardRedeem,
|
||||
Amount: 50, ReferenceType: constants.WalletRefTypeRewardRedemption, ReferenceID: payment, OutletID: &outlet,
|
||||
Description: "Tukar voucher"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := wallet.Credit(ctx, processor.WalletCreditInput{
|
||||
@@ -162,7 +162,7 @@ func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
||||
assert.Contains(t, tx, "source", tx["type"])
|
||||
assert.NotContains(t, tx, "destination")
|
||||
} else {
|
||||
assert.Equal(t, map[string]any{"type": "PAYMENT", "id": payment.String()}, tx["destination"])
|
||||
assert.Equal(t, map[string]any{"type": "REWARD_REDEMPTION", "id": payment.String()}, tx["destination"])
|
||||
assert.NotContains(t, tx, "source")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func TestOutletLoyaltySettingsEndpoints_AgainstPostgres(t *testing.T) {
|
||||
got := data(body)
|
||||
assert.Equal(t, map[string]any{"enabled": false, "earn_mode": "PER_AMOUNT", "earn_per_amount": float64(100), "earn_value": float64(1), "earn_percent": float64(1), "min_order_amount": float64(0), "max_per_order": nil}, got["point"])
|
||||
assert.Equal(t, map[string]any{"enabled": false, "earn_mode": "PER_AMOUNT", "earn_per_amount": float64(25000), "earn_value": float64(1), "earn_percent": float64(1), "min_order_amount": float64(0), "max_per_order": nil}, got["coin"])
|
||||
assert.Equal(t, map[string]any{"accept_payment": false, "min_payment_points": float64(1), "max_payment_percent": float64(100)}, got["point_payment"])
|
||||
assert.NotContains(t, got, "point_payment", "EnakPoint cannot pay (docs/enakgame-prd.md §3.2)")
|
||||
assert.EqualValues(t, 1, got["point_value"])
|
||||
assert.EqualValues(t, 1, got["point_cashback_percent"])
|
||||
|
||||
@@ -133,16 +133,16 @@ func TestOutletLoyaltySettingsEndpoints_AgainstPostgres(t *testing.T) {
|
||||
|
||||
// Values out of bounds, unknown fields and bad JSON are refused and change nothing.
|
||||
for name, bad := range map[string]string{
|
||||
"earn_per_amount 0": `{"point": {"earn_per_amount": 0}}`,
|
||||
"negative earn_value": `{"coin": {"earn_value": -1}}`,
|
||||
"earn mode unknown": `{"point": {"earn_mode": "PERCENT"}}`,
|
||||
"earn_percent over 100": `{"coin": {"earn_percent": 101}}`,
|
||||
"negative min_order": `{"point": {"min_order_amount": -5}}`,
|
||||
"negative max_per_order": `{"point": {"max_per_order": -1}}`,
|
||||
"payment percent over 100": `{"point_payment": {"max_payment_percent": 101}}`,
|
||||
"unknown field": `{"point": {"earn_per_amout": 50}}`,
|
||||
"wrong type": `{"point": {"enabled": "yes"}}`,
|
||||
"not json": `enabled=true`,
|
||||
"earn_per_amount 0": `{"point": {"earn_per_amount": 0}}`,
|
||||
"negative earn_value": `{"coin": {"earn_value": -1}}`,
|
||||
"earn mode unknown": `{"point": {"earn_mode": "PERCENT"}}`,
|
||||
"earn_percent over 100": `{"coin": {"earn_percent": 101}}`,
|
||||
"negative min_order": `{"point": {"min_order_amount": -5}}`,
|
||||
"negative max_per_order": `{"point": {"max_per_order": -1}}`,
|
||||
"point_payment removed": `{"point_payment": {"accept_payment": true}}`,
|
||||
"unknown field": `{"point": {"earn_per_amout": 50}}`,
|
||||
"wrong type": `{"point": {"enabled": "yes"}}`,
|
||||
"not json": `enabled=true`,
|
||||
} {
|
||||
status, _ = call(http.MethodPut, "/manager"+path, bad)
|
||||
assert.Equal(t, http.StatusBadRequest, status, name)
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/transformer"
|
||||
"apskel-pos-be/internal/util"
|
||||
@@ -205,11 +202,6 @@ func (h *OrderHandler) RefundOrder(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.orderService.RefundOrder(ctx, id, modelReq, userID); err != nil {
|
||||
// Refusing to hand EnakPoint back as cash is a bad request, not a server fault.
|
||||
if errors.Is(err, processor.ErrPointPaymentRejected) {
|
||||
util.HandleResponse(c.Writer, c.Request, service.PointPaymentErrorResponse(err), "OrderHandler::RefundOrder")
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::RefundOrder", err.Error())}), "OrderHandler::RefundOrder")
|
||||
return
|
||||
}
|
||||
@@ -233,11 +225,6 @@ func (h *OrderHandler) CreatePayment(c *gin.Context) {
|
||||
|
||||
response, err := h.orderService.CreatePayment(c.Request.Context(), modelReq)
|
||||
if err != nil {
|
||||
// A refused EnakPoint payment is the cashier's or customer's to fix, not a server fault.
|
||||
if errors.Is(err, processor.ErrPointPaymentRejected) {
|
||||
util.HandleResponse(c.Writer, c.Request, service.PointPaymentErrorResponse(err), "OrderHandler::CreatePayment")
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::CreatePayment", err.Error())}), "OrderHandler::CreatePayment")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -113,16 +113,6 @@ func (h *PaymentMethodHandler) ListPaymentMethods(c *gin.Context) {
|
||||
|
||||
req.OrganizationID = &contextInfo.OrganizationID
|
||||
|
||||
// At the cashier, EnakPoint is listed only where the outlet accepts it (F9).
|
||||
if outletStr := c.Query("outlet_id"); outletStr != "" {
|
||||
if outletID, err := uuid.Parse(outletStr); err == nil {
|
||||
req.OutletID = &outletID
|
||||
}
|
||||
} else if contextInfo.OutletID != uuid.Nil {
|
||||
outletID := contextInfo.OutletID
|
||||
req.OutletID = &outletID
|
||||
}
|
||||
|
||||
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
||||
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||
req.IsActive = &isActive
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
)
|
||||
|
||||
// PointPaymentHandler serves GET /orders/:id/point-payment/preview
|
||||
// (docs/prd-point-coin.md F9).
|
||||
type PointPaymentHandler struct {
|
||||
pointPaymentService service.PointPaymentService
|
||||
}
|
||||
|
||||
func NewPointPaymentHandler(pointPaymentService service.PointPaymentService) *PointPaymentHandler {
|
||||
return &PointPaymentHandler{pointPaymentService: pointPaymentService}
|
||||
}
|
||||
|
||||
func (h *PointPaymentHandler) Preview(c *gin.Context) {
|
||||
orderID, ok := parseUUIDParam(c, "id", "PointPaymentHandler::Preview")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
util.HandleResponse(c.Writer, c.Request, h.pointPaymentService.Preview(ctx, appcontext.FromGinContext(ctx), orderID), "PointPaymentHandler::Preview")
|
||||
}
|
||||
Reference in New Issue
Block a user