238 lines
5.8 KiB
Go
238 lines
5.8 KiB
Go
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)
|
|
}
|