From d1b377ddaccae82cc165a79861d1f7e7fbba69b1 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 15 Oct 2025 11:20:32 +0700 Subject: [PATCH] feat(BE-58,59,60,61): implement stock transfer API, validation, audit log, and schema update - Build stock transfer API with nested details, deliveries, and items - Extend DB schema for stock transfers - Implement validation for transfer request and stock - Prepare/implement transfer audit log structure - Preload all relations for complete response - Update DTOs for nested response - Remove redundant root fields, use relation objects --- ...20250925040409_create_master_tables.up.sql | 2 +- internal/entities/stock-transfer.go | 1 + .../inventory/transfers/dto/transfer.dto.go | 157 ++++++++++++++++-- .../stock_transfer_detail.repository.go | 9 +- .../transfers/services/transfer.service.go | 84 +++++++--- .../validations/transfer.validation.go | 1 - 6 files changed, 210 insertions(+), 44 deletions(-) diff --git a/internal/database/migrations/20250925040409_create_master_tables.up.sql b/internal/database/migrations/20250925040409_create_master_tables.up.sql index 07e3005a..09b1c46e 100644 --- a/internal/database/migrations/20250925040409_create_master_tables.up.sql +++ b/internal/database/migrations/20250925040409_create_master_tables.up.sql @@ -316,7 +316,7 @@ CREATE TABLE stock_logs ( before_quantity NUMERIC(15, 3) NOT NULL, after_quantity NUMERIC(15, 3) NOT NULL, log_type VARCHAR(50) NOT NULL, - log_id BIGINT , + log_id BIGINT, note TEXT, product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE, created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE, diff --git a/internal/entities/stock-transfer.go b/internal/entities/stock-transfer.go index ca615f2d..e003d601 100644 --- a/internal/entities/stock-transfer.go +++ b/internal/entities/stock-transfer.go @@ -19,4 +19,5 @@ type StockTransfer struct { ToWarehouse *Warehouse `gorm:"foreignKey:ToWarehouseId"` Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"` Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"` + CreatedUser *User `gorm:"foreignKey:CreatedBy"` } diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index 5ef74c14..10bc820b 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -4,48 +4,146 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs === type TransferBaseDTO struct { - Id uint64 `json:"id"` - TransferReason string `json:"transfer_reason"` - TransferDate string `json:"transfer_date"` - SourceWarehouseId uint64 `json:"source_warehouse_id"` - DestinationWarehouseId uint64 `json:"destination_warehouse_id"` + Id uint64 `json:"id"` + TransferReason string `json:"transfer_reason"` + TransferDate string `json:"transfer_date"` + SourceWarehouse *WarehouseSimpleDTO `json:"source_warehouse,omitempty"` + DestinationWarehouse *WarehouseSimpleDTO `json:"destination_warehouse,omitempty"` +} + +// Only id and name for warehouse simple view +type WarehouseSimpleDTO struct { + Id uint `json:"id"` + Name string `json:"name"` } type TransferListDTO struct { TransferBaseDTO - CreatedBy uint64 `json:"created_by"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Details []TransferDetailItemDTO `json:"details"` + Deliveries []TransferDeliveryDTO `json:"deliveries"` } type TransferDetailDTO struct { TransferListDTO - // Tambahkan detail produk, deliveries, dsb jika perlu + Details []TransferDetailItemDTO `json:"details"` + Deliveries []TransferDeliveryDTO `json:"deliveries"` +} + +// Detail produk +type TransferDetailItemDTO struct { + Id uint64 `json:"id"` + ProductId uint64 `json:"product_id"` + Quantity float64 `json:"quantity"` + BeforeQuantity float64 `json:"before_quantity"` + AfterQuantity float64 `json:"after_quantity"` + Note string `json:"note"` +} + +// Delivery ekspedisi +type TransferDeliveryDTO struct { + Id uint64 `json:"id"` + SupplierId uint64 `json:"supplier_id"` + 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"` + Note string `json:"note"` + Items []TransferDeliveryItemDTO `json:"items"` +} + +type TransferDeliveryItemDTO struct { + Id uint64 `json:"id"` + StockTransferDetailId uint64 `json:"stock_transfer_detail_id"` + Quantity float64 `json:"quantity"` } // === Mapper Functions === func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO { + var sourceWarehouse *WarehouseSimpleDTO + if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 { + sourceWarehouse = &WarehouseSimpleDTO{ + Id: e.FromWarehouse.Id, + Name: e.FromWarehouse.Name, + } + } + var destinationWarehouse *WarehouseSimpleDTO + if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 { + destinationWarehouse = &WarehouseSimpleDTO{ + Id: e.ToWarehouse.Id, + Name: e.ToWarehouse.Name, + } + } return TransferBaseDTO{ - Id: e.Id, - TransferReason: e.Reason, // atau field lain sesuai entity - TransferDate: e.CreatedAt.Format("2006-01-02"), - SourceWarehouseId: e.FromWarehouseId, - DestinationWarehouseId: e.ToWarehouseId, + Id: e.Id, + TransferReason: e.Reason, + TransferDate: e.CreatedAt.Format("2006-01-02"), + SourceWarehouse: sourceWarehouse, + DestinationWarehouse: destinationWarehouse, } } func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser != nil { + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + // Map details + var details []TransferDetailItemDTO + for _, d := range e.Details { + details = append(details, TransferDetailItemDTO{ + Id: d.Id, + ProductId: d.ProductId, + Quantity: d.Quantity, + BeforeQuantity: d.BeforeQuantity, + AfterQuantity: d.AfterQuantity, + Note: d.Note, + }) + } + // Map deliveries + var deliveries []TransferDeliveryDTO + for _, del := range e.Deliveries { + // Map delivery items + var items []TransferDeliveryItemDTO + for _, item := range del.Items { + items = append(items, TransferDeliveryItemDTO{ + Id: item.Id, + StockTransferDetailId: item.StockTransferDetailId, + Quantity: item.Quantity, + }) + } + deliveries = append(deliveries, TransferDeliveryDTO{ + Id: del.Id, + SupplierId: del.SupplierId, + VehiclePlate: del.VehiclePlate, + DriverName: del.DriverName, + DocumentNumber: del.DocumentNumber, + DocumentPath: del.DocumentPath, + ShippingCostItem: del.ShippingCostItem, + ShippingCostTotal: del.ShippingCostTotal, + Note: del.Note, + Items: items, + }) + } return TransferListDTO{ TransferBaseDTO: ToTransferBaseDTO(e), - CreatedBy: e.CreatedBy, + CreatedUser: createdUser, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, + Details: details, + Deliveries: deliveries, } } @@ -58,7 +156,36 @@ func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO { } func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { + // Map details + var details []TransferDetailItemDTO + for _, d := range e.Details { + details = append(details, TransferDetailItemDTO{ + Id: d.Id, + ProductId: d.ProductId, + Quantity: d.Quantity, + BeforeQuantity: d.BeforeQuantity, + AfterQuantity: d.AfterQuantity, + Note: d.Note, + }) + } + // Map deliveries + var deliveries []TransferDeliveryDTO + for _, del := range e.Deliveries { + deliveries = append(deliveries, TransferDeliveryDTO{ + Id: del.Id, + SupplierId: del.SupplierId, + VehiclePlate: del.VehiclePlate, + DriverName: del.DriverName, + DocumentNumber: del.DocumentNumber, + DocumentPath: del.DocumentPath, + ShippingCostItem: del.ShippingCostItem, + ShippingCostTotal: del.ShippingCostTotal, + Note: del.Note, + }) + } return TransferDetailDTO{ TransferListDTO: ToTransferListDTO(e), + Details: details, + Deliveries: deliveries, } } diff --git a/internal/modules/inventory/transfers/repositories/stock_transfer_detail.repository.go b/internal/modules/inventory/transfers/repositories/stock_transfer_detail.repository.go index 7c8ab63d..fa9afd57 100644 --- a/internal/modules/inventory/transfers/repositories/stock_transfer_detail.repository.go +++ b/internal/modules/inventory/transfers/repositories/stock_transfer_detail.repository.go @@ -1,6 +1,10 @@ +// Find all details by StockTransferId + package repositories import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -8,7 +12,7 @@ import ( type StockTransferDetailRepository interface { repository.BaseRepository[entity.StockTransferDetail] - // Tambahkan custom method jika perlu + FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error } type StockTransferDetailRepositoryImpl struct { @@ -20,3 +24,6 @@ func NewStockTransferDetailRepository(db *gorm.DB) StockTransferDetailRepository BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDetail](db), } } +func (r *StockTransferDetailRepositoryImpl) FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error { + return r.DB().WithContext(ctx).Where("stock_transfer_id = ?", transferId).Find(out).Error +} diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index ddc47c02..80267e0c 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -3,6 +3,7 @@ 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" @@ -46,9 +47,13 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr ProductWarehouseRepo: productWarehouseRepo, } } - func (s transferService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") + return db. + Preload("CreatedUser"). + Preload("FromWarehouse"). + Preload("ToWarehouse"). + Preload("Details"). + Preload("Deliveries.Items") } func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) { @@ -56,26 +61,37 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return nil, 0, err } - // offset := (params.Page - 1) * params.Limit + 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") - // }) + transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = db.Preload("CreatedUser").Preload("FromWarehouse").Preload("ToWarehouse").Preload("Details").Preload("Deliveries.Items") + 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 - // 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 + var transfer entity.StockTransfer + db := s.StockTransferRepo.DB().WithContext(c.Context()) + db = db.Preload("CreatedUser").Preload("FromWarehouse").Preload("ToWarehouse").Preload("Details").Preload("Deliveries.Items") + if err := db.First(&transfer, id).Error; err != nil { + 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") + } + return &transfer, nil } func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) { @@ -160,16 +176,31 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err) return err } - // tambahkan insert ke delivery items sebagai fivot + + detailMap := make(map[uint64]uint64) + for _, d := range details { + detailMap[d.ProductId] = d.Id + } + 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, - }) + + for _, delivery := range deliveries { + for _, item := range req.Deliveries { + if item.Document == delivery.DocumentPath { + 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 { @@ -225,8 +256,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return err } s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id) - } + + } return nil }) diff --git a/internal/modules/inventory/transfers/validations/transfer.validation.go b/internal/modules/inventory/transfers/validations/transfer.validation.go index 05c7215d..778bdef4 100644 --- a/internal/modules/inventory/transfers/validations/transfer.validation.go +++ b/internal/modules/inventory/transfers/validations/transfer.validation.go @@ -25,7 +25,6 @@ type TransferDelivery struct { DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"` Document string `json:"document"` DriverName string `json:"driver_name" validate:"required"` - DeliveryNoteNumber string `json:"delivery_note_number" validate:"required"` VehiclePlate string `json:"vehicle_plate" validate:"required"` SupplierID uint `json:"supplier_id" validate:"required"` Products []TransferDeliveryProduct `json:"products" validate:"required,dive"`