Files
apskel-pos-backend/internal/mappers/order_mapper.go
T
efrilmandClaude Opus 5.5 4b3beaed41 feat(loyalty): pay orders with EnakPoint at the cashier
Adds paying with the EnakPoint method (docs/prd-point-coin.md F9, K7,
PC-305).

POST /payments with the EnakPoint method now takes points and the
customer's payment code and goes through PointPaymentProcessor instead of
the generic path, which would record a payment without taking any balance.
After checking the order, its customer (not walk-in, active), the outlet
(accepts EnakPoint, minimum) and the method, it redeems the code, then in
one transaction locks the order row and the wallet, recomputes the F9
limits from fresh data, inserts the payment with points_used and the frozen
point_value, writes the PAYMENT ledger row (key payment:{id}, the outlet,
the cashier) and updates the order. The limits are
min(balance, floor(min(remaining, total × max_payment_percent / 100 − paid
with EnakPoint) / point_value)) in cents, so EnakPoint never pays more than
what is left and gives no change.

Unlike the generic CreatePayment, which always marks the order paid, an
EnakPoint payment leaves it partial with the right remaining amount until
it is settled, so the rest can be paid in cash. Settling it triggers
earning, whose basis leaves out the EnakPoint part. Splitting with the
EnakPoint method is refused. Refusals answer 400. The payment response
carries points_used and point_value for the receipt.

GET /orders/:id/point-payment/preview returns eligibility, balance, point
value and the maximum for the use-maximum button.

The payment and order repositories write outside transactions, so this
path uses its own repository that joins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:46:31 +07:00

381 lines
11 KiB
Go

package mappers
import (
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
if order == nil {
return nil
}
response := &models.OrderResponse{
ID: order.ID,
OrganizationID: order.OrganizationID,
OutletID: order.OutletID,
UserID: order.UserID,
CustomerID: order.CustomerID,
OrderNumber: order.OrderNumber,
TableNumber: order.TableNumber,
OrderType: constants.OrderType(order.OrderType),
Status: constants.OrderStatus(order.Status),
Subtotal: order.Subtotal,
TaxAmount: order.TaxAmount,
DiscountAmount: order.DiscountAmount,
TotalAmount: order.TotalAmount,
TotalCost: order.TotalCost,
RemainingAmount: order.RemainingAmount,
PaymentStatus: constants.PaymentStatus(order.PaymentStatus),
RefundAmount: order.RefundAmount,
IsVoid: order.IsVoid,
IsRefund: order.IsRefund,
VoidReason: order.VoidReason,
VoidedAt: order.VoidedAt,
VoidedBy: order.VoidedBy,
RefundReason: order.RefundReason,
RefundedAt: order.RefundedAt,
RefundedBy: order.RefundedBy,
Metadata: map[string]interface{}(order.Metadata),
CreatedAt: order.CreatedAt,
UpdatedAt: order.UpdatedAt,
}
// Calculate payment summary and determine split type
var totalPaid float64
var paymentCount int
var splitType *string
if order.Payments != nil {
paymentCount = len(order.Payments)
for _, payment := range order.Payments {
if payment.Status == entities.PaymentTransactionStatusCompleted {
totalPaid += payment.Amount
}
// Determine split type from the first split payment
if splitType == nil && payment.SplitType != nil && payment.SplitTotal > 1 {
st := string(*payment.SplitType)
splitType = &st
}
}
}
response.TotalPaid = totalPaid
response.PaymentCount = paymentCount
response.SplitType = splitType
// Map order items
if order.OrderItems != nil {
response.OrderItems = make([]models.OrderItemResponse, len(order.OrderItems))
// Build map of paid quantities per order item from payments
paidQtyByOrderItem := map[uuid.UUID]int{}
if order.Payments != nil {
for _, p := range order.Payments {
for _, poi := range p.PaymentOrderItems {
paidQtyByOrderItem[poi.OrderItemID] += poi.Quantity
}
}
}
for i, item := range order.OrderItems {
resp := OrderItemEntityToResponse(&item, order.OutletID)
if resp != nil {
resp.PaidQuantity = paidQtyByOrderItem[item.ID]
response.OrderItems[i] = *resp
}
}
}
// Map payments
if order.Payments != nil {
response.Payments = make([]models.PaymentResponse, len(order.Payments))
for i, payment := range order.Payments {
response.Payments[i] = *PaymentEntityToResponse(&payment)
}
}
return response
}
func OrderItemEntityToResponse(item *entities.OrderItem, outletID uuid.UUID) *models.OrderItemResponse {
if item == nil {
return nil
}
// Resolve print_to_checker from preloaded outlet prices
printToChecker := true // default
for _, op := range item.Product.ProductOutletPrices {
if op.OutletID == outletID {
printToChecker = op.PrintToChecker
break
}
}
// Unit is only meaningful for weighed lines, but carry it whenever it was captured.
var unitName, unitAbbreviation *string
if item.Unit != nil {
unitName = &item.Unit.Name
unitAbbreviation = item.Unit.Abbreviation
}
response := &models.OrderItemResponse{
ID: item.ID,
OrderID: item.OrderID,
ProductID: item.ProductID,
ProductVariantID: item.ProductVariantID,
Quantity: item.Quantity,
Weight: item.Weight,
UnitID: item.UnitID,
UnitName: unitName,
UnitAbbreviation: unitAbbreviation,
UnitPrice: item.UnitPrice,
TotalPrice: item.TotalPrice,
UnitCost: item.UnitCost,
TotalCost: item.TotalCost,
RefundAmount: item.RefundAmount,
RefundQuantity: item.RefundQuantity,
IsPartiallyRefunded: item.IsPartiallyRefunded,
IsFullyRefunded: item.IsFullyRefunded,
RefundReason: item.RefundReason,
RefundedAt: item.RefundedAt,
RefundedBy: item.RefundedBy,
Modifiers: []map[string]interface{}(item.Modifiers),
Notes: item.Notes,
Metadata: map[string]interface{}(item.Metadata),
Status: constants.OrderItemStatus(item.Status),
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
PrinterType: item.Product.PrinterType,
PrintToChecker: printToChecker,
}
if item.Product.ID != uuid.Nil {
response.ProductName = item.Product.Name
if item.Product.CategoryID != uuid.Nil {
categoryID := item.Product.CategoryID
response.CategoryID = &categoryID
}
if item.Product.Category.ID != uuid.Nil {
categoryName := item.Product.Category.Name
response.CategoryName = &categoryName
}
}
if item.ProductVariant != nil {
response.ProductVariantName = &item.ProductVariant.Name
}
return response
}
func PaymentEntityToResponse(payment *entities.Payment) *models.PaymentResponse {
if payment == nil {
return nil
}
response := &models.PaymentResponse{
ID: payment.ID,
OrderID: payment.OrderID,
PaymentMethodID: payment.PaymentMethodID,
Amount: payment.Amount,
Status: constants.PaymentTransactionStatus(payment.Status),
TransactionID: payment.TransactionID,
SplitNumber: payment.SplitNumber,
SplitTotal: payment.SplitTotal,
SplitType: (*string)(payment.SplitType),
SplitDescription: payment.SplitDescription,
RefundAmount: payment.RefundAmount,
PointsUsed: payment.PointsUsed,
PointValue: payment.PointValue,
RefundReason: payment.RefundReason,
RefundedAt: payment.RefundedAt,
RefundedBy: payment.RefundedBy,
Metadata: map[string]interface{}(payment.Metadata),
CreatedAt: payment.CreatedAt,
UpdatedAt: payment.UpdatedAt,
}
// Add payment method information if available
if payment.PaymentMethod.ID != uuid.Nil {
response.PaymentMethodName = payment.PaymentMethod.Name
response.PaymentMethodType = constants.PaymentMethodType(payment.PaymentMethod.Type)
}
// Map payment order items
if payment.PaymentOrderItems != nil {
response.PaymentOrderItems = make([]models.PaymentOrderItemResponse, len(payment.PaymentOrderItems))
for i, item := range payment.PaymentOrderItems {
response.PaymentOrderItems[i] = *PaymentOrderItemEntityToResponse(&item)
}
}
return response
}
func PaymentOrderItemEntityToResponse(item *entities.PaymentOrderItem) *models.PaymentOrderItemResponse {
if item == nil {
return nil
}
return &models.PaymentOrderItemResponse{
ID: item.ID,
PaymentID: item.PaymentID,
OrderItemID: item.OrderItemID,
Amount: item.Amount,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
}
}
// Request to Entity mappers
func CreateOrderRequestToEntity(req *models.CreateOrderRequest, organizationID uuid.UUID) *entities.Order {
if req == nil {
return nil
}
order := &entities.Order{
OrganizationID: organizationID,
OutletID: req.OutletID,
UserID: req.UserID,
CustomerID: req.CustomerID,
TableNumber: req.TableNumber,
OrderType: entities.OrderType(req.OrderType),
Status: entities.OrderStatusPending,
PaymentStatus: entities.PaymentStatusPending,
IsVoid: false,
IsRefund: false,
Metadata: entities.Metadata(req.Metadata),
}
return order
}
func CreateOrderItemRequestToEntity(req *models.CreateOrderItemRequest) *entities.OrderItem {
if req == nil {
return nil
}
// UnitPrice will be set from database product price, not from request
return &entities.OrderItem{
ProductID: req.ProductID,
ProductVariantID: req.ProductVariantID,
Quantity: req.Quantity,
UnitPrice: 0, // Will be set from database product price
Modifiers: entities.Modifiers(req.Modifiers),
Notes: req.Notes,
Metadata: entities.Metadata(req.Metadata),
Status: entities.OrderItemStatusPending,
}
}
func CreatePaymentRequestToEntity(req *models.CreatePaymentRequest) *entities.Payment {
if req == nil {
return nil
}
var splitType *entities.SplitType
if req.SplitType != nil {
st := entities.SplitType(*req.SplitType)
splitType = &st
}
payment := &entities.Payment{
OrderID: req.OrderID,
PaymentMethodID: req.PaymentMethodID,
Amount: req.Amount,
Status: entities.PaymentTransactionStatusPending,
TransactionID: req.TransactionID,
SplitNumber: req.SplitNumber,
SplitTotal: req.SplitTotal,
SplitType: splitType,
SplitDescription: req.SplitDescription,
Metadata: entities.Metadata(req.Metadata),
}
return payment
}
func CreatePaymentOrderItemRequestToEntity(req *models.CreatePaymentOrderItemRequest) *entities.PaymentOrderItem {
if req == nil {
return nil
}
return &entities.PaymentOrderItem{
OrderItemID: req.OrderItemID,
Amount: req.Amount,
}
}
// Update request to entity updates
func UpdateOrderRequestToEntityUpdates(req *models.UpdateOrderRequest) map[string]interface{} {
updates := make(map[string]interface{})
if req.TableNumber != nil {
updates["table_number"] = *req.TableNumber
}
if req.Status != nil {
updates["status"] = entities.OrderStatus(*req.Status)
}
if req.DiscountAmount != nil {
updates["discount_amount"] = *req.DiscountAmount
}
if req.Metadata != nil {
updates["metadata"] = entities.Metadata(req.Metadata)
}
return updates
}
// Helper functions for list responses
func OrderEntitiesToResponses(orders []*entities.Order) []models.OrderResponse {
if orders == nil {
return nil
}
responses := make([]models.OrderResponse, len(orders))
for i, order := range orders {
response := OrderEntityToResponse(order)
if response != nil {
responses[i] = *response
}
}
return responses
}
func OrderItemEntitiesToResponses(items []*entities.OrderItem, outletID uuid.UUID) []models.OrderItemResponse {
if items == nil {
return nil
}
responses := make([]models.OrderItemResponse, len(items))
for i, item := range items {
response := OrderItemEntityToResponse(item, outletID)
if response != nil {
responses[i] = *response
}
}
return responses
}
func PaymentEntitiesToResponses(payments []*entities.Payment) []models.PaymentResponse {
if payments == nil {
return nil
}
responses := make([]models.PaymentResponse, len(payments))
for i, payment := range payments {
response := PaymentEntityToResponse(payment)
if response != nil {
responses[i] = *response
}
}
return responses
}