package service import ( "errors" "github.com/shopspring/decimal" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/internal/repository" ) var ( ErrApplicationNotFound = errors.New("application not found") ErrCannotCancelApplication = errors.New("cannot cancel application") ErrApplicationNotPending = errors.New("application is not pending") ) type ApplicationService struct { appRepo *repository.ApplicationRepository materialSvc *MaterialService stockRepo *repository.StockRepo } func NewApplicationService() *ApplicationService { return &ApplicationService{ appRepo: repository.NewApplicationRepository(), materialSvc: NewMaterialService(), stockRepo: repository.NewStockRepo(), } } func (s *ApplicationService) Create(userID int64, req *model.CreateApplicationRequest) (*model.MaterialApplication, error) { // Build items from request var items []model.ApplicationItem for _, item := range req.Items { exists, err := s.materialSvc.Exists(item.MaterialID) if err != nil || !exists { return nil, ErrMaterialNotFound } active, err := s.materialSvc.IsActive(item.MaterialID) if err != nil { return nil, err } if !active { return nil, ErrMaterialObsolete } items = append(items, model.ApplicationItem{ MaterialID: item.MaterialID, Quantity: item.Quantity, }) } app := &model.MaterialApplication{ UserID: userID, Purpose: req.Purpose, Status: model.ApplicationStatusPending, Items: items, } if err := s.appRepo.Create(app); err != nil { return nil, err } // Get full details return s.appRepo.GetByID(app.ID) } func (s *ApplicationService) GetByID(id int64) (*model.MaterialApplication, error) { app, err := s.appRepo.GetByID(id) if err != nil { return nil, ErrApplicationNotFound } return app, nil } func (s *ApplicationService) ListByUser(userID int64, status string, limit, offset int) ([]model.MaterialApplication, int, error) { apps, total, err := s.appRepo.ListByUserID(userID, status, limit, offset) if err != nil { return nil, 0, err } if apps == nil { apps = []model.MaterialApplication{} } return apps, total, nil } func (s *ApplicationService) ListAll(status string, limit, offset int) ([]model.MaterialApplication, int, error) { apps, total, err := s.appRepo.ListAllApplications(status, limit, offset) if err != nil { return nil, 0, err } if apps == nil { apps = []model.MaterialApplication{} } return apps, total, nil } func (s *ApplicationService) Approve(id int64, approverID int64, autoOutbound bool) error { app, err := s.appRepo.GetByID(id) if err != nil { return ErrApplicationNotFound } if app.Status != model.ApplicationStatusPending { return ErrApplicationNotPending } // Check stock for all items for _, item := range app.Items { stock, err := s.stockRepo.GetStock(item.MaterialID) if err != nil { return ErrMaterialNotFound } if stock.LessThan(decimal.NewFromInt(int64(item.Quantity))) { return ErrInsufficientStock } } // Auto outbound if requested if autoOutbound { // Start transaction tx, err := repository.BeginTx() if err != nil { return err } defer func() { if err != nil { tx.Rollback() } }() // Get approver name for handler approverName, _ := s.appRepo.GetApproverName(approverID) // Create outbound order within transaction order := &model.OutboundOrder{ Handler: approverName, Operator: approverName, Receiver: app.Username, Status: "completed", } if err := s.appRepo.CreateOutboundOrderTx(tx, order); err != nil { return err } // Process each item within transaction for _, appItem := range app.Items { // Re-check stock with row lock lockedStock, lockErr := s.stockRepo.GetStockForUpdateTx(tx, appItem.MaterialID) err = lockErr if err != nil { return ErrMaterialNotFound } if lockedStock.LessThan(decimal.NewFromInt(int64(appItem.Quantity))) { return ErrInsufficientStock } // Deduct stock within transaction if err := s.stockRepo.UpdateStockTx(tx, appItem.MaterialID, decimal.NewFromInt(int64(-appItem.Quantity))); err != nil { return err } // Create outbound item within transaction item := &model.OutboundItem{ OutboundOrderID: order.ID, MaterialID: appItem.MaterialID, Quantity: appItem.Quantity, } if err := s.appRepo.CreateOutboundItemTx(tx, item); err != nil { return err } // Create stock log within transaction stockLog := &model.StockLog{ MaterialID: appItem.MaterialID, Type: "out", Quantity: appItem.Quantity, RelatedOrderType: "application", RelatedOrderID: id, } if err := s.appRepo.CreateStockLogTx(tx, stockLog); err != nil { return err } } // Update status to approved within transaction if err := s.appRepo.ApproveTx(tx, id, approverID); err != nil { return err } // Commit transaction if err = tx.Commit(); err != nil { return err } } else { // Non-auto-outbound: just approve if err := s.appRepo.Approve(id, approverID); err != nil { return err } } return nil } func (s *ApplicationService) Reject(id int64, reason string) error { app, err := s.appRepo.GetByID(id) if err != nil { return ErrApplicationNotFound } if app.Status != model.ApplicationStatusPending { return ErrApplicationNotPending } return s.appRepo.Reject(id, reason) } func (s *ApplicationService) Cancel(id int64, userID int64) error { app, err := s.appRepo.GetByID(id) if err != nil { return ErrApplicationNotFound } // User can only cancel their own applications if app.UserID != userID { return errors.New("cannot cancel others' applications") } if app.Status != model.ApplicationStatusPending { return ErrCannotCancelApplication } return s.appRepo.Cancel(id) } func (s *ApplicationService) GetMaterialStock(materialID int64) (decimal.Decimal, error) { return s.stockRepo.GetStock(materialID) }