package service import ( "errors" "time" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/internal/repository" ) var ErrInsufficientStock = errors.New("insufficient stock") type OutboundService struct { outboundRepo *repository.OutboundRepo materialSvc *MaterialService stockRepo *repository.StockRepo } func NewOutboundService() *OutboundService { return &OutboundService{ outboundRepo: repository.NewOutboundRepo(), materialSvc: NewMaterialService(), stockRepo: repository.NewStockRepo(), } } func (s *OutboundService) Create(req *model.CreateOutboundRequest) (*model.OutboundOrder, error) { order := &model.OutboundOrder{ OutboundDate: time.Now(), Handler: req.Handler, Receiver: req.Receiver, Items: make([]model.OutboundItem, len(req.Items)), } // Validate all items first (outside transaction) for i, item := range req.Items { exists, err := s.materialSvc.Exists(item.MaterialID) if err != nil { return nil, err } if !exists { return nil, ErrMaterialNotFound } currentStock, err := s.materialSvc.GetStock(item.MaterialID) if err != nil { return nil, err } if currentStock < item.Quantity { return nil, ErrInsufficientStock } order.Items[i] = model.OutboundItem{ MaterialID: item.MaterialID, Quantity: item.Quantity, } } // Start transaction for stock update (prevents race condition) tx, err := repository.BeginTx() if err != nil { return nil, err } defer func() { if err != nil { tx.Rollback() } }() // Re-check stock with row lock (SELECT FOR UPDATE) to prevent overselling for i := range order.Items { item := &order.Items[i] lockedStock, err := s.materialSvc.GetStockForUpdateTx(tx, item.MaterialID) if err != nil { return nil, err } if lockedStock < item.Quantity { err = ErrInsufficientStock return nil, err } } // Create outbound order err = s.outboundRepo.CreateTx(tx, order) if err != nil { return nil, err } // Update stock and create logs within transaction for i := range order.Items { item := &order.Items[i] material, err := s.materialSvc.GetByID(item.MaterialID) if err != nil { return nil, err } item.MaterialCode = material.MaterialCode item.MaterialName = material.Name // Update stock within transaction err = s.materialSvc.UpdateStockTx(tx, item.MaterialID, -item.Quantity) if err != nil { return nil, err } // Create stock log within transaction stockLog := &model.StockLog{ MaterialID: item.MaterialID, Type: "out", Quantity: item.Quantity, RelatedOrderType: "outbound", RelatedOrderID: order.ID, } err = s.stockRepo.CreateLogTx(tx, stockLog) if err != nil { return nil, err } } // Commit transaction if err = tx.Commit(); err != nil { return nil, err } order.Status = "completed" return order, nil } func (s *OutboundService) GetByID(id int64) (*model.OutboundOrder, error) { return s.outboundRepo.GetByID(id) } func (s *OutboundService) List(search string, limit, offset int) ([]model.OutboundOrder, int64, error) { return s.outboundRepo.List(search, limit, offset) } func (s *OutboundService) GetByMaterialID(materialID int64) ([]model.OutboundRecord, error) { return s.outboundRepo.GetByMaterialID(materialID) } func (s *OutboundService) Delete(id int64) error { // Get the outbound order with items order, err := s.outboundRepo.GetByID(id) if err != nil { return err } // Start transaction tx, err := repository.BeginTx() if err != nil { return err } defer func() { if err != nil { tx.Rollback() } }() // Reverse stock for each item (add back) within transaction for _, item := range order.Items { err = s.materialSvc.UpdateStockTx(tx, item.MaterialID, item.Quantity) if err != nil { return err } } // Delete stock logs within transaction err = s.stockRepo.DeleteLogsTx(tx, "outbound", id) if err != nil { return err } // Delete the outbound order (items will be cascade deleted) err = s.outboundRepo.DeleteTx(tx, id) if err != nil { return err } // Commit transaction if err = tx.Commit(); err != nil { return err } return nil } func (s *OutboundService) Update(id int64, req *model.UpdateOutboundRequest) (*model.OutboundOrder, error) { // Get old order data oldOrder, err := s.outboundRepo.GetByID(id) if err != nil { return nil, err } // Validate all new items first (outside transaction) for _, item := range req.Items { exists, err := s.materialSvc.Exists(item.MaterialID) if err != nil { return nil, err } if !exists { return nil, ErrMaterialNotFound } currentStock, err := s.materialSvc.GetStock(item.MaterialID) if err != nil { return nil, err } // Calculate effective stock: current + old outbound quantity - new outbound quantity oldQty := 0 for _, oldItem := range oldOrder.Items { if oldItem.MaterialID == item.MaterialID { oldQty = oldItem.Quantity break } } effectiveStock := currentStock + oldQty if effectiveStock < item.Quantity { return nil, ErrInsufficientStock } } // Start transaction tx, err := repository.BeginTx() if err != nil { return nil, err } defer func() { if err != nil { tx.Rollback() } }() // Reverse stock for old items (add back what was previously subtracted) for _, item := range oldOrder.Items { err = s.materialSvc.UpdateStockTx(tx, item.MaterialID, item.Quantity) if err != nil { return nil, err } } // Re-check stock with row lock (SELECT FOR UPDATE) to prevent overselling for _, item := range req.Items { lockedStock, err := s.materialSvc.GetStockForUpdateTx(tx, item.MaterialID) if err != nil { return nil, err } oldQty := 0 for _, oldItem := range oldOrder.Items { if oldItem.MaterialID == item.MaterialID { oldQty = oldItem.Quantity break } } effectiveStock := lockedStock + oldQty if effectiveStock < item.Quantity { err = ErrInsufficientStock return nil, err } } // Delete old stock logs err = s.stockRepo.DeleteLogsTx(tx, "outbound", id) if err != nil { return nil, err } // Update order in repository err = s.outboundRepo.UpdateTx(tx, id, req) if err != nil { return nil, err } // Deduct stock for new items and create logs for _, item := range req.Items { err = s.materialSvc.UpdateStockTx(tx, item.MaterialID, -item.Quantity) if err != nil { return nil, err } stockLog := &model.StockLog{ MaterialID: item.MaterialID, Type: "out", Quantity: item.Quantity, RelatedOrderType: "outbound", RelatedOrderID: id, } err = s.stockRepo.CreateLogTx(tx, stockLog) if err != nil { return nil, err } } // Commit transaction if err = tx.Commit(); err != nil { return nil, err } // Get updated order with material details updatedOrder, err := s.outboundRepo.GetByID(id) if err != nil { return nil, err } updatedOrder.Status = "completed" return updatedOrder, nil }