material_manage_system/internal/service/outbound.go
2026-10-09 10:38:02 +08:00

307 lines
7.5 KiB
Go

package service
import (
"errors"
"time"
"github.com/shopspring/decimal"
"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(organizationID int64, req *model.CreateOutboundRequest, operator string) (*model.OutboundOrder, error) {
order := &model.OutboundOrder{
OutboundDate: time.Now(),
Operator: operator,
Handler: operator,
Receiver: req.Receiver,
Remark: req.Remark,
Items: make([]model.OutboundItem, len(req.Items)),
}
// Validate all items first (outside transaction)
for i, item := range req.Items {
exists, err := s.materialSvc.Exists(organizationID, item.MaterialID)
if err != nil {
return nil, err
}
if !exists {
return nil, ErrMaterialNotFound
}
active, err := s.materialSvc.IsActive(organizationID, item.MaterialID)
if err != nil {
return nil, err
}
if !active {
return nil, ErrMaterialObsolete
}
currentStock, err := s.stockRepo.GetDefaultStock(item.MaterialID)
if err != nil {
return nil, err
}
if currentStock.LessThan(decimal.NewFromInt(int64(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 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.stockRepo.GetStockForUpdateTx(tx, item.MaterialID)
if err != nil {
return nil, err
}
if lockedStock.LessThan(decimal.NewFromInt(int64(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(organizationID, item.MaterialID)
if err != nil {
return nil, err
}
item.MaterialCode = material.MaterialCode
item.MaterialName = material.Name
// Update stock within transaction
err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(-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 tx.Rollback()
// Reverse stock for each item (add back) within transaction
for _, item := range order.Items {
err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(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(organizationID, id int64, req *model.UpdateOutboundRequest, operator string) (*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(organizationID, item.MaterialID)
if err != nil {
return nil, err
}
if !exists {
return nil, ErrMaterialNotFound
}
active, err := s.materialSvc.IsActive(organizationID, item.MaterialID)
if err != nil {
return nil, err
}
if !active {
return nil, ErrMaterialObsolete
}
currentStock, err := s.stockRepo.GetDefaultStock(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.Add(decimal.NewFromInt(int64(oldQty)))
if effectiveStock.LessThan(decimal.NewFromInt(int64(item.Quantity))) {
return nil, ErrInsufficientStock
}
}
// Start transaction
tx, err := repository.BeginTx()
if err != nil {
return nil, err
}
defer tx.Rollback()
// Reverse stock for old items (add back what was previously subtracted)
for _, item := range oldOrder.Items {
err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(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.stockRepo.GetStockForUpdateTx(tx, item.MaterialID)
if err != nil {
return nil, err
}
// Old quantities have already been restored above.
if lockedStock.LessThan(decimal.NewFromInt(int64(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
// 客户端不能指定操作人;编辑也由当前登录用户接管操作人和兼容字段 handler。
req.Handler = operator
req.Operator = operator
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.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(-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
}