feature/print-checker #12

Merged
aefril merged 2 commits from feature/print-checker into main 2026-05-28 08:31:30 +00:00
14 changed files with 199 additions and 108 deletions
Showing only changes of commit 23ac572e3f - Show all commits

View File

@ -17,6 +17,7 @@ type CreateProductRequest struct {
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"`
PrintToChecker *bool `json:"print_to_checker,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
@ -36,6 +37,7 @@ type UpdateProductRequest struct {
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"`
PrintToChecker *bool `json:"print_to_checker,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
@ -71,6 +73,7 @@ type ProductResponse struct {
BusinessType string `json:"business_type"`
ImageURL *string `json:"image_url"`
PrinterType string `json:"printer_type"`
PrintToChecker bool `json:"print_to_checker"`
Metadata map[string]interface{} `json:"metadata"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`

View File

@ -10,10 +10,12 @@ type CreateProductOutletPriceRequest struct {
ProductID uuid.UUID `json:"product_id" validate:"required"`
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
Price float64 `json:"price" validate:"required,min=0"`
PrintToChecker bool `json:"print_to_checker"`
}
type UpdateProductOutletPriceRequest struct {
Price float64 `json:"price" validate:"required,min=0"`
PrintToChecker *bool `json:"print_to_checker"`
}
type ProductOutletPriceResponse struct {
@ -22,6 +24,7 @@ type ProductOutletPriceResponse struct {
OutletID uuid.UUID `json:"outlet_id"`
OutletName string `json:"outlet_name,omitempty"`
Price float64 `json:"price"`
PrintToChecker bool `json:"print_to_checker"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
@ -39,4 +42,5 @@ type BulkCreateProductOutletPriceRequest struct {
type CreateProductOutletPricePerOutletRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
Price float64 `json:"price" validate:"required,min=0"`
PrintToChecker bool `json:"print_to_checker"`
}

View File

@ -12,6 +12,7 @@ type ProductOutletPrice struct {
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"`
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
PrintToChecker bool `gorm:"not null;default:true" json:"print_to_checker"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`

View File

@ -15,6 +15,7 @@ func ProductOutletPriceEntityToModel(entity *entities.ProductOutletPrice) *model
ProductID: entity.ProductID,
OutletID: entity.OutletID,
Price: entity.Price,
PrintToChecker: entity.PrintToChecker,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
@ -30,6 +31,7 @@ func ProductOutletPriceModelToEntity(model *models.ProductOutletPrice) *entities
ProductID: model.ProductID,
OutletID: model.OutletID,
Price: model.Price,
PrintToChecker: model.PrintToChecker,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}

View File

@ -50,6 +50,7 @@ type CreateProductRequest struct {
BusinessType constants.BusinessType `validate:"required"`
ImageURL *string `validate:"omitempty,max=500"`
PrinterType *string `validate:"omitempty,max=50"`
PrintToChecker *bool `validate:"omitempty"`
UnitID *uuid.UUID `validate:"omitempty"`
HasIngredients bool `validate:"omitempty"`
Metadata map[string]interface{}
@ -70,6 +71,7 @@ type UpdateProductRequest struct {
Cost *float64 `validate:"omitempty,min=0"`
ImageURL *string `validate:"omitempty,max=500"`
PrinterType *string `validate:"omitempty,max=50"`
PrintToChecker *bool `validate:"omitempty"`
UnitID *uuid.UUID `validate:"omitempty"`
HasIngredients *bool `validate:"omitempty"`
Metadata map[string]interface{}
@ -108,6 +110,7 @@ type ProductResponse struct {
BusinessType constants.BusinessType
ImageURL *string
PrinterType string
PrintToChecker bool
UnitID *uuid.UUID
HasIngredients bool
Metadata map[string]interface{}
@ -121,6 +124,7 @@ type OutletPrice struct {
OutletID uuid.UUID
OutletName string
Price float64
PrintToChecker bool
}
type ProductVariantResponse struct {

View File

@ -11,6 +11,7 @@ type ProductOutletPrice struct {
ProductID uuid.UUID
OutletID uuid.UUID
Price float64
PrintToChecker bool
CreatedAt time.Time
UpdatedAt time.Time
}
@ -19,10 +20,12 @@ type CreateProductOutletPriceRequest struct {
ProductID uuid.UUID `validate:"required"`
OutletID uuid.UUID `validate:"required"`
Price float64 `validate:"required,min=0"`
PrintToChecker bool
}
type UpdateProductOutletPriceRequest struct {
Price *float64 `validate:"required,min=0"`
PrintToChecker *bool
}
type ProductOutletPriceResponse struct {

View File

@ -49,6 +49,7 @@ func (p *ProductOutletPriceProcessorImpl) Upsert(ctx context.Context, req *model
ProductID: req.ProductID,
OutletID: req.OutletID,
Price: req.Price,
PrintToChecker: req.PrintToChecker,
}
if err := p.repo.Upsert(ctx, entity); err != nil {

View File

@ -5,6 +5,7 @@ import (
"fmt"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/logger"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
@ -125,10 +126,15 @@ func (p *ProductProcessorImpl) CreateProduct(ctx context.Context, req *models.Cr
// Upsert outlet-specific price if outlet context is present
if req.OutletID != uuid.Nil {
printToChecker := true // default
if req.PrintToChecker != nil {
printToChecker = *req.PrintToChecker
}
outletPriceEntity := &entities.ProductOutletPrice{
ProductID: productEntity.ID,
OutletID: req.OutletID,
Price: req.Price,
PrintToChecker: printToChecker,
}
if err := p.outletPriceRepo.Upsert(ctx, outletPriceEntity); err != nil {
return nil, fmt.Errorf("failed to assign outlet price: %w", err)
@ -196,16 +202,39 @@ func (p *ProductProcessorImpl) UpdateProduct(ctx context.Context, id uuid.UUID,
}
}
// Upsert outlet-specific price if outlet context is present
if req.OutletID != uuid.Nil && req.Price != nil {
// Upsert outlet-specific price if outlet context is present and price or print_to_checker is provided
if req.OutletID != uuid.Nil && (req.Price != nil || req.PrintToChecker != nil) {
// Fetch existing outlet price to use as fallback for fields not provided
existing, _ := p.outletPriceRepo.GetByProductAndOutlet(ctx, id, req.OutletID)
price := float64(0)
if existing != nil {
price = existing.Price
}
if req.Price != nil {
price = *req.Price
}
printToChecker := true // default
if existing != nil {
printToChecker = existing.PrintToChecker
}
if req.PrintToChecker != nil {
printToChecker = *req.PrintToChecker
}
outletPriceEntity := &entities.ProductOutletPrice{
ProductID: id,
OutletID: req.OutletID,
Price: *req.Price,
Price: price,
PrintToChecker: printToChecker,
}
logger.FromContext(ctx).Infof("ProductProcessor::UpdateProduct -> upserting outlet price: productID=%s outletID=%s price=%f printToChecker=%v", id, req.OutletID, price, printToChecker)
if err := p.outletPriceRepo.Upsert(ctx, outletPriceEntity); err != nil {
return nil, fmt.Errorf("failed to assign outlet price: %w", err)
}
} else {
logger.FromContext(ctx).Infof("ProductProcessor::UpdateProduct -> skipping outlet price upsert: outletID=%s price=%v printToChecker=%v", req.OutletID, req.Price, req.PrintToChecker)
}
productWithCategory, err := p.productRepo.GetWithCategory(ctx, id)
@ -256,6 +285,7 @@ func (p *ProductProcessorImpl) GetProductByID(ctx context.Context, id uuid.UUID,
outletPrice, err := p.outletPriceRepo.GetByProductAndOutlet(ctx, id, outletID)
if err == nil {
response.OutletPrice = &outletPrice.Price
response.PrintToChecker = outletPrice.PrintToChecker
}
} else {
// No outlet context — return all outlet prices for this product
@ -267,6 +297,7 @@ func (p *ProductProcessorImpl) GetProductByID(ctx context.Context, id uuid.UUID,
OutletID: op.OutletID,
OutletName: op.Outlet.Name,
Price: op.Price,
PrintToChecker: op.PrintToChecker,
}
}
response.OutletPrices = prices
@ -303,12 +334,37 @@ func (p *ProductProcessorImpl) ListProducts(ctx context.Context, filters map[str
}
responses := make([]models.ProductResponse, len(productEntities))
if outletID != uuid.Nil && len(productEntities) > 0 {
// Bulk-fetch outlet prices to populate OutletPrice and PrintToChecker per product
productIDs := make([]uuid.UUID, len(productEntities))
for i, e := range productEntities {
productIDs[i] = e.ID
}
outletPrices, opErr := p.outletPriceRepo.GetByProductsAndOutlet(ctx, productIDs, outletID)
priceMap := make(map[uuid.UUID]*entities.ProductOutletPrice)
if opErr == nil {
for _, op := range outletPrices {
priceMap[op.ProductID] = op
}
}
for i, entity := range productEntities {
response := mappers.ProductEntityToResponse(entity)
if response != nil {
if op, ok := priceMap[entity.ID]; ok {
response.OutletPrice = &op.Price
response.PrintToChecker = op.PrintToChecker
}
responses[i] = *response
}
}
} else {
for i, entity := range productEntities {
response := mappers.ProductEntityToResponse(entity)
if response != nil {
responses[i] = *response
}
}
}
return responses, int(total), nil
}

View File

@ -7,7 +7,6 @@ import (
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type ProductOutletPriceRepository interface {
@ -53,10 +52,18 @@ func (r *ProductOutletPriceRepositoryImpl) GetByOutlet(ctx context.Context, outl
}
func (r *ProductOutletPriceRepositoryImpl) Upsert(ctx context.Context, price *entities.ProductOutletPrice) error {
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "product_id"}, {Name: "outlet_id"}},
DoUpdates: clause.AssignmentColumns([]string{"price", "updated_at"}),
}).Create(price).Error
if price.ID == uuid.Nil {
price.ID = uuid.New()
}
return r.db.WithContext(ctx).Exec(`
INSERT INTO product_outlet_prices (id, product_id, outlet_id, price, print_to_checker, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, NOW(), NOW())
ON CONFLICT (product_id, outlet_id)
DO UPDATE SET
price = EXCLUDED.price,
print_to_checker = EXCLUDED.print_to_checker,
updated_at = NOW()
`, price.ID, price.ProductID, price.OutletID, price.Price, price.PrintToChecker).Error
}
func (r *ProductOutletPriceRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {

View File

@ -108,6 +108,7 @@ func (s *ProductOutletPriceServiceImpl) BulkUpsert(ctx context.Context, req *con
ProductID: req.ProductID,
OutletID: p.OutletID,
Price: p.Price,
PrintToChecker: p.PrintToChecker,
}
}

View File

@ -14,6 +14,7 @@ func CreateProductOutletPriceRequestToModel(req *contract.CreateProductOutletPri
ProductID: req.ProductID,
OutletID: req.OutletID,
Price: req.Price,
PrintToChecker: req.PrintToChecker,
}
}
@ -24,6 +25,7 @@ func UpdateProductOutletPriceRequestToModel(req *contract.UpdateProductOutletPri
return &models.UpdateProductOutletPriceRequest{
Price: &req.Price,
PrintToChecker: req.PrintToChecker,
}
}
@ -37,6 +39,7 @@ func ProductOutletPriceModelToResponse(m *models.ProductOutletPrice) *contract.P
ProductID: m.ProductID,
OutletID: m.OutletID,
Price: m.Price,
PrintToChecker: m.PrintToChecker,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}

View File

@ -57,6 +57,7 @@ func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Cr
BusinessType: businessType,
ImageURL: req.ImageURL,
PrinterType: req.PrinterType,
PrintToChecker: req.PrintToChecker,
Metadata: metadata,
Variants: variants,
}
@ -84,6 +85,7 @@ func UpdateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Up
Cost: req.Cost,
ImageURL: req.ImageURL,
PrinterType: req.PrinterType,
PrintToChecker: req.PrintToChecker,
Metadata: metadata,
IsActive: req.IsActive,
}
@ -122,6 +124,7 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
OutletID: op.OutletID,
OutletName: op.OutletName,
Price: op.Price,
PrintToChecker: op.PrintToChecker,
}
}
}
@ -141,6 +144,7 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
BusinessType: string(prod.BusinessType),
ImageURL: prod.ImageURL,
PrinterType: prod.PrinterType,
PrintToChecker: prod.PrintToChecker,
Metadata: prod.Metadata,
IsActive: prod.IsActive,
CreatedAt: prod.CreatedAt,

View File

@ -0,0 +1 @@
ALTER TABLE product_outlet_prices DROP COLUMN IF EXISTS print_to_checker;

View File

@ -0,0 +1 @@
ALTER TABLE product_outlet_prices ADD COLUMN print_to_checker BOOLEAN NOT NULL DEFAULT TRUE;