Products like fish are sold per weighing (4.2 ons, 5.6 ons), which the order line could not represent: quantity is INTEGER and prices are always computed as quantity * unit_price. Model one weighing as one order line. quantity stays INTEGER and keeps meaning "how many items"; the measured amount goes into a new nullable order_items.weight, and the line is priced weight * unit_price. Two weighings of the same product are two lines, never merged into one. Keeping quantity integral avoids float comparisons in void, refund and split bill, where accumulated rounding error would silently misbehave — "1.4 + 1.4 + 1.4" is not 4.2 in float64, which would leave a fully paid split-bill item marked unpaid. BillableQuantity() is now the single place that decides between weight and count; every price and cost calculation goes through it. Missing one would bill a 4.2 ons fish as a single ons — wrong money, no error. Two database constraints back the design: a weighed line always carries a positive weight, and its quantity is pinned to 1. The latter also makes void all-or-nothing for weighed lines, so the row-splitting branch can never produce a zero-weight remainder row. Also wires product.unit_id through the API, which was previously not settable at all, and corrects the misleading comment on the request's unit_price field — that value has never been used; price always comes from the database. Design notes and the audit of every price multiplication site are in docs/rfc-weight-based-products.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
379 lines
11 KiB
Go
379 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,
|
|
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
|
|
}
|