Feat[BE]: add document handling to stock transfer process

This commit is contained in:
aguhh18
2025-12-23 14:10:08 +07:00
committed by Hafizh A. Y
parent 306cf11fee
commit 7dc5c9e9a5
6 changed files with 95 additions and 57 deletions
@@ -80,15 +80,14 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
// ambil file
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
_ = form.File["documents"]
// todo: tunggu ada aws baru proses
result, err := u.TransferService.CreateOne(c, &req)
files := form.File["documents"]
result, err := u.TransferService.CreateOne(c, &req, files)
if err != nil {
return err
}
@@ -43,6 +43,14 @@ type SupplierSimpleDTO struct {
Name string `json:"name"`
}
type DocumentDTO struct {
Id uint `json:"id"`
Path string `json:"path"`
Name string `json:"name"`
Ext string `json:"ext"`
Size float64 `json:"size"`
}
type WarehouseDetailDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
@@ -57,6 +65,7 @@ type TransferListDTO struct {
UpdatedAt time.Time `json:"updated_at"`
Details []TransferDetailItemDTO `json:"details"`
Deliveries []TransferDeliveryDTO `json:"deliveries"`
Documents []DocumentDTO `json:"documents"`
}
type TransferDetailDTO struct {
@@ -79,7 +88,6 @@ type TransferDeliveryDTO struct {
VehiclePlate string `json:"vehicle_plate"`
DriverName string `json:"driver_name"`
DocumentNumber string `json:"document_number"`
DocumentPath string `json:"document_path"`
ShippingCostItem float64 `json:"shipping_cost_item"`
ShippingCostTotal float64 `json:"shipping_cost_total"`
Items []TransferDeliveryItemDTO `json:"items"`
@@ -174,6 +182,7 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
Quantity: item.Quantity,
})
}
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
Supplier: SupplierSimpleDTO{
@@ -183,12 +192,22 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
Items: items,
})
}
var documents []DocumentDTO
for _, doc := range e.Documents {
documents = append(documents, DocumentDTO{
Id: doc.Id,
Path: doc.Path,
Name: doc.Name,
Ext: doc.Ext,
Size: doc.Size,
})
}
return TransferListDTO{
TransferRelationDTO: ToTransferRelationDTO(e),
CreatedUser: createdUser,
@@ -196,6 +215,7 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
UpdatedAt: e.UpdatedAt,
Details: details,
Deliveries: deliveries,
Documents: documents,
}
}
@@ -232,7 +252,6 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
})
+11 -1
View File
@@ -1,10 +1,14 @@
package transfers
import (
"context"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
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"
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
@@ -29,8 +33,14 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
userRepo := rUser.NewUserRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db)
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(err)
}
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc)
userService := sUser.NewUserService(userRepo, validate)
TransferRoutes(router, userService, transferService)
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"mime/multipart"
"strings"
"github.com/go-playground/validator/v10"
@@ -27,7 +28,7 @@ import (
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)
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error)
}
type transferService struct {
@@ -42,9 +43,10 @@ type transferService struct {
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) TransferService {
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,
@@ -57,6 +59,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
SupplierRepo: supplierRepo,
WarehouseRepo: warehouseRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
DocumentSvc: documentSvc,
}
}
@@ -72,7 +75,10 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Details").
Preload("Details.Product").
Preload("Deliveries.Items").
Preload("Deliveries.Supplier")
Preload("Deliveries.Supplier").
Preload("Documents", func(db *gorm.DB) *gorm.DB {
return db.Where("documentable_type = ?", "STOCK_TRANSFER")
})
}
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
@@ -94,31 +100,31 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
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
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")
}
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)
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) (*entity.StockTransfer, error) {
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) {
pwIDs := make([]uint, 0, len(req.Products))
@@ -180,7 +186,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
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)
@@ -198,10 +203,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
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 {
s.Log.Errorf("Failed to create stock transfer: %+v", err)
return err
}
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
var details []*entity.StockTransferDetail
for _, product := range req.Products {
@@ -212,10 +215,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
})
}
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)
var deliveries []*entity.StockTransferDelivery
for _, delivery := range req.Deliveries {
@@ -224,13 +225,11 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
SupplierId: uint64(delivery.SupplierID),
VehiclePlate: delivery.VehiclePlate,
DriverName: delivery.DriverName,
DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf",
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
}
@@ -256,27 +255,46 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}
}
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)
actorIDCopy := actorID
if s.DocumentSvc != nil && len(files) > 0 {
s.Log.Infof("Starting document upload for %d files", len(files))
documentFiles := make([]commonSvc.DocumentFile, 0, len(files))
for idx, file := range files {
docIndex := idx
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: "STOCK_TRANSFER_DOCUMENT",
Index: &docIndex,
})
}
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: "STOCK_TRANSFER",
DocumentableID: entityTransfer.Id,
CreatedBy: &actorIDCopy,
Files: documentFiles,
})
if err != nil {
s.Log.Errorf("Failed to upload documents for transfer %d: %+v", entityTransfer.Id, err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload transfer documents")
}
s.Log.Infof("Successfully uploaded documents for transfer ID %d", entityTransfer.Id)
}
for _, product := range req.Products {
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)
decreaseLog := &entity.StockLog{
Decrease: product.ProductQty,
@@ -287,7 +305,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
CreatedBy: actorID,
}
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
}
@@ -295,7 +312,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
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) {
@@ -311,18 +327,14 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
ProjectFlockKandangId: &projectFlockKandangID,
}
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)
increaseLog := &entity.StockLog{
Increase: product.ProductQty,
@@ -333,7 +345,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
CreatedBy: actorID,
}
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
}
}
@@ -343,7 +354,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
}
result, err := s.GetOne(c, uint(entityTransfer.Id))
@@ -359,7 +370,6 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
}
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
}
@@ -372,7 +382,6 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
}
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
}