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:
efrilm
2026-10-07 13:48:29 +07:00
co-authored by Claude Opus 5.5
parent 3ebc09f818
commit 2c9753fae7
87 changed files with 423 additions and 3071 deletions
@@ -1,34 +0,0 @@
package service
import (
"context"
"github.com/google/uuid"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/processor"
"apskel-pos-be/internal/transformer"
)
// CustomerOrderPaymentService lets customers pay their own orders with EnakPoint in the
// app or a self-order (docs/prd-point-coin.md F9).
type CustomerOrderPaymentService interface {
PayWithPoints(ctx context.Context, customerID, orderID uuid.UUID, req *contract.PayWithPointsRequest, info models.CustomerPinRequestInfo) *contract.Response
}
type CustomerOrderPaymentServiceImpl struct {
orders processor.OrderProcessor
}
func NewCustomerOrderPaymentService(orders processor.OrderProcessor) *CustomerOrderPaymentServiceImpl {
return &CustomerOrderPaymentServiceImpl{orders: orders}
}
func (s *CustomerOrderPaymentServiceImpl) PayWithPoints(ctx context.Context, customerID, orderID uuid.UUID, req *contract.PayWithPointsRequest, info models.CustomerPinRequestInfo) *contract.Response {
payment, err := s.orders.PayWithPointsInApp(ctx, customerID, orderID, req.Points, req.Pin, info)
if err != nil {
return PointPaymentErrorResponse(err)
}
return contract.BuildSuccessResponse(transformer.PaymentModelToContract(payment))
}
+3 -15
View File
@@ -25,18 +25,14 @@ type CustomerPinService interface {
RemovePin(ctx context.Context, apctx *appcontext.ContextInfo, customerID uuid.UUID, req *contract.RemoveCustomerPinRequest, info models.CustomerPinRequestInfo) *contract.Response
ListSecurityEvents(ctx context.Context, apctx *appcontext.ContextInfo, customerID uuid.UUID, page, limit int) *contract.Response
// IssuePaymentCode checks the PIN and returns a one-time code for the cashier (F9).
IssuePaymentCode(ctx context.Context, customerID uuid.UUID, req *contract.IssuePaymentCodeRequest, info models.CustomerPinRequestInfo) *contract.Response
}
type CustomerPinServiceImpl struct {
pins *processor.CustomerPinProcessor
codes *processor.PaymentCodeProcessor
pins *processor.CustomerPinProcessor
}
func NewCustomerPinService(pins *processor.CustomerPinProcessor, codes *processor.PaymentCodeProcessor) *CustomerPinServiceImpl {
return &CustomerPinServiceImpl{pins: pins, codes: codes}
func NewCustomerPinService(pins *processor.CustomerPinProcessor) *CustomerPinServiceImpl {
return &CustomerPinServiceImpl{pins: pins}
}
func (s *CustomerPinServiceImpl) Status(ctx context.Context, customerID uuid.UUID) *contract.Response {
@@ -130,11 +126,3 @@ func PinErrorResponse(err error) *contract.Response {
contract.NewResponseError(code, constants.CustomerPinServiceEntity, err.Error()),
})
}
func (s *CustomerPinServiceImpl) IssuePaymentCode(ctx context.Context, customerID uuid.UUID, req *contract.IssuePaymentCodeRequest, info models.CustomerPinRequestInfo) *contract.Response {
code, err := s.codes.Issue(ctx, customerID, req.Pin, info)
if err != nil {
return PinErrorResponse(err)
}
return contract.BuildSuccessResponse(code)
}
+1 -2
View File
@@ -557,8 +557,7 @@ func (s *OrderServiceImpl) validateCreatePaymentRequest(req *models.CreatePaymen
return fmt.Errorf("payment method ID is required")
}
// A payment with EnakPoint gives points instead; its amount is computed from them.
if req.Points == nil && req.Amount <= 0 {
if req.Amount <= 0 {
return fmt.Errorf("payment amount must be greater than zero")
}
@@ -42,14 +42,6 @@ func (m *MockOrderProcessor) UpdateOrder(ctx context.Context, id uuid.UUID, req
return args.Get(0).(*models.OrderResponse), args.Error(1)
}
func (m *MockOrderProcessor) PayWithPointsInApp(ctx context.Context, customerID, orderID uuid.UUID, points int64, pin string, info models.CustomerPinRequestInfo) (*models.PaymentResponse, error) {
args := m.Called(ctx, customerID, orderID, points, pin, info)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.PaymentResponse), args.Error(1)
}
func (m *MockOrderProcessor) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
+3 -14
View File
@@ -2,10 +2,8 @@ package service
import (
"context"
"errors"
"apskel-pos-be/internal/appcontext"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/processor"
@@ -41,7 +39,7 @@ func (s *PaymentMethodServiceImpl) CreatePaymentMethod(ctx context.Context, cont
response, err := s.paymentMethodProcessor.CreatePaymentMethod(ctx, modelReq)
if err != nil {
return contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(paymentMethodErrorCode(err, "PAYMENT_METHOD_CREATE_ERROR"), "payment_method", err.Error()),
contract.NewResponseError("PAYMENT_METHOD_CREATE_ERROR", "payment_method", err.Error()),
})
}
@@ -86,7 +84,7 @@ func (s *PaymentMethodServiceImpl) UpdatePaymentMethod(ctx context.Context, id u
response, err := s.paymentMethodProcessor.UpdatePaymentMethod(ctx, id, modelReq)
if err != nil {
return contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(paymentMethodErrorCode(err, "PAYMENT_METHOD_UPDATE_ERROR"), "payment_method", err.Error()),
contract.NewResponseError("PAYMENT_METHOD_UPDATE_ERROR", "payment_method", err.Error()),
})
}
@@ -99,7 +97,7 @@ func (s *PaymentMethodServiceImpl) DeletePaymentMethod(ctx context.Context, id u
err := s.paymentMethodProcessor.DeletePaymentMethod(ctx, id)
if err != nil {
return contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(paymentMethodErrorCode(err, "PAYMENT_METHOD_DELETE_ERROR"), "payment_method", err.Error()),
contract.NewResponseError("PAYMENT_METHOD_DELETE_ERROR", "payment_method", err.Error()),
})
}
@@ -125,12 +123,3 @@ func (s *PaymentMethodServiceImpl) GetActivePaymentMethodsByOrganization(ctx con
return contract.BuildSuccessResponse(contractResponses)
}
// paymentMethodErrorCode answers a request to create, delete or retype the EnakPoint
// method as a bad request instead of a server error.
func paymentMethodErrorCode(err error, fallback string) string {
if errors.Is(err, processor.ErrSystemPaymentMethod) {
return constants.ValidationErrorCode
}
return fallback
}
-55
View File
@@ -1,55 +0,0 @@
package service
import (
"context"
"errors"
"github.com/google/uuid"
"apskel-pos-be/internal/appcontext"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/processor"
"apskel-pos-be/internal/repository"
)
// PointPaymentService serves what the cashier needs before taking EnakPoint
// (docs/prd-point-coin.md F9).
type PointPaymentService interface {
Preview(ctx context.Context, apctx *appcontext.ContextInfo, orderID uuid.UUID) *contract.Response
}
type PointPaymentServiceImpl struct {
payments *processor.PointPaymentProcessor
}
func NewPointPaymentService(payments *processor.PointPaymentProcessor) *PointPaymentServiceImpl {
return &PointPaymentServiceImpl{payments: payments}
}
func (s *PointPaymentServiceImpl) Preview(ctx context.Context, apctx *appcontext.ContextInfo, orderID uuid.UUID) *contract.Response {
preview, err := s.payments.Preview(ctx, apctx.OrganizationID, orderID)
if err != nil {
return PointPaymentErrorResponse(err)
}
return contract.BuildSuccessResponse(preview)
}
// PointPaymentErrorResponse answers a refused EnakPoint payment as a bad request, PIN
// problems with their own codes, and anything else as a server error.
func PointPaymentErrorResponse(err error) *contract.Response {
var pinErr *processor.PinError
if errors.As(err, &pinErr) {
return PinErrorResponse(err)
}
code := constants.InternalServerErrorCode
switch {
case errors.Is(err, repository.ErrPointPaymentOrderNotFound):
code = constants.NotFoundErrorCode
case errors.Is(err, processor.ErrPointPaymentRejected):
code = constants.ValidationErrorCode
}
return contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(code, constants.WalletServiceEntity, err.Error()),
})
}