material_manage_system/internal/service/outbound.go
2026-09-15 16:08:18 +08:00

306 lines
6.8 KiB
Go

package service
import (
"errors"
"time"
"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) (*model.OutboundOrder, error) {
order := &model.OutboundOrder{
OutboundDate: time.Now(),
Handler: req.Handler,
Receiver: req.Receiver,
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
}
currentStock, err := s.materialSvc.GetStock(item.MaterialID)
if err != nil {
return nil, err
}
if currentStock < 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.materialSvc.GetStockForUpdateTx(tx, item.MaterialID)
if err != nil {
return nil, err
}
if lockedStock < 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.materialSvc.UpdateStockTx(tx, item.MaterialID, -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.materialSvc.UpdateStockTx(tx, item.MaterialID, 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) (*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
}
currentStock, err := s.materialSvc.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 + oldQty
if effectiveStock < 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.materialSvc.UpdateStockTx(tx, item.MaterialID, 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.materialSvc.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 + oldQty
if effectiveStock < 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
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.materialSvc.UpdateStockTx(tx, item.MaterialID, -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
}