Merge pull request 'feat: add printer types' (#40) from fix/point-payment-method-types into main
Reviewed-on: #40
This commit was merged in pull request #40.
This commit is contained in:
@@ -121,7 +121,7 @@ type OrderItemResponse struct {
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PrinterType string `json:"printer_type"`
|
||||
PrinterTypes []string `json:"printer_types"`
|
||||
PrintToChecker bool `json:"print_to_checker"`
|
||||
PaidQuantity int `json:"paid_quantity"`
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ type CreateProductRequest struct {
|
||||
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
||||
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
||||
PrinterTypes []string `json:"printer_types,omitempty" validate:"omitempty,dive,max=50"`
|
||||
PrintToChecker *bool `json:"print_to_checker,omitempty"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
SellBy *string `json:"sell_by,omitempty" validate:"omitempty,oneof=unit weight"`
|
||||
@@ -38,7 +38,7 @@ type UpdateProductRequest struct {
|
||||
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
||||
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
||||
PrinterTypes []string `json:"printer_types,omitempty" validate:"omitempty,dive,max=50"` // Replaces the whole list when sent
|
||||
PrintToChecker *bool `json:"print_to_checker,omitempty"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
SellBy *string `json:"sell_by,omitempty" validate:"omitempty,oneof=unit weight"`
|
||||
@@ -76,7 +76,7 @@ type ProductResponse struct {
|
||||
Cost float64 `json:"cost"`
|
||||
BusinessType string `json:"business_type"`
|
||||
ImageURL *string `json:"image_url"`
|
||||
PrinterType string `json:"printer_type"`
|
||||
PrinterTypes []string `json:"printer_types"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
SellBy string `json:"sell_by"`
|
||||
PrintToChecker bool `json:"print_to_checker"`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -8,24 +9,26 @@ import (
|
||||
)
|
||||
|
||||
type Product struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
||||
CategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"category_id" validate:"required"`
|
||||
SKU *string `gorm:"size:100;index" json:"sku"`
|
||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
Price float64 `gorm:"type:decimal(10,2);not null" json:"price" validate:"required,min=0"`
|
||||
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost" validate:"min=0"`
|
||||
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
|
||||
ImageURL *string `gorm:"size:500" json:"image_url"`
|
||||
PrinterType string `gorm:"size:50;default:'kitchen'" json:"printer_type"`
|
||||
UnitID *uuid.UUID `gorm:"type:uuid;index" json:"unit_id"`
|
||||
SellBy string `gorm:"size:20;default:'unit'" json:"sell_by"`
|
||||
HasIngredients bool `gorm:"default:false" json:"has_ingredients"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
||||
CategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"category_id" validate:"required"`
|
||||
SKU *string `gorm:"size:100;index" json:"sku"`
|
||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
Price float64 `gorm:"type:decimal(10,2);not null" json:"price" validate:"required,min=0"`
|
||||
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost" validate:"min=0"`
|
||||
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
|
||||
ImageURL *string `gorm:"size:500" json:"image_url"`
|
||||
// PrinterTypes are the stations the product is printed at. Set them through
|
||||
// SetPrinterTypes.
|
||||
PrinterTypes StringSlice `gorm:"type:jsonb;not null;default:'[\"kitchen\"]'" json:"printer_types"`
|
||||
UnitID *uuid.UUID `gorm:"type:uuid;index" json:"unit_id"`
|
||||
SellBy string `gorm:"size:20;default:'unit'" json:"sell_by"`
|
||||
HasIngredients bool `gorm:"default:false" json:"has_ingredients"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
@@ -48,6 +51,32 @@ func (Product) TableName() string {
|
||||
return "products"
|
||||
}
|
||||
|
||||
// SetPrinterTypes records the stations the product is printed at, in order, dropping
|
||||
// blanks and repeats.
|
||||
func (p *Product) SetPrinterTypes(printerTypes []string) {
|
||||
cleaned := StringSlice{}
|
||||
seen := make(map[string]bool, len(printerTypes))
|
||||
for _, printerType := range printerTypes {
|
||||
printerType = strings.TrimSpace(printerType)
|
||||
if printerType == "" || seen[printerType] {
|
||||
continue
|
||||
}
|
||||
seen[printerType] = true
|
||||
cleaned = append(cleaned, printerType)
|
||||
}
|
||||
|
||||
p.PrinterTypes = cleaned
|
||||
}
|
||||
|
||||
// GetPrinterTypes returns the stations the product is printed at, never nil, so a
|
||||
// response always carries a list. A product that was not loaded has none.
|
||||
func (p *Product) GetPrinterTypes() []string {
|
||||
if p.PrinterTypes == nil {
|
||||
return []string{}
|
||||
}
|
||||
return []string(p.PrinterTypes)
|
||||
}
|
||||
|
||||
type ProductVariant struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"`
|
||||
|
||||
@@ -149,7 +149,7 @@ func OrderItemEntityToResponse(item *entities.OrderItem, outletID uuid.UUID) *mo
|
||||
Status: constants.OrderItemStatus(item.Status),
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
PrinterType: item.Product.PrinterType,
|
||||
PrinterTypes: item.Product.GetPrinterTypes(),
|
||||
PrintToChecker: printToChecker,
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func ProductEntityToModel(entity *entities.Product) *models.Product {
|
||||
Cost: entity.Cost,
|
||||
BusinessType: constants.BusinessType(entity.BusinessType),
|
||||
ImageURL: entity.ImageURL,
|
||||
PrinterType: entity.PrinterType,
|
||||
PrinterTypes: entity.GetPrinterTypes(),
|
||||
UnitID: entity.UnitID,
|
||||
SellBy: entity.SellBy,
|
||||
HasIngredients: entity.HasIngredients,
|
||||
@@ -51,7 +51,7 @@ func ProductModelToEntity(model *models.Product) *entities.Product {
|
||||
Cost: model.Cost,
|
||||
BusinessType: string(model.BusinessType),
|
||||
ImageURL: model.ImageURL,
|
||||
PrinterType: model.PrinterType,
|
||||
PrinterTypes: entities.StringSlice(model.PrinterTypes),
|
||||
UnitID: model.UnitID,
|
||||
SellBy: model.SellBy,
|
||||
HasIngredients: model.HasIngredients,
|
||||
@@ -77,11 +77,6 @@ func CreateProductRequestToEntity(req *models.CreateProductRequest) *entities.Pr
|
||||
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
|
||||
@@ -92,7 +87,7 @@ func CreateProductRequestToEntity(req *models.CreateProductRequest) *entities.Pr
|
||||
metadata = entities.Metadata(req.Metadata)
|
||||
}
|
||||
|
||||
return &entities.Product{
|
||||
product := &entities.Product{
|
||||
OrganizationID: req.OrganizationID,
|
||||
CategoryID: req.CategoryID,
|
||||
SKU: req.SKU,
|
||||
@@ -102,12 +97,18 @@ func CreateProductRequestToEntity(req *models.CreateProductRequest) *entities.Pr
|
||||
Cost: cost,
|
||||
BusinessType: businessType,
|
||||
ImageURL: req.ImageURL,
|
||||
PrinterType: printerType,
|
||||
UnitID: req.UnitID,
|
||||
SellBy: sellBy,
|
||||
Metadata: metadata,
|
||||
IsActive: true, // Default to active
|
||||
}
|
||||
|
||||
product.SetPrinterTypes(req.PrinterTypes)
|
||||
if len(product.PrinterTypes) == 0 {
|
||||
product.SetPrinterTypes([]string{"kitchen"})
|
||||
}
|
||||
|
||||
return product
|
||||
}
|
||||
|
||||
func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse {
|
||||
@@ -152,7 +153,7 @@ func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse {
|
||||
Cost: entity.Cost,
|
||||
BusinessType: constants.BusinessType(entity.BusinessType),
|
||||
ImageURL: entity.ImageURL,
|
||||
PrinterType: entity.PrinterType,
|
||||
PrinterTypes: entity.GetPrinterTypes(),
|
||||
UnitID: entity.UnitID,
|
||||
SellBy: entity.SellBy,
|
||||
Metadata: map[string]interface{}(entity.Metadata),
|
||||
@@ -196,8 +197,8 @@ func UpdateProductEntityFromRequest(entity *entities.Product, req *models.Update
|
||||
entity.ImageURL = req.ImageURL
|
||||
}
|
||||
|
||||
if req.PrinterType != nil {
|
||||
entity.PrinterType = *req.PrinterType
|
||||
if req.PrinterTypes != nil {
|
||||
entity.SetPrinterTypes(req.PrinterTypes)
|
||||
}
|
||||
|
||||
if req.UnitID != nil {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateProductRequestToEntityPrinterTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
printerTypes []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "nothing sent prints in the kitchen",
|
||||
want: []string{"kitchen"},
|
||||
},
|
||||
{
|
||||
name: "a meal-and-drink package prints in the kitchen and the bar",
|
||||
printerTypes: []string{"kitchen", "bar"},
|
||||
want: []string{"kitchen", "bar"},
|
||||
},
|
||||
{
|
||||
name: "blanks and repeats are dropped",
|
||||
printerTypes: []string{" kitchen ", "", "kitchen", "bar"},
|
||||
want: []string{"kitchen", "bar"},
|
||||
},
|
||||
{
|
||||
name: "a list of only blanks falls back to the kitchen",
|
||||
printerTypes: []string{" ", ""},
|
||||
want: []string{"kitchen"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
product := CreateProductRequestToEntity(&models.CreateProductRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
CategoryID: uuid.New(),
|
||||
Name: "Paket Makan Minum",
|
||||
PrinterTypes: tt.printerTypes,
|
||||
})
|
||||
|
||||
assert.Equal(t, entities.StringSlice(tt.want), product.PrinterTypes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProductEntityFromRequestPrinterTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
printerTypes []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "a save that does not send printers keeps them",
|
||||
want: []string{"kitchen", "bar"},
|
||||
},
|
||||
{
|
||||
name: "printer_types replaces the whole list",
|
||||
printerTypes: []string{"bar"},
|
||||
want: []string{"bar"},
|
||||
},
|
||||
{
|
||||
name: "an empty printer_types prints nowhere",
|
||||
printerTypes: []string{},
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
product := &entities.Product{}
|
||||
product.SetPrinterTypes([]string{"kitchen", "bar"})
|
||||
|
||||
UpdateProductEntityFromRequest(product, &models.UpdateProductRequest{PrinterTypes: tt.printerTypes})
|
||||
|
||||
assert.Equal(t, entities.StringSlice(tt.want), product.PrinterTypes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderItemEntityToResponsePrinterTypes(t *testing.T) {
|
||||
product := entities.Product{ID: uuid.New(), Name: "Paket Makan Minum"}
|
||||
product.SetPrinterTypes([]string{"kitchen", "bar"})
|
||||
|
||||
response := OrderItemEntityToResponse(&entities.OrderItem{ProductID: product.ID, Product: product}, uuid.New())
|
||||
|
||||
assert.Equal(t, []string{"kitchen", "bar"}, response.PrinterTypes)
|
||||
}
|
||||
|
||||
func TestProductEntityToResponseAlwaysHasAList(t *testing.T) {
|
||||
// The POS reads printer_types as a list, so a product without one sends [] not null.
|
||||
response := ProductEntityToResponse(&entities.Product{})
|
||||
|
||||
assert.Equal(t, []string{}, response.PrinterTypes)
|
||||
}
|
||||
@@ -218,7 +218,7 @@ type OrderItemResponse struct {
|
||||
Status constants.OrderItemStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
PrinterType string
|
||||
PrinterTypes []string
|
||||
PrintToChecker bool
|
||||
PaidQuantity int
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type Product struct {
|
||||
Cost float64
|
||||
BusinessType constants.BusinessType
|
||||
ImageURL *string
|
||||
PrinterType string
|
||||
PrinterTypes []string
|
||||
SellBy string
|
||||
UnitID *uuid.UUID
|
||||
HasIngredients bool
|
||||
@@ -50,7 +50,7 @@ type CreateProductRequest struct {
|
||||
Cost float64 `validate:"min=0"`
|
||||
BusinessType constants.BusinessType `validate:"required"`
|
||||
ImageURL *string `validate:"omitempty,max=500"`
|
||||
PrinterType *string `validate:"omitempty,max=50"`
|
||||
PrinterTypes []string `validate:"omitempty,dive,max=50"`
|
||||
PrintToChecker *bool `validate:"omitempty"`
|
||||
UnitID *uuid.UUID `validate:"omitempty"`
|
||||
SellBy string `validate:"omitempty,oneof=unit weight"`
|
||||
@@ -72,7 +72,7 @@ type UpdateProductRequest struct {
|
||||
Price *float64 `validate:"omitempty,min=0"`
|
||||
Cost *float64 `validate:"omitempty,min=0"`
|
||||
ImageURL *string `validate:"omitempty,max=500"`
|
||||
PrinterType *string `validate:"omitempty,max=50"`
|
||||
PrinterTypes []string `validate:"omitempty,dive,max=50"` // Replaces the whole list when not nil
|
||||
PrintToChecker *bool `validate:"omitempty"`
|
||||
UnitID *uuid.UUID `validate:"omitempty"`
|
||||
SellBy *string `validate:"omitempty,oneof=unit weight"`
|
||||
@@ -112,7 +112,7 @@ type ProductResponse struct {
|
||||
Cost float64
|
||||
BusinessType constants.BusinessType
|
||||
ImageURL *string
|
||||
PrinterType string
|
||||
PrinterTypes []string
|
||||
SellBy string
|
||||
PrintToChecker bool
|
||||
UnitID *uuid.UUID
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The items AddToOrder creates have no product loaded, so their responses used to go out
|
||||
// without a name or a printer, and the POS could not print them.
|
||||
func TestAddedItemsFromOrderUsesTheReloadedProduct(t *testing.T) {
|
||||
outletID := uuid.New()
|
||||
|
||||
paket := entities.Product{ID: uuid.New(), Name: "Paket Makan Minum"}
|
||||
paket.SetPrinterTypes([]string{"kitchen", "bar"})
|
||||
paket.ProductOutletPrices = []entities.ProductOutletPrice{{OutletID: outletID, PrintToChecker: false}}
|
||||
esTeh := entities.Product{ID: uuid.New(), Name: "Es Teh"}
|
||||
esTeh.SetPrinterTypes([]string{"bar"})
|
||||
|
||||
existing := entities.OrderItem{ID: uuid.New(), ProductID: esTeh.ID, Product: esTeh}
|
||||
addedPaket := &entities.OrderItem{ID: uuid.New(), ProductID: paket.ID}
|
||||
addedEsTeh := &entities.OrderItem{ID: uuid.New(), ProductID: esTeh.ID}
|
||||
|
||||
order := &entities.Order{
|
||||
OutletID: outletID,
|
||||
OrderItems: []entities.OrderItem{
|
||||
existing,
|
||||
{ID: addedEsTeh.ID, ProductID: esTeh.ID, Product: esTeh},
|
||||
{ID: addedPaket.ID, ProductID: paket.ID, Product: paket},
|
||||
},
|
||||
}
|
||||
|
||||
responses := addedItemsFromOrder([]*entities.OrderItem{addedPaket, addedEsTeh}, order)
|
||||
|
||||
require.Len(t, responses, 2, "only the added items, not the ones already on the order")
|
||||
assert.Equal(t, addedPaket.ID, responses[0].ID, "in the order they were requested")
|
||||
assert.Equal(t, "Paket Makan Minum", responses[0].ProductName)
|
||||
assert.Equal(t, []string{"kitchen", "bar"}, responses[0].PrinterTypes)
|
||||
assert.False(t, responses[0].PrintToChecker, "the outlet's print_to_checker is loaded too")
|
||||
assert.Equal(t, addedEsTeh.ID, responses[1].ID)
|
||||
assert.Equal(t, []string{"bar"}, responses[1].PrinterTypes)
|
||||
}
|
||||
@@ -568,16 +568,10 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
||||
return nil, fmt.Errorf("failed to update order: %w", err)
|
||||
}
|
||||
|
||||
var addedItemResponses []models.OrderItemResponse
|
||||
for _, orderItem := range addedOrderItems {
|
||||
if err := p.orderItemRepo.Create(ctx, orderItem); err != nil {
|
||||
return nil, fmt.Errorf("failed to create order item: %w", err)
|
||||
}
|
||||
|
||||
itemResponse := mappers.OrderItemEntityToResponse(orderItem, order.OutletID)
|
||||
if itemResponse != nil {
|
||||
addedItemResponses = append(addedItemResponses, *itemResponse)
|
||||
}
|
||||
}
|
||||
|
||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID)
|
||||
@@ -585,6 +579,10 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
||||
return nil, fmt.Errorf("failed to retrieve updated order: %w", err)
|
||||
}
|
||||
|
||||
// The items just created carry no product, so take them from the reloaded order: the
|
||||
// POS prints added items from this response and needs their name and printers.
|
||||
addedItemResponses := addedItemsFromOrder(addedOrderItems, orderWithRelations)
|
||||
|
||||
updatedOrderResponse := mappers.OrderEntityToResponse(orderWithRelations)
|
||||
p.attachEarnings(ctx, updatedOrderResponse)
|
||||
|
||||
@@ -596,6 +594,26 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// addedItemsFromOrder maps the added items in the order they were requested, using their
|
||||
// copies in the reloaded order, which have the product and its outlet prices loaded.
|
||||
func addedItemsFromOrder(added []*entities.OrderItem, order *entities.Order) []models.OrderItemResponse {
|
||||
reloaded := make(map[uuid.UUID]*entities.OrderItem, len(order.OrderItems))
|
||||
for i := range order.OrderItems {
|
||||
reloaded[order.OrderItems[i].ID] = &order.OrderItems[i]
|
||||
}
|
||||
|
||||
var responses []models.OrderItemResponse
|
||||
for _, item := range added {
|
||||
if loaded, ok := reloaded[item.ID]; ok {
|
||||
item = loaded
|
||||
}
|
||||
if response := mappers.OrderItemEntityToResponse(item, order.OutletID); response != nil {
|
||||
responses = append(responses, *response)
|
||||
}
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) {
|
||||
// Get existing order
|
||||
order, err := p.orderRepo.GetByID(ctx, id)
|
||||
|
||||
@@ -199,7 +199,7 @@ func (p *ProductRecipeProcessorImpl) entityToResponse(entity *entities.ProductRe
|
||||
Cost: entity.Product.Cost,
|
||||
BusinessType: string(entity.Product.BusinessType),
|
||||
ImageURL: entity.Product.ImageURL,
|
||||
PrinterType: entity.Product.PrinterType,
|
||||
PrinterTypes: entity.Product.GetPrinterTypes(),
|
||||
Metadata: entity.Product.Metadata,
|
||||
IsActive: entity.Product.IsActive,
|
||||
CreatedAt: entity.Product.CreatedAt,
|
||||
|
||||
@@ -39,7 +39,7 @@ func (r *ProductIngredientRepository) Create(ctx context.Context, productIngredi
|
||||
func (r *ProductIngredientRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.ProductIngredient, error) {
|
||||
query := `
|
||||
SELECT pi.id, pi.organization_id, pi.outlet_id, pi.product_id, pi.ingredient_id, pi.quantity, pi.created_at, pi.updated_at,
|
||||
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_type, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_types, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||
i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at
|
||||
FROM product_ingredients pi
|
||||
LEFT JOIN products p ON pi.product_id = p.id
|
||||
@@ -70,7 +70,7 @@ func (r *ProductIngredientRepository) GetByID(ctx context.Context, id, organizat
|
||||
&product.Cost,
|
||||
&product.BusinessType,
|
||||
&product.ImageURL,
|
||||
&product.PrinterType,
|
||||
&product.PrinterTypes,
|
||||
&product.UnitID,
|
||||
&product.HasIngredients,
|
||||
&product.Metadata,
|
||||
@@ -103,7 +103,7 @@ func (r *ProductIngredientRepository) GetByID(ctx context.Context, id, organizat
|
||||
func (r *ProductIngredientRepository) GetByProductID(ctx context.Context, productID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) {
|
||||
query := `
|
||||
SELECT pi.id, pi.organization_id, pi.outlet_id, pi.product_id, pi.ingredient_id, pi.quantity, pi.created_at, pi.updated_at,
|
||||
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_type, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_types, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||
i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at
|
||||
FROM product_ingredients pi
|
||||
LEFT JOIN products p ON pi.product_id = p.id
|
||||
@@ -143,7 +143,7 @@ func (r *ProductIngredientRepository) GetByProductID(ctx context.Context, produc
|
||||
&product.Cost,
|
||||
&product.BusinessType,
|
||||
&product.ImageURL,
|
||||
&product.PrinterType,
|
||||
&product.PrinterTypes,
|
||||
&product.UnitID,
|
||||
&product.HasIngredients,
|
||||
&product.Metadata,
|
||||
@@ -178,7 +178,7 @@ func (r *ProductIngredientRepository) GetByProductID(ctx context.Context, produc
|
||||
func (r *ProductIngredientRepository) GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) {
|
||||
query := `
|
||||
SELECT pi.id, pi.organization_id, pi.outlet_id, pi.product_id, pi.ingredient_id, pi.quantity, pi.created_at, pi.updated_at,
|
||||
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_type, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_types, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||
i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at
|
||||
FROM product_ingredients pi
|
||||
LEFT JOIN products p ON pi.product_id = p.id
|
||||
@@ -218,7 +218,7 @@ func (r *ProductIngredientRepository) GetByIngredientID(ctx context.Context, ing
|
||||
&product.Cost,
|
||||
&product.BusinessType,
|
||||
&product.ImageURL,
|
||||
&product.PrinterType,
|
||||
&product.PrinterTypes,
|
||||
&product.UnitID,
|
||||
&product.HasIngredients,
|
||||
&product.Metadata,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
)
|
||||
|
||||
// Needs TEST_DATABASE_URL pointing at a migrated database; see walletTestDB.
|
||||
func TestProductPrinterTypes_AgainstPostgres(t *testing.T) {
|
||||
db := walletTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
org, category := uuid.New(), uuid.New()
|
||||
require.NoError(t, db.Exec(`INSERT INTO organizations (id, name, plan_type) VALUES (?, 'printer test', 'basic')`, org).Error)
|
||||
require.NoError(t, db.Exec(`INSERT INTO categories (id, organization_id, name) VALUES (?, ?, 'Paket')`, category, org).Error)
|
||||
t.Cleanup(func() {
|
||||
db.Exec(`DELETE FROM products WHERE organization_id = ?`, org)
|
||||
db.Exec(`DELETE FROM categories WHERE id = ?`, category)
|
||||
db.Exec(`DELETE FROM organizations WHERE id = ?`, org)
|
||||
})
|
||||
|
||||
repo := NewProductRepositoryImpl(db)
|
||||
product := &entities.Product{OrganizationID: org, CategoryID: category, Name: "Paket Makan Minum", Price: 25000}
|
||||
product.SetPrinterTypes([]string{"kitchen", "bar"})
|
||||
require.NoError(t, repo.Create(ctx, product))
|
||||
|
||||
stored, err := repo.GetByID(ctx, product.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, entities.StringSlice{"kitchen", "bar"}, stored.PrinterTypes)
|
||||
|
||||
stored.SetPrinterTypes([]string{"bar"})
|
||||
require.NoError(t, repo.Update(ctx, stored))
|
||||
|
||||
var printerTypes string
|
||||
require.NoError(t, db.Raw(`SELECT printer_types::text FROM products WHERE id = ?`, product.ID).Scan(&printerTypes).Error)
|
||||
assert.Equal(t, `["bar"]`, printerTypes)
|
||||
|
||||
// The old single column is gone.
|
||||
var columns int64
|
||||
require.NoError(t, db.Raw(`SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'printer_type'`).Scan(&columns).Error)
|
||||
assert.Zero(t, columns)
|
||||
}
|
||||
@@ -44,8 +44,9 @@ func InventoryModelResponseToResponse(inv *models.InventoryResponse) *contract.I
|
||||
IsLowStock: inv.IsLowStock,
|
||||
UpdatedAt: inv.UpdatedAt,
|
||||
Product: &contract.ProductResponse{
|
||||
ID: inv.ProductID,
|
||||
Name: inv.ProductName,
|
||||
ID: inv.ProductID,
|
||||
Name: inv.ProductName,
|
||||
PrinterTypes: []string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse {
|
||||
Status: string(item.Status),
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
PrinterType: item.PrinterType,
|
||||
PrinterTypes: item.PrinterTypes,
|
||||
PrintToChecker: item.PrintToChecker,
|
||||
PaidQuantity: item.PaidQuantity,
|
||||
}
|
||||
@@ -195,6 +195,7 @@ func AddToOrderModelToContract(resp *models.AddToOrderResponse) *contract.AddToO
|
||||
Status: string(item.Status),
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
PrinterTypes: item.PrinterTypes,
|
||||
PrintToChecker: item.PrintToChecker,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package transformer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The POS prints items added to an open order from this response, so each one has to
|
||||
// say where it is printed.
|
||||
func TestAddToOrderModelToContractCarriesPrinters(t *testing.T) {
|
||||
result := AddToOrderModelToContract(&models.AddToOrderResponse{
|
||||
AddedItems: []models.OrderItemResponse{{PrinterTypes: []string{"kitchen", "bar"}}},
|
||||
})
|
||||
|
||||
require.Len(t, result.AddedItems, 1)
|
||||
assert.Equal(t, []string{"kitchen", "bar"}, result.AddedItems[0].PrinterTypes)
|
||||
}
|
||||
|
||||
// Follows an order from the database row to the JSON the POS reads.
|
||||
func TestOrderJSONCarriesPrinters(t *testing.T) {
|
||||
paket := entities.Product{ID: uuid.New(), Name: "Paket Makan Minum"}
|
||||
paket.SetPrinterTypes([]string{"kitchen", "bar"})
|
||||
order := &entities.Order{
|
||||
ID: uuid.New(),
|
||||
OrderItems: []entities.OrderItem{{ID: uuid.New(), ProductID: paket.ID, Product: paket, Quantity: 1}},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(OrderModelToContract(mappers.OrderEntityToResponse(order)))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(body), `"printer_types":["kitchen","bar"]`)
|
||||
assert.NotContains(t, string(body), `"printer_type":`, "printer_types replaced it")
|
||||
}
|
||||
|
||||
func TestOrderModelToContractCarriesPrinters(t *testing.T) {
|
||||
result := OrderModelToContract(&models.OrderResponse{
|
||||
OrderItems: []models.OrderItemResponse{{PrinterTypes: []string{"kitchen", "bar"}}},
|
||||
})
|
||||
|
||||
require.Len(t, result.OrderItems, 1)
|
||||
assert.Equal(t, []string{"kitchen", "bar"}, result.OrderItems[0].PrinterTypes)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Cr
|
||||
Cost: cost,
|
||||
BusinessType: businessType,
|
||||
ImageURL: req.ImageURL,
|
||||
PrinterType: req.PrinterType,
|
||||
PrinterTypes: req.PrinterTypes,
|
||||
PrintToChecker: req.PrintToChecker,
|
||||
UnitID: req.UnitID,
|
||||
SellBy: sellBy,
|
||||
@@ -91,7 +91,7 @@ func UpdateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Up
|
||||
Price: req.Price,
|
||||
Cost: req.Cost,
|
||||
ImageURL: req.ImageURL,
|
||||
PrinterType: req.PrinterType,
|
||||
PrinterTypes: req.PrinterTypes,
|
||||
PrintToChecker: req.PrintToChecker,
|
||||
UnitID: req.UnitID,
|
||||
SellBy: req.SellBy,
|
||||
@@ -152,7 +152,7 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
|
||||
Cost: prod.Cost,
|
||||
BusinessType: string(prod.BusinessType),
|
||||
ImageURL: prod.ImageURL,
|
||||
PrinterType: prod.PrinterType,
|
||||
PrinterTypes: prod.PrinterTypes,
|
||||
PrintToChecker: prod.PrintToChecker,
|
||||
UnitID: prod.UnitID,
|
||||
SellBy: prod.SellBy,
|
||||
|
||||
@@ -59,8 +59,8 @@ func (v *ProductValidatorImpl) ValidateCreateProductRequest(req *contract.Create
|
||||
return errors.New("image_url cannot exceed 500 characters"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
if req.PrinterType != nil && len(*req.PrinterType) > 50 {
|
||||
return errors.New("printer_type cannot exceed 50 characters"), constants.MalformedFieldErrorCode
|
||||
if err, code := validatePrinterTypes(req.PrinterTypes); err != nil {
|
||||
return err, code
|
||||
}
|
||||
|
||||
if err, code := validateSellBy(req.SellBy, req.UnitID); err != nil {
|
||||
@@ -70,6 +70,18 @@ func (v *ProductValidatorImpl) ValidateCreateProductRequest(req *contract.Create
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// validatePrinterTypes holds each printer to the 50 characters the single printer_type
|
||||
// column allowed.
|
||||
func validatePrinterTypes(printerTypes []string) (error, string) {
|
||||
for _, printerType := range printerTypes {
|
||||
if len(strings.TrimSpace(printerType)) > 50 {
|
||||
return errors.New("each printer_types entry cannot exceed 50 characters"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// validateSellBy checks how a product is sold and that it carries what that choice
|
||||
// needs. A weight-based product without a unit would produce order lines with no unit
|
||||
// to print, so the receipt could show "4,2" with no idea of what.
|
||||
@@ -100,7 +112,7 @@ func (v *ProductValidatorImpl) ValidateUpdateProductRequest(req *contract.Update
|
||||
// At least one field should be provided for update
|
||||
if req.CategoryID == nil && req.SKU == nil && req.Name == nil && req.Description == nil &&
|
||||
req.Price == nil && req.Cost == nil && req.BusinessType == nil && req.ImageURL == nil &&
|
||||
req.PrinterType == nil && req.PrintToChecker == nil && req.UnitID == nil &&
|
||||
req.PrinterTypes == nil && req.PrintToChecker == nil && req.UnitID == nil &&
|
||||
req.SellBy == nil && req.Metadata == nil && req.IsActive == nil {
|
||||
return errors.New("at least one field must be provided for update"), constants.MissingFieldErrorCode
|
||||
}
|
||||
@@ -134,8 +146,8 @@ func (v *ProductValidatorImpl) ValidateUpdateProductRequest(req *contract.Update
|
||||
return errors.New("image_url cannot exceed 500 characters"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
if req.PrinterType != nil && len(*req.PrinterType) > 50 {
|
||||
return errors.New("printer_type cannot exceed 50 characters"), constants.MalformedFieldErrorCode
|
||||
if err, code := validatePrinterTypes(req.PrinterTypes); err != nil {
|
||||
return err, code
|
||||
}
|
||||
|
||||
// Only the value is checked here. Whether the product ends up with a unit depends on
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"apskel-pos-be/internal/contract"
|
||||
)
|
||||
|
||||
func TestValidateProductRequestPrinterTypes(t *testing.T) {
|
||||
v := NewProductValidator()
|
||||
tooLong := strings.Repeat("a", 51)
|
||||
|
||||
create := baseCreateRequest()
|
||||
create.PrinterTypes = []string{"kitchen", "bar"}
|
||||
if err, _ := v.ValidateCreateProductRequest(create); err != nil {
|
||||
t.Fatalf("kitchen and bar should be accepted, got: %v", err)
|
||||
}
|
||||
|
||||
create.PrinterTypes = []string{"kitchen", tooLong}
|
||||
if err, _ := v.ValidateCreateProductRequest(create); err == nil {
|
||||
t.Fatal("expected an error for a printer longer than 50 characters")
|
||||
}
|
||||
|
||||
err, _ := v.ValidateUpdateProductRequest(&contract.UpdateProductRequest{PrinterTypes: []string{tooLong}})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for a printer longer than 50 characters on update")
|
||||
}
|
||||
|
||||
// An update that only changes the printers is a real update.
|
||||
err, _ = v.ValidateUpdateProductRequest(&contract.UpdateProductRequest{PrinterTypes: []string{"kitchen", "bar"}})
|
||||
if err != nil {
|
||||
t.Fatalf("an update of only printer_types should be accepted, got: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user