mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
387 lines
15 KiB
Go
387 lines
15 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"strings"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sirupsen/logrus"
|
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
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"
|
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
|
|
"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, files []*multipart.FileHeader) (*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
|
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
|
DocumentSvc commonSvc.DocumentService
|
|
}
|
|
|
|
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, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService) TransferService {
|
|
return &transferService{
|
|
Log: utils.Log,
|
|
Validate: validate,
|
|
StockTransferRepo: stockTransferRepo,
|
|
StockTransferDetailRepo: stockTransferDetailRepo,
|
|
StockTransferDeliveryRepo: stockTransferDeliveryRepo,
|
|
StockTransferDeliveryItemRepo: stockTransferDeliveryItemRepo,
|
|
StockLogsRepository: stockLogsRepo,
|
|
ProductWarehouseRepo: productWarehouseRepo,
|
|
SupplierRepo: supplierRepo,
|
|
WarehouseRepo: warehouseRepo,
|
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
|
DocumentSvc: documentSvc,
|
|
}
|
|
}
|
|
|
|
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").
|
|
Preload("Deliveries.Documents", func(db *gorm.DB) *gorm.DB {
|
|
return db.Where("documentable_type = ?", string(utils.DocumentableTypeTransfer))
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return transfers, total, nil
|
|
}
|
|
|
|
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
|
s.Log.Infof("Attempting to get StockTransfer with ID: %d", id)
|
|
|
|
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
|
return s.withRelations(db)
|
|
})
|
|
if err != nil {
|
|
s.Log.Errorf("Error getting StockTransfer ID %d: %+v", id, err)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
|
}
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
|
}
|
|
|
|
if transferPtr != nil {
|
|
s.Log.Infof("StockTransfer %d has %d documents", transferPtr.Id, len(transferPtr.Documents))
|
|
}
|
|
|
|
return transferPtr, nil
|
|
}
|
|
|
|
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) {
|
|
|
|
pwIDs := make([]uint, 0, len(req.Products))
|
|
|
|
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))
|
|
}
|
|
pwIDs = append(pwIDs, sourcePW.Id)
|
|
}
|
|
|
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
|
c.Context(),
|
|
s.StockTransferRepo.DB(),
|
|
pwIDs,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
actorID, err := m.ActorIDFromContext(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
deliveryQtyMap := make(map[uint]float64)
|
|
for _, delivery := range req.Deliveries {
|
|
for _, prod := range delivery.Products {
|
|
deliveryQtyMap[prod.ProductID] += prod.ProductQty
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
|
if err != nil {
|
|
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: uint64(actorID),
|
|
}
|
|
|
|
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
|
|
|
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
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 {
|
|
return err
|
|
}
|
|
|
|
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,
|
|
ShippingCostItem: delivery.DeliveryCostPerItem,
|
|
ShippingCostTotal: delivery.DeliveryCost,
|
|
})
|
|
}
|
|
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
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 {
|
|
return err
|
|
}
|
|
|
|
if s.DocumentSvc != nil && len(files) > 0 {
|
|
|
|
for idx, file := range files {
|
|
documentFiles := []commonSvc.DocumentFile{
|
|
{
|
|
File: file,
|
|
Type: string(utils.DocumentTypeTransfer),
|
|
Index: &idx,
|
|
},
|
|
}
|
|
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
|
DocumentableType: string(utils.DocumentableTypeTransfer),
|
|
DocumentableID: deliveries[idx].Id,
|
|
CreatedBy: &actorID,
|
|
Files: documentFiles,
|
|
})
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d", idx+1))
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, product := range req.Products {
|
|
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
|
|
}
|
|
if sourcePW.Quantity < product.ProductQty {
|
|
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 {
|
|
return err
|
|
}
|
|
|
|
decreaseLog := &entity.StockLog{
|
|
Decrease: product.ProductQty,
|
|
Notes: "",
|
|
LoggableType: string(utils.StockLogTypeTransfer),
|
|
LoggableId: uint(entityTransfer.Id),
|
|
ProductWarehouseId: sourcePW.Id,
|
|
CreatedBy: actorID,
|
|
}
|
|
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
|
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
|
)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
|
|
}
|
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
|
ctx := c.Context()
|
|
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
destPW = &entity.ProductWarehouse{
|
|
ProductId: uint(product.ProductID),
|
|
WarehouseId: uint(req.DestinationWarehouseID),
|
|
Quantity: 0,
|
|
ProjectFlockKandangId: &projectFlockKandangID,
|
|
}
|
|
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create destination product warehouse")
|
|
}
|
|
}
|
|
|
|
destPW.Quantity += product.ProductQty
|
|
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
increaseLog := &entity.StockLog{
|
|
Increase: product.ProductQty,
|
|
LoggableType: string(utils.StockLogTypeTransfer),
|
|
LoggableId: uint(entityTransfer.Id),
|
|
Notes: "",
|
|
ProductWarehouseId: destPW.Id,
|
|
CreatedBy: actorID,
|
|
}
|
|
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
|
|
}
|
|
|
|
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
|
}
|
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
|
}
|
|
|
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
|
}
|
|
|
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
|
}
|
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
|
}
|
|
|
|
return uint(projectFlockKandang.Id), nil
|
|
}
|