feat(loyalty): refund EnakPoint payments as EnakPoint only

Adds refunds of EnakPoint payments (docs/prd-point-coin.md F9, K7, Q13,
PC-307).

After a void or refund, onOrderRefunded now returns EnakPoint before taking
earning back. For each EnakPoint payment of the order it returns everything
on a void, and floor(refunded rupiah / the frozen point_value) when the
payment itself was refunded, so a later change of the point value does not
change how many come back and a remainder below one EnakPoint is lost. It
never returns more than the payment used, and only what has not come back
yet, so repeating is safe. PAYMENT_REFUND rows point at the PAYMENT they
reverse, and the EnakPoint go back into lots with the expiry of the lots
they were taken from, longest-lasting first (the 7-day extension waits on
note N4).

RefundOrder, which hands money back in cash or another method, is now
limited to what was paid with other methods; the EnakPoint part has to be
refunded through its own payment. That answers 400.

Fixes earning reversal from PC-204: a refund of the EnakPoint part raised
orders.refund_amount and so took earning back, although that part never
earned. It is now left out of the refund the reversal uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-09-30 11:55:30 +07:00
co-authored by Claude Opus 5.5
parent 43eac0ced4
commit d7138b8f87
6 changed files with 512 additions and 4 deletions
+5
View File
@@ -205,6 +205,11 @@ 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
}