package service import ( "database/sql" "github.com/shopspring/decimal" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/internal/repository" ) type StockService struct { stockRepo *repository.StockRepo } func NewStockService() *StockService { return &StockService{ stockRepo: repository.NewStockRepo(), } } // ClassifyStock 与 repository.stockWarningHavingSQL 是同一套规则,后者负责筛出预警列表, // 两者必须保持一致:只要预警处于启用状态、并且现存量已经触达阈值就是 low。阈值为 0 是 // 合法配置("库存降到 0 时预警"),不再被排除。 // // 没配过规则和手动关闭预警的物料都由 warningEnabled=false 表达,因此在这里自然落到 // normal——那是操作者自己选择不被告知,不是告警。 func ClassifyStock(totalOnHand, warningStock decimal.Decimal, warningEnabled bool) string { if !warningEnabled { return model.StockStatusNormal } if totalOnHand.LessThanOrEqual(warningStock) { return model.StockStatusLow } return model.StockStatusNormal } func classifyAll(stocks []model.Stock) { for i := range stocks { stocks[i].StockStatus = ClassifyStock(stocks[i].TotalOnHand, stocks[i].WarningStock, stocks[i].WarningEnabled) } } func (s *StockService) GetAllStock(organizationID int64, search, category string, limit, offset int) ([]model.Stock, int64, error) { stocks, total, err := s.stockRepo.GetAllStock(organizationID, search, category, limit, offset) if err != nil { return nil, 0, err } classifyAll(stocks) return stocks, total, nil } func (s *StockService) GetByMaterialID(organizationID, materialID int64) (*model.Stock, error) { stock, err := s.stockRepo.GetByMaterialID(organizationID, materialID) if err != nil { return nil, err } stock.StockStatus = ClassifyStock(stock.TotalOnHand, stock.WarningStock, stock.WarningEnabled) return stock, nil } func (s *StockService) GetWarningStock(organizationID int64, limit, offset int) ([]model.Stock, int64, error) { stocks, total, err := s.stockRepo.GetWarningStock(organizationID, limit, offset) if err != nil { return nil, 0, err } classifyAll(stocks) return stocks, total, nil } // UpdateWarningStock 是改变库存域配置的写操作,维度由调用方按当前公司解析。 func (s *StockService) UpdateWarningStock(scope model.InventoryScope, materialID int64, minimumAvailable decimal.Decimal, enabled bool) error { return s.stockRepo.UpdateWarningStock(scope, materialID, minimumAvailable, enabled) } // signedChangeQuantity keeps the API contract consistent: inbound changes are // positive and outbound changes are negative. The type still identifies the // business operation for display and filtering. func signedChangeQuantity(logType string, quantity decimal.Decimal) decimal.Decimal { if logType == "in" { return quantity } return quantity.Neg() } func (s *StockService) GetLogsByMaterialID(organizationID, materialID int64, limit, offset int) ([]model.StockLogResponse, int64, error) { logs, total, err := s.stockRepo.GetLogsByMaterialID(organizationID, materialID, limit, offset) if err != nil { return nil, 0, err } var responses []model.StockLogResponse balance := decimal.Zero for _, log := range logs { quantity := decimal.NewFromInt(int64(log.Quantity)) if log.Type == "in" { balance = balance.Add(quantity) } else { balance = balance.Sub(quantity) } changeType := "outbound" if log.Type == "in" { changeType = "inbound" } beforeQuantity := balance.Sub(quantity) if log.Type != "in" { beforeQuantity = balance.Add(quantity) } responses = append(responses, model.StockLogResponse{ ID: log.ID, MaterialID: log.MaterialID, ChangeType: changeType, ChangeQuantity: signedChangeQuantity(log.Type, quantity), BeforeQuantity: beforeQuantity, AfterQuantity: balance, ReferenceID: log.RelatedOrderID, ReferenceType: log.RelatedOrderType, CreatedAt: log.CreatedAt, }) } return responses, total, nil } func (s *StockService) GetAllLogs(organizationID int64, limit, offset int) ([]model.StockLogResponse, int64, error) { logs, total, err := s.stockRepo.GetAllLogsWithBalance(organizationID, limit, offset) if err != nil { return nil, 0, err } var responses []model.StockLogResponse for _, log := range logs { changeType := "outbound" if log.Type == "in" { changeType = "inbound" } quantity := decimal.NewFromInt(int64(log.Quantity)) beforeQuantity := log.RunningBalance.Sub(quantity) if log.Type != "in" { beforeQuantity = log.RunningBalance.Add(quantity) } responses = append(responses, model.StockLogResponse{ ID: log.ID, MaterialID: log.MaterialID, ChangeType: changeType, ChangeQuantity: signedChangeQuantity(log.Type, quantity), BeforeQuantity: beforeQuantity, AfterQuantity: log.RunningBalance, ReferenceID: log.RelatedOrderID, ReferenceType: log.RelatedOrderType, CreatedAt: log.CreatedAt, }) } return responses, total, nil } func (s *StockService) MaterialExists(organizationID, materialID int64) (bool, error) { _, err := s.stockRepo.GetByMaterialID(organizationID, materialID) if err == sql.ErrNoRows { return false, nil } if err != nil { return false, err } return true, nil } func (s *StockService) GetInventoryBalanceDetails(organizationID, materialID int64) ([]model.InventoryBalanceDetail, error) { return s.stockRepo.GetInventoryBalanceDetails(organizationID, materialID) }