mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 15:25:43 +00:00
feat(BE-59,60,61): build stock transfer API with validation and audit log
- Implement CreateOne for stock transfer with multi-delivery and validation - Preload warehouse, location, and area relations in transfer response - Add audit log for transfer - Improve transaction handling and error management
This commit is contained in:
@@ -51,7 +51,11 @@ 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("Deliveries.Items")
|
||||
}
|
||||
@@ -64,7 +68,7 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
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")
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
db = db.Where("movement_number LIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
||||
}
|
||||
@@ -83,8 +87,10 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
||||
|
||||
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
||||
var transfer entity.StockTransfer
|
||||
|
||||
db := s.StockTransferRepo.DB().WithContext(c.Context())
|
||||
db = db.Preload("CreatedUser").Preload("FromWarehouse").Preload("ToWarehouse").Preload("Details").Preload("Deliveries.Items")
|
||||
db = s.withRelations(db)
|
||||
|
||||
if err := db.First(&transfer, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
||||
@@ -95,11 +101,8 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
||||
}
|
||||
|
||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validasi stok di gudang asal
|
||||
// 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),
|
||||
@@ -115,6 +118,22 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
// Generate movement number
|
||||
// Format: PND-MBU-00001
|
||||
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
||||
@@ -167,7 +186,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
SupplierId: uint64(delivery.SupplierID),
|
||||
VehiclePlate: delivery.VehiclePlate,
|
||||
DriverName: delivery.DriverName,
|
||||
DocumentPath: delivery.Document,
|
||||
DocumentPath: "dummy duls", // todo: tunggu ada aws baru proses
|
||||
ShippingCostItem: delivery.DeliveryCostPerItem,
|
||||
ShippingCostTotal: delivery.DeliveryCost,
|
||||
})
|
||||
@@ -176,8 +195,7 @@ 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
|
||||
|
||||
// tambahkan insert ke delivery items sebagai pivot
|
||||
detailMap := make(map[uint64]uint64)
|
||||
for _, d := range details {
|
||||
detailMap[d.ProductId] = d.Id
|
||||
@@ -185,22 +203,18 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
|
||||
var deliveryItems []*entity.StockTransferDeliveryItem
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
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 {
|
||||
@@ -250,6 +264,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
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)
|
||||
@@ -257,9 +272,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -268,5 +283,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
|
||||
}
|
||||
|
||||
return entityTransfer, nil
|
||||
// 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user