material_manage_system/internal/service/application.go
2026-09-20 14:29:41 +08:00

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)
}