From 7dc5c9e9a5b373ef2b16da5ba425c58aaf4cc13c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 23 Dec 2025 14:10:08 +0700 Subject: [PATCH] Feat[BE]: add document handling to stock transfer process --- internal/entities/stock-transfer.go | 1 + internal/entities/stock_transfer_delivery.go | 34 ++++----- .../controllers/transfer.controller.go | 7 +- .../inventory/transfers/dto/transfer.dto.go | 25 ++++++- .../modules/inventory/transfers/module.go | 12 ++- .../transfers/services/transfer.service.go | 73 +++++++++++-------- 6 files changed, 95 insertions(+), 57 deletions(-) diff --git a/internal/entities/stock-transfer.go b/internal/entities/stock-transfer.go index e003d601..7da7a9f5 100644 --- a/internal/entities/stock-transfer.go +++ b/internal/entities/stock-transfer.go @@ -20,4 +20,5 @@ type StockTransfer struct { Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"` Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"` CreatedUser *User `gorm:"foreignKey:CreatedBy"` + Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` } diff --git a/internal/entities/stock_transfer_delivery.go b/internal/entities/stock_transfer_delivery.go index 3a7562ea..69324b65 100644 --- a/internal/entities/stock_transfer_delivery.go +++ b/internal/entities/stock_transfer_delivery.go @@ -4,20 +4,20 @@ import "time" // DETAIL EKSPEDISI type StockTransferDelivery struct { - Id uint64 `gorm:"primaryKey;autoIncrement"` - StockTransferId uint64 - SupplierId uint64 - VehiclePlate string - DriverName string - DocumentNumber string - DocumentPath string - ShippingCostItem float64 - ShippingCostTotal float64 - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time `gorm:"index"` - // Relations - StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"` - Supplier *Supplier `gorm:"foreignKey:SupplierId"` - Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"` -} \ No newline at end of file + Id uint64 `gorm:"primaryKey;autoIncrement"` + StockTransferId uint64 + SupplierId uint64 + VehiclePlate string + DriverName string + DocumentNumber string + DocumentPath string + ShippingCostItem float64 + ShippingCostTotal float64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time `gorm:"index"` + // Relations + StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"` + Supplier *Supplier `gorm:"foreignKey:SupplierId"` + Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"` +} diff --git a/internal/modules/inventory/transfers/controllers/transfer.controller.go b/internal/modules/inventory/transfers/controllers/transfer.controller.go index b53d6e9a..c21e5286 100644 --- a/internal/modules/inventory/transfers/controllers/transfer.controller.go +++ b/internal/modules/inventory/transfers/controllers/transfer.controller.go @@ -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 } diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index fe97ce0f..d38fb78d 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -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, }) diff --git a/internal/modules/inventory/transfers/module.go b/internal/modules/inventory/transfers/module.go index 19a0ded6..9389f9f4 100644 --- a/internal/modules/inventory/transfers/module.go +++ b/internal/modules/inventory/transfers/module.go @@ -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) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index f94295f6..33ca77ff 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -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") }