mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
352 lines
14 KiB
Go
352 lines
14 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
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"
|
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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
|
|
SupplierRepo rSupplier.SupplierRepository
|
|
}
|
|
|
|
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository) TransferService {
|
|
return &transferService{
|
|
Log: utils.Log,
|
|
Validate: validate,
|
|
StockTransferRepo: stockTransferRepo,
|
|
StockTransferDetailRepo: stockTransferDetailRepo,
|
|
StockTransferDeliveryRepo: stockTransferDeliveryRepo,
|
|
StockTransferDeliveryItemRepo: stockTransferDeliveryItemRepo,
|
|
StockLogsRepository: stockLogsRepo,
|
|
ProductWarehouseRepo: productWarehouseRepo,
|
|
SupplierRepo: supplierRepo,
|
|
}
|
|
}
|
|
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
|
return db.
|
|
Preload("CreatedUser").
|
|
Preload("FromWarehouse").
|
|
Preload("FromWarehouse.Location").
|
|
Preload("FromWarehouse.Area").
|
|
Preload("ToWarehouse").
|
|
Preload("ToWarehouse.Location").
|
|
Preload("ToWarehouse.Area").
|
|
Preload("Details").
|
|
Preload("Details.Product").
|
|
Preload("Deliveries.Items").
|
|
Preload("Deliveries.Supplier")
|
|
}
|
|
|
|
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.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
|
db = s.withRelations(db)
|
|
if params.Search != "" {
|
|
db = db.Where("movement_number LIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
|
}
|
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
s.Log.Infof("Retrieved %d transfers", len(transfers))
|
|
|
|
return transfers, total, nil
|
|
|
|
}
|
|
|
|
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
|
var transfer entity.StockTransfer
|
|
|
|
// gunakan repo secara langsung
|
|
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
|
return s.withRelations(db)
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
|
}
|
|
s.Log.Errorf("Failed to get transfer by ID: %+v", err)
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
|
}
|
|
|
|
s.Log.Infof("Retrieved transfer: %+v", transfer)
|
|
|
|
return transferPtr, nil
|
|
}
|
|
|
|
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
|
|
|
// Validasi stok di gudang asal harus exist dan mencukupi
|
|
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))
|
|
}
|
|
}
|
|
|
|
// validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid
|
|
deliveryQtyMap := make(map[uint]float64)
|
|
for _, delivery := range req.Deliveries {
|
|
for _, prod := range delivery.Products {
|
|
deliveryQtyMap[prod.ProductID] += prod.ProductQty
|
|
}
|
|
}
|
|
|
|
// Cek: qty delivery tidak boleh melebihi qty di root
|
|
for _, product := range req.Products {
|
|
if deliveryQtyMap[product.ProductID] > product.ProductQty {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest,
|
|
fmt.Sprintf("Total qty delivery untuk produk %d (%v) melebihi qty transfer (%v)", product.ProductID, deliveryQtyMap[product.ProductID], product.ProductQty))
|
|
}
|
|
}
|
|
|
|
// cek suplier id caegory BOP cek by id
|
|
for _, delivery := range req.Deliveries {
|
|
supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
|
|
}
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
|
|
}
|
|
if supplier.Category != "BOP" {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
|
|
}
|
|
}
|
|
|
|
// 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: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf", // todo: tunggu ada aws baru proses
|
|
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 pivot
|
|
detailMap := make(map[uint64]uint64)
|
|
for _, d := range details {
|
|
detailMap[d.ProductId] = d.Id
|
|
}
|
|
|
|
var deliveryItems []*entity.StockTransferDeliveryItem
|
|
|
|
for i, delivery := range deliveries {
|
|
item := req.Deliveries[i]
|
|
for _, prod := range item.Products {
|
|
detailID, ok := detailMap[uint64(prod.ProductID)]
|
|
if !ok {
|
|
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
|
}
|
|
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
|
StockTransferDeliveryId: delivery.Id,
|
|
StockTransferDetailId: detailID,
|
|
Quantity: prod.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)
|
|
|
|
// create stock log for decrease (source)
|
|
beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
|
|
decreaseLog := &entity.StockLog{
|
|
TransactionType: entity.TransactionTypeDecrease,
|
|
Quantity: product.ProductQty,
|
|
BeforeQuantity: beforeQty,
|
|
AfterQuantity: sourcePW.Quantity,
|
|
LogType: entity.LogTypeTransfer,
|
|
LogId: uint(entityTransfer.Id),
|
|
Note: "",
|
|
ProductWarehouseId: sourcePW.Id,
|
|
CreatedBy: 1,
|
|
}
|
|
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
|
|
s.Log.Errorf("Failed to create stock log decrease: %+v", err)
|
|
return err
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
// Update stok di gudang tujuan
|
|
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)
|
|
|
|
// create stock log for increase (destination)
|
|
beforeDestQty := destPW.Quantity - product.ProductQty
|
|
increaseLog := &entity.StockLog{
|
|
TransactionType: entity.TransactionTypeIncrease,
|
|
Quantity: product.ProductQty,
|
|
BeforeQuantity: beforeDestQty,
|
|
AfterQuantity: destPW.Quantity,
|
|
LogType: entity.LogTypeTransfer,
|
|
LogId: uint(entityTransfer.Id),
|
|
Note: "",
|
|
ProductWarehouseId: destPW.Id,
|
|
CreatedBy: 1,
|
|
}
|
|
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
|
|
s.Log.Errorf("Failed to create stock log increase: %+v", err)
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
// Ambil data lengkap hasil create dengan GetOne (agar preload relasi sama dengan GetOne)
|
|
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|