Files
lti-api/internal/modules/inventory/transfers/services/transfer.service.go
T
aguhh18 9b016dc30a (BE-58,,59): extend db schema & build stock transfer api
- Extend DB schema for stock transfers
- Build stock transfer API (create,)
2025-10-14 22:16:50 +07:00

241 lines
10 KiB
Go

package service
import (
"errors"
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type TransferService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error)
}
type transferService struct {
Log *logrus.Logger
Validate *validator.Validate
StockTransferRepo rStockTransfer.StockTransferRepository
StockTransferDetailRepo rStockTransfer.StockTransferDetailRepository
StockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository
StockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository
StockLogsRepository rStockLogs.StockLogRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
}
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository) TransferService {
return &transferService{
Log: utils.Log,
Validate: validate,
StockTransferRepo: stockTransferRepo,
StockTransferDetailRepo: stockTransferDetailRepo,
StockTransferDeliveryRepo: stockTransferDeliveryRepo,
StockTransferDeliveryItemRepo: stockTransferDeliveryItemRepo,
StockLogsRepository: stockLogsRepo,
ProductWarehouseRepo: productWarehouseRepo,
}
}
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
// offset := (params.Page - 1) * params.Limit
// transfers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
// db = s.withRelations(db)
// if params.Search != "" {
// return db.Where("name LIKE ?", "%"+params.Search+"%")
// }
// return db.Order("created_at DESC").Order("updated_at DESC")
// })
// if err != nil {
// s.Log.Errorf("Failed to get transfers: %+v", err)
// return nil, 0, err
// }
return nil, 0, nil
}
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
return nil, nil
}
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
// Validasi stok di gudang asal
for _, product := range req.Products {
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
}
if sourcePW.Quantity < product.ProductQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
}
}
// Generate movement number
// Format: PND-MBU-00001
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
if err != nil {
s.Log.Errorf("Failed to get next movement number: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
}
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
transferDate, _ := utils.ParseDateString(req.TransferDate)
entityTransfer := &entity.StockTransfer{
FromWarehouseId: uint64(req.SourceWarehouseID),
ToWarehouseId: uint64(req.DestinationWarehouseID),
Reason: req.TransferReason,
TransferDate: transferDate,
MovementNumber: movementNumber,
CreatedBy: 1, //todo: get from token
}
// Save the transfer entity to the database
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
// Insert header
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer: %+v", err)
return err
}
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
// insert ke details
var details []*entity.StockTransferDetail
for _, product := range req.Products {
details = append(details, &entity.StockTransferDetail{
StockTransferId: entityTransfer.Id,
ProductId: uint64(product.ProductID),
Quantity: product.ProductQty,
})
}
if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer details: %+v", err)
return err
}
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
// Tambahkan proses insert delivery
var deliveries []*entity.StockTransferDelivery
for _, delivery := range req.Deliveries {
deliveries = append(deliveries, &entity.StockTransferDelivery{
StockTransferId: entityTransfer.Id,
SupplierId: uint64(delivery.SupplierID),
VehiclePlate: delivery.VehiclePlate,
DriverName: delivery.DriverName,
DocumentPath: delivery.Document,
ShippingCostItem: delivery.DeliveryCostPerItem,
ShippingCostTotal: delivery.DeliveryCost,
})
}
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
return err
}
// tambahkan insert ke delivery items sebagai fivot
var deliveryItems []*entity.StockTransferDeliveryItem
for i, delivery := range req.Deliveries {
for _, item := range delivery.Products {
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
StockTransferDeliveryId: deliveries[i].Id,
StockTransferDetailId: uint64(item.ProductID),
Quantity: item.ProductQty,
})
}
}
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer delivery items: %+v", err)
return err
}
s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id)
// Proses pengurangan stok di gudang asal dan penambahan stok di gudang tujuan
for _, product := range req.Products {
// Kurangi stok di gudang asal
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
if err != nil {
s.Log.Errorf("Failed to get source product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
}
if sourcePW.Quantity < product.ProductQty {
s.Log.Errorf("Insufficient stock in source warehouse for product ID: %+v", product.ProductID)
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock in source warehouse for product ID: %d", product.ProductID))
}
sourcePW.Quantity -= product.ProductQty
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), sourcePW.Id, sourcePW, nil); err != nil {
s.Log.Errorf("Failed to update source product warehouse: %+v", err)
return err
}
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
// Tambah stok di gudang tujuan
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to get destination product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
}
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
// Jika belum ada record untuk produk di gudang tujuan, buat baru
destPW = &entity.ProductWarehouse{
ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0,
CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create destination product warehouse")
}
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
}
destPW.Quantity += product.ProductQty
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
s.Log.Errorf("Failed to update destination product warehouse: %+v", err)
return err
}
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
}
return nil
})
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
}
return entityTransfer, nil
}