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>
370 lines
9.0 KiB
Go
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
|
|
}
|