Files
apskel-pos-backend/internal/mappers/product_mapper.go
efrilmandClaude Opus 5 ebf666c004 feat(product): require a unit for weight-based products
A product sold by weight with no unit produces order lines with nothing to
print: the receipt would read "4,2" with no idea of what. Until now nothing
stopped that — the mistake only surfaced at the cashier.

Enforce it in two places, because neither alone sees the whole picture. On
create, the validator has everything it needs. On update, the request may
omit unit_id for a product that already has one, so the check runs in the
processor against the merged product: what is rejected is the end state, a
product sold by weight with no unit.

Also fixes two things this uncovered:

The struct tags on the product contracts are decorative — this validator is
hand-written and never calls validator.Struct — so `oneof=unit weight` was
never enforced, and an unknown sell_by was silently rewritten to "unit" by
the mapper. It is now rejected with a message that names the valid values.

The update validator's "at least one field" guard did not list unit_id,
sell_by or print_to_checker, so an update carrying only one of those was
turned away as an empty request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 17:29:34 +07:00

370 lines
9.0 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 ProductEntityToModel(entity *entities.Product) *models.Product {
if entity == nil {
return nil
}
return &models.Product{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
CategoryID: entity.CategoryID,
SKU: entity.SKU,
Name: entity.Name,
Description: entity.Description,
Price: entity.Price,
Cost: entity.Cost,
BusinessType: constants.BusinessType(entity.BusinessType),
ImageURL: entity.ImageURL,
PrinterType: entity.PrinterType,
UnitID: entity.UnitID,
SellBy: entity.SellBy,
HasIngredients: entity.HasIngredients,
Metadata: map[string]interface{}(entity.Metadata),
IsActive: entity.IsActive,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
}
func ProductModelToEntity(model *models.Product) *entities.Product {
if model == nil {
return nil
}
return &entities.Product{
ID: model.ID,
OrganizationID: model.OrganizationID,
CategoryID: model.CategoryID,
SKU: model.SKU,
Name: model.Name,
Description: model.Description,
Price: model.Price,
Cost: model.Cost,
BusinessType: string(model.BusinessType),
ImageURL: model.ImageURL,
PrinterType: model.PrinterType,
UnitID: model.UnitID,
SellBy: model.SellBy,
HasIngredients: model.HasIngredients,
Metadata: entities.Metadata(model.Metadata),
IsActive: model.IsActive,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
func CreateProductRequestToEntity(req *models.CreateProductRequest) *entities.Product {
if req == nil {
return nil
}
cost := float64(0)
if req.Cost > 0 {
cost = req.Cost
}
businessType := "restaurant"
if req.BusinessType != "" {
businessType = string(req.BusinessType)
}
printerType := "kitchen"
if req.PrinterType != nil && *req.PrinterType != "" {
printerType = *req.PrinterType
}
sellBy := constants.SellByUnit
if constants.IsValidSellBy(req.SellBy) {
sellBy = req.SellBy
}
metadata := entities.Metadata{}
if req.Metadata != nil {
metadata = entities.Metadata(req.Metadata)
}
return &entities.Product{
OrganizationID: req.OrganizationID,
CategoryID: req.CategoryID,
SKU: req.SKU,
Name: req.Name,
Description: req.Description,
Price: req.Price,
Cost: cost,
BusinessType: businessType,
ImageURL: req.ImageURL,
PrinterType: printerType,
UnitID: req.UnitID,
SellBy: sellBy,
Metadata: metadata,
IsActive: true, // Default to active
}
}
func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse {
if entity == nil {
return nil
}
// Convert variants to response models
var variantResponses []models.ProductVariantResponse
if entity.ProductVariants != nil {
variantResponses = make([]models.ProductVariantResponse, len(entity.ProductVariants))
for i, variant := range entity.ProductVariants {
variantResponses[i] = models.ProductVariantResponse{
ID: variant.ID,
ProductID: variant.ProductID,
Name: variant.Name,
PriceModifier: variant.PriceModifier,
Cost: variant.Cost,
Metadata: map[string]interface{}(variant.Metadata),
CreatedAt: variant.CreatedAt,
UpdatedAt: variant.UpdatedAt,
}
}
}
// Get category name from the Category relation
categoryName := ""
if entity.Category.ID != uuid.Nil {
categoryName = entity.Category.Name
}
return &models.ProductResponse{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
CategoryID: entity.CategoryID,
CategoryName: categoryName,
SKU: entity.SKU,
Name: entity.Name,
Description: entity.Description,
Price: entity.Price,
OutletPrice: nil, // populated by processor when outletID is available
Cost: entity.Cost,
BusinessType: constants.BusinessType(entity.BusinessType),
ImageURL: entity.ImageURL,
PrinterType: entity.PrinterType,
UnitID: entity.UnitID,
SellBy: entity.SellBy,
Metadata: map[string]interface{}(entity.Metadata),
IsActive: entity.IsActive,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
Variants: variantResponses,
}
}
func UpdateProductEntityFromRequest(entity *entities.Product, req *models.UpdateProductRequest) {
if entity == nil || req == nil {
return
}
if req.CategoryID != nil {
entity.CategoryID = *req.CategoryID
}
if req.SKU != nil {
entity.SKU = req.SKU
}
if req.Name != nil {
entity.Name = *req.Name
}
if req.Description != nil {
entity.Description = req.Description
}
if req.Price != nil {
entity.Price = *req.Price
}
if req.Cost != nil {
entity.Cost = *req.Cost
}
if req.ImageURL != nil {
entity.ImageURL = req.ImageURL
}
if req.PrinterType != nil {
entity.PrinterType = *req.PrinterType
}
if req.UnitID != nil {
entity.UnitID = req.UnitID
}
if req.SellBy != nil && constants.IsValidSellBy(*req.SellBy) {
entity.SellBy = *req.SellBy
}
if req.Metadata != nil {
if entity.Metadata == nil {
entity.Metadata = make(entities.Metadata)
}
for k, v := range req.Metadata {
entity.Metadata[k] = v
}
}
if req.IsActive != nil {
entity.IsActive = *req.IsActive
}
}
func ProductEntitiesToModels(entities []*entities.Product) []*models.Product {
if entities == nil {
return nil
}
models := make([]*models.Product, len(entities))
for i, entity := range entities {
models[i] = ProductEntityToModel(entity)
}
return models
}
func ProductEntitiesToResponses(entities []*entities.Product) []*models.ProductResponse {
if entities == nil {
return nil
}
responses := make([]*models.ProductResponse, len(entities))
for i, entity := range entities {
responses[i] = ProductEntityToResponse(entity)
}
return responses
}
// Product Variant Mappers
func ProductVariantEntityToModel(entity *entities.ProductVariant) *models.ProductVariant {
if entity == nil {
return nil
}
return &models.ProductVariant{
ID: entity.ID,
ProductID: entity.ProductID,
Name: entity.Name,
PriceModifier: entity.PriceModifier,
Cost: entity.Cost,
Metadata: map[string]interface{}(entity.Metadata),
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
}
func ProductVariantModelToEntity(model *models.ProductVariant) *entities.ProductVariant {
if model == nil {
return nil
}
return &entities.ProductVariant{
ID: model.ID,
ProductID: model.ProductID,
Name: model.Name,
PriceModifier: model.PriceModifier,
Cost: model.Cost,
Metadata: entities.Metadata(model.Metadata),
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
func CreateProductVariantRequestToEntity(req *models.CreateProductVariantRequest) *entities.ProductVariant {
if req == nil {
return nil
}
metadata := entities.Metadata{}
if req.Metadata != nil {
metadata = entities.Metadata(req.Metadata)
}
return &entities.ProductVariant{
ProductID: req.ProductID,
Name: req.Name,
PriceModifier: req.PriceModifier,
Cost: req.Cost,
Metadata: metadata,
}
}
func ProductVariantEntityToResponse(entity *entities.ProductVariant) *models.ProductVariantResponse {
if entity == nil {
return nil
}
return &models.ProductVariantResponse{
ID: entity.ID,
ProductID: entity.ProductID,
Name: entity.Name,
PriceModifier: entity.PriceModifier,
Cost: entity.Cost,
Metadata: map[string]interface{}(entity.Metadata),
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
}
func UpdateProductVariantEntityFromRequest(entity *entities.ProductVariant, req *models.UpdateProductVariantRequest) {
if entity == nil || req == nil {
return
}
if req.Name != nil {
entity.Name = *req.Name
}
if req.PriceModifier != nil {
entity.PriceModifier = *req.PriceModifier
}
if req.Cost != nil {
entity.Cost = *req.Cost
}
if req.Metadata != nil {
if entity.Metadata == nil {
entity.Metadata = make(entities.Metadata)
}
for k, v := range req.Metadata {
entity.Metadata[k] = v
}
}
}
func ProductVariantEntitiesToModels(entities []*entities.ProductVariant) []*models.ProductVariant {
if entities == nil {
return nil
}
models := make([]*models.ProductVariant, len(entities))
for i, entity := range entities {
models[i] = ProductVariantEntityToModel(entity)
}
return models
}
func ProductVariantEntitiesToResponses(entities []*entities.ProductVariant) []*models.ProductVariantResponse {
if entities == nil {
return nil
}
responses := make([]*models.ProductVariantResponse, len(entities))
for i, entity := range entities {
responses[i] = ProductVariantEntityToResponse(entity)
}
return responses
}