package service import ( "context" "database/sql" "fmt" "time" "github.com/google/uuid" "github.com/muyuqingfeng/iloom/purchaser-service/internal/model" "github.com/muyuqingfeng/iloom/purchaser-service/internal/repository" ) type ProductService struct { repo *repository.ProductRepo db *sql.DB } func NewProductService(repo *repository.ProductRepo, db *sql.DB) *ProductService { return &ProductService{repo: repo, db: db} } type YarnRatio struct { YarnName string `json:"yarn_name"` YarnType string `json:"yarn_type"` Ratio float64 `json:"ratio"` } type CreateProductRequest struct { ProductName string `json:"product_name"` FabricCode string `json:"fabric_code"` Color string `json:"color"` ColorCode string `json:"color_code"` Weight float64 `json:"weight"` WarpWeight *float64 `json:"warp_weight"` WeftWeight *float64 `json:"weft_weight"` TotalWeight *float64 `json:"total_weight"` YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` ProductionPrice *float64 `json:"production_price"` ImageURL *string `json:"image_url"` ProductType string `json:"product_type"` YarnTypes []string `json:"yarn_types"` YarnRatios []YarnRatio `json:"yarn_ratios"` } type UpdateProductRequest struct { ProductName string `json:"product_name"` FabricCode string `json:"fabric_code"` Color string `json:"color"` ColorCode string `json:"color_code"` Weight float64 `json:"weight"` WarpWeight *float64 `json:"warp_weight"` WeftWeight *float64 `json:"weft_weight"` TotalWeight *float64 `json:"total_weight"` YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` ProductionPrice *float64 `json:"production_price"` ImageURL *string `json:"image_url"` ProductType string `json:"product_type"` YarnTypes []string `json:"yarn_types"` YarnRatios []YarnRatio `json:"yarn_ratios"` } type InboundRequest struct { BatchNo string `json:"batch_no"` Rolls int `json:"rolls"` Meters float64 `json:"meters"` Notes string `json:"notes"` WarehouseID string `json:"warehouse_id"` } type OutboundRequest struct { BatchNo string `json:"batch_no"` Rolls int `json:"rolls"` Meters float64 `json:"meters"` OutboundType string `json:"outbound_type"` Notes string `json:"notes"` RecipientCompanyID string `json:"recipient_company_id"` RecipientCompanyName string `json:"recipient_company_name"` SourceBatchID string `json:"source_batch_id"` WashingPlanID string `json:"washing_plan_id"` } func (s *ProductService) List(ctx context.Context, companyID string) ([]model.Product, error) { return s.repo.List(ctx, companyID) } func (s *ProductService) Create(ctx context.Context, companyID string, req CreateProductRequest) (*model.Product, error) { now := time.Now() yarnRatios := make(model.JSONYarnRatios, len(req.YarnRatios)) for i, yr := range req.YarnRatios { yarnRatios[i] = model.ProductYarnRatio{ YarnName: yr.YarnName, YarnType: yr.YarnType, Ratio: yr.Ratio, } } productType := req.ProductType if productType == "" { productType = "raw_fabric" } p := &model.Product{ ID: uuid.New().String(), CompanyID: companyID, ProductName: req.ProductName, FabricCode: req.FabricCode, Color: req.Color, ColorCode: req.ColorCode, Weight: req.Weight, WarpWeight: req.WarpWeight, WeftWeight: req.WeftWeight, TotalWeight: req.TotalWeight, YarnUsagePerMeter: req.YarnUsagePerMeter, ProductionPrice: req.ProductionPrice, ImageURL: req.ImageURL, ProductType: productType, TotalStock: 0, RawFabricMeters: 0, RawFabricRolls: 0, YarnTypes: model.JSONStringSlice(req.YarnTypes), YarnRatios: yarnRatios, CreatedAt: now, UpdatedAt: now, } if err := s.repo.Create(ctx, p); err != nil { return nil, err } return p, nil } func (s *ProductService) Update(ctx context.Context, id, companyID string, req UpdateProductRequest) error { yarnRatios := make(model.JSONYarnRatios, len(req.YarnRatios)) for i, yr := range req.YarnRatios { yarnRatios[i] = model.ProductYarnRatio{ YarnName: yr.YarnName, YarnType: yr.YarnType, Ratio: yr.Ratio, } } productType := req.ProductType if productType == "" { productType = "raw_fabric" } p := &model.Product{ ID: id, ProductName: req.ProductName, FabricCode: req.FabricCode, Color: req.Color, ColorCode: req.ColorCode, Weight: req.Weight, WarpWeight: req.WarpWeight, WeftWeight: req.WeftWeight, TotalWeight: req.TotalWeight, YarnUsagePerMeter: req.YarnUsagePerMeter, ProductionPrice: req.ProductionPrice, ImageURL: req.ImageURL, ProductType: productType, YarnTypes: model.JSONStringSlice(req.YarnTypes), YarnRatios: yarnRatios, } return s.repo.Update(ctx, p) } func (s *ProductService) Delete(ctx context.Context, id, companyID string) error { return s.repo.Delete(ctx, id, companyID) } func (s *ProductService) Inbound(ctx context.Context, productID, companyID, userID string, req InboundRequest) error { now := time.Now() var notesPtr *string if req.Notes != "" { notesPtr = &req.Notes } var warehousePtr *string if req.WarehouseID != "" { warehousePtr = &req.WarehouseID } rec := &model.ProductInventoryRecord{ ID: uuid.New().String(), ProductID: productID, BatchNo: req.BatchNo, Rolls: req.Rolls, Meters: req.Meters, Notes: notesPtr, OperatorID: &userID, CompanyID: &companyID, WarehouseID: warehousePtr, CreatedAt: now, } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer tx.Rollback() if err := s.repo.Inbound(ctx, tx, rec); err != nil { return err } if err := s.repo.UpdateStock(ctx, tx, productID, req.Meters, req.Rolls); err != nil { return err } return tx.Commit() } func (s *ProductService) Outbound(ctx context.Context, productID, companyID, userID string, req OutboundRequest) error { now := time.Now() var notesPtr *string if req.Notes != "" { notesPtr = &req.Notes } var recipientIDPtr, recipientNamePtr, sourceBatchPtr, washingPlanPtr *string if req.RecipientCompanyID != "" { recipientIDPtr = &req.RecipientCompanyID } if req.RecipientCompanyName != "" { recipientNamePtr = &req.RecipientCompanyName } if req.SourceBatchID != "" { sourceBatchPtr = &req.SourceBatchID } if req.WashingPlanID != "" { washingPlanPtr = &req.WashingPlanID } outboundType := req.OutboundType if outboundType == "" { outboundType = "normal" } rec := &model.ProductOutboundRecord{ ID: uuid.New().String(), ProductID: productID, BatchNo: req.BatchNo, Rolls: req.Rolls, Meters: req.Meters, OutboundType: outboundType, Notes: notesPtr, OperatorID: &userID, CompanyID: &companyID, RecipientCompanyID: recipientIDPtr, RecipientCompanyName: recipientNamePtr, SourceBatchID: sourceBatchPtr, WashingPlanID: washingPlanPtr, CreatedAt: now, } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer tx.Rollback() if err := s.repo.Outbound(ctx, tx, rec); err != nil { return err } if err := s.repo.UpdateStock(ctx, tx, productID, -req.Meters, -req.Rolls); err != nil { return err } return tx.Commit() } func (s *ProductService) Records(ctx context.Context, productID string) ([]model.ProductInventoryRecord, error) { return s.repo.Records(ctx, productID) } func (s *ProductService) UpdatePrice(ctx context.Context, productID, companyID, userID string, newPrice float64, reason string) error { oldPrice, err := s.repo.GetPrice(ctx, productID) if err != nil { return err } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer tx.Rollback() if err := s.repo.UpdatePrice(ctx, tx, productID, newPrice); err != nil { return err } if err := s.repo.InsertPriceHistory(ctx, tx, productID, oldPrice, newPrice, reason, userID); err != nil { return err } return tx.Commit() } func (s *ProductService) PriceHistory(ctx context.Context, productID string) ([]model.ProductPriceHistory, error) { return s.repo.PriceHistory(ctx, productID) }