243 lines
6.2 KiB
Go
243 lines
6.2 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
|
||
}
|
||
// 这里不能靠 err 判断是否回滚:事务内每个 if 里的 err 都是 := 声明的局部变量,
|
||
// 闭包捕获到的外层 err 始终为 nil,任何一步失败都不会回滚,事务和它持有的行锁
|
||
// 会一直挂在连接上不释放。改用标志位,只要没走到提交就一定回滚。
|
||
committed := false
|
||
defer func() {
|
||
if !committed {
|
||
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
|
||
}
|
||
committed = true
|
||
} 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)
|
||
}
|