326 lines
7.7 KiB
Go
326 lines
7.7 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(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(item.MaterialID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
return nil, ErrMaterialNotFound
|
|
}
|
|
active, err := s.materialSvc.IsActive(item.MaterialID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !active {
|
|
return nil, ErrMaterialObsolete
|
|
}
|
|
|
|
currentStock, err := s.stockRepo.GetStock(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 func() {
|
|
if err != nil {
|
|
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(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 func() {
|
|
if err != nil {
|
|
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(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(item.MaterialID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
return nil, ErrMaterialNotFound
|
|
}
|
|
active, err := s.materialSvc.IsActive(item.MaterialID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !active {
|
|
return nil, ErrMaterialObsolete
|
|
}
|
|
|
|
currentStock, err := s.stockRepo.GetStock(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 func() {
|
|
if err != nil {
|
|
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
|
|
}
|
|
oldQty := 0
|
|
for _, oldItem := range oldOrder.Items {
|
|
if oldItem.MaterialID == item.MaterialID {
|
|
oldQty = oldItem.Quantity
|
|
break
|
|
}
|
|
}
|
|
effectiveStock := lockedStock.Add(decimal.NewFromInt(int64(oldQty)))
|
|
if effectiveStock.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
|
|
}
|