mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-25 07:45:44 +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:
@@ -9,7 +9,6 @@ CREATE TABLE IF NOT EXISTS stock_transfer_details (
|
|||||||
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0),
|
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0),
|
||||||
before_quantity NUMERIC(15, 3),
|
before_quantity NUMERIC(15, 3),
|
||||||
after_quantity NUMERIC(15, 3),
|
after_quantity NUMERIC(15, 3),
|
||||||
note TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ CREATE TABLE IF NOT EXISTS stock_transfer_deliveries (
|
|||||||
document_path TEXT,
|
document_path TEXT,
|
||||||
shipping_cost_item NUMERIC(15,3),
|
shipping_cost_item NUMERIC(15,3),
|
||||||
shipping_cost_total NUMERIC(15,3),
|
shipping_cost_total NUMERIC(15,3),
|
||||||
note TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ func Run(db *gorm.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := seedTransferStock(tx, adminID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("✅ Master data seeding completed")
|
fmt.Println("✅ Master data seeding completed")
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -775,7 +779,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
|||||||
}{
|
}{
|
||||||
{ProductID: 1, WarehouseID: 1, Quantity: 100},
|
{ProductID: 1, WarehouseID: 1, Quantity: 100},
|
||||||
{ProductID: 2, WarehouseID: 2, Quantity: 200},
|
{ProductID: 2, WarehouseID: 2, Quantity: 200},
|
||||||
{ProductID: 1, WarehouseID: 1, Quantity: 300},
|
{ProductID: 2, WarehouseID: 1, Quantity: 300},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, seed := range seeds {
|
for _, seed := range seeds {
|
||||||
@@ -799,6 +803,84 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func seedTransferStock(tx *gorm.DB, createdBy uint) error {
|
||||||
|
// Seeder Transfer Stock
|
||||||
|
// 1. Insert StockTransfer (header)
|
||||||
|
transfer := entity.StockTransfer{
|
||||||
|
FromWarehouseId: 1,
|
||||||
|
ToWarehouseId: 2,
|
||||||
|
Reason: "Seed transfer stock",
|
||||||
|
TransferDate: time.Now(),
|
||||||
|
MovementNumber: "SEED-TRF-00001",
|
||||||
|
CreatedBy: 1,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&transfer).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Insert StockTransferDetail (detail)
|
||||||
|
details := []entity.StockTransferDetail{
|
||||||
|
{
|
||||||
|
StockTransferId: transfer.Id,
|
||||||
|
ProductId: 1,
|
||||||
|
Quantity: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StockTransferId: transfer.Id,
|
||||||
|
ProductId: 2,
|
||||||
|
Quantity: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := range details {
|
||||||
|
if err := tx.Create(&details[i]).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Insert StockTransferDelivery (delivery)
|
||||||
|
deliveries := []entity.StockTransferDelivery{
|
||||||
|
{
|
||||||
|
StockTransferId: transfer.Id,
|
||||||
|
SupplierId: 1,
|
||||||
|
VehiclePlate: "B 1234 XYZ",
|
||||||
|
DriverName: "Driver Seed",
|
||||||
|
DocumentPath: "seed.pdf",
|
||||||
|
ShippingCostItem: 1000,
|
||||||
|
ShippingCostTotal: 2000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := range deliveries {
|
||||||
|
if err := tx.Create(&deliveries[i]).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detailMap := make(map[uint64]uint64)
|
||||||
|
for _, d := range details {
|
||||||
|
detailMap[d.ProductId] = d.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryItems := []entity.StockTransferDeliveryItem{
|
||||||
|
{
|
||||||
|
StockTransferDeliveryId: deliveries[0].Id,
|
||||||
|
StockTransferDetailId: detailMap[1],
|
||||||
|
Quantity: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
StockTransferDeliveryId: deliveries[0].Id,
|
||||||
|
StockTransferDetailId: detailMap[2],
|
||||||
|
Quantity: 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := range deliveryItems {
|
||||||
|
if err := tx.Create(&deliveryItems[i]).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func ptr[T any](v T) *T {
|
func ptr[T any](v T) *T {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
LogTypeAdjustment = "ADJUSTMENT"
|
LogTypeAdjustment = "ADJUSTMENT"
|
||||||
LogTypeTransfer = "TRANSFER"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ type StockTransferDelivery struct {
|
|||||||
DocumentPath string
|
DocumentPath string
|
||||||
ShippingCostItem float64
|
ShippingCostItem float64
|
||||||
ShippingCostTotal float64
|
ShippingCostTotal float64
|
||||||
Note string
|
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
|
|||||||
@@ -2,16 +2,12 @@ package entities
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
|
||||||
// DETAIL PRODUK
|
// DETAIL PRODUK
|
||||||
type StockTransferDetail struct {
|
type StockTransferDetail struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
StockTransferId uint64
|
StockTransferId uint64
|
||||||
ProductId uint64
|
ProductId uint64
|
||||||
Quantity float64
|
Quantity float64
|
||||||
BeforeQuantity float64
|
|
||||||
AfterQuantity float64
|
|
||||||
Note string
|
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -72,11 +73,21 @@ func (u *TransferController) GetOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
data := c.FormValue("data")
|
||||||
|
|
||||||
var req validation.TransferRequest
|
var req validation.TransferRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := json.Unmarshal([]byte(data), &req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
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)
|
result, err := u.TransferService.CreateOne(c, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ type TransferBaseDTO struct {
|
|||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
TransferReason string `json:"transfer_reason"`
|
TransferReason string `json:"transfer_reason"`
|
||||||
TransferDate string `json:"transfer_date"`
|
TransferDate string `json:"transfer_date"`
|
||||||
SourceWarehouse *WarehouseSimpleDTO `json:"source_warehouse,omitempty"`
|
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
|
||||||
DestinationWarehouse *WarehouseSimpleDTO `json:"destination_warehouse,omitempty"`
|
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only id and name for warehouse simple view
|
// Only id and name for warehouse simple view
|
||||||
@@ -23,6 +23,24 @@ type WarehouseSimpleDTO struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AreaDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Area *AreaDTO `json:"area"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WarehouseDetailDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Location *LocationDTO `json:"location"`
|
||||||
|
Area *AreaDTO `json:"area"`
|
||||||
|
}
|
||||||
|
|
||||||
type TransferListDTO struct {
|
type TransferListDTO struct {
|
||||||
TransferBaseDTO
|
TransferBaseDTO
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
||||||
@@ -45,7 +63,6 @@ type TransferDetailItemDTO struct {
|
|||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
BeforeQuantity float64 `json:"before_quantity"`
|
BeforeQuantity float64 `json:"before_quantity"`
|
||||||
AfterQuantity float64 `json:"after_quantity"`
|
AfterQuantity float64 `json:"after_quantity"`
|
||||||
Note string `json:"note"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delivery ekspedisi
|
// Delivery ekspedisi
|
||||||
@@ -58,7 +75,6 @@ type TransferDeliveryDTO struct {
|
|||||||
DocumentPath string `json:"document_path"`
|
DocumentPath string `json:"document_path"`
|
||||||
ShippingCostItem float64 `json:"shipping_cost_item"`
|
ShippingCostItem float64 `json:"shipping_cost_item"`
|
||||||
ShippingCostTotal float64 `json:"shipping_cost_total"`
|
ShippingCostTotal float64 `json:"shipping_cost_total"`
|
||||||
Note string `json:"note"`
|
|
||||||
Items []TransferDeliveryItemDTO `json:"items"`
|
Items []TransferDeliveryItemDTO `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,19 +87,14 @@ type TransferDeliveryItemDTO struct {
|
|||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
|
func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
|
||||||
var sourceWarehouse *WarehouseSimpleDTO
|
|
||||||
|
var sourceWarehouse *WarehouseDetailDTO
|
||||||
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
||||||
sourceWarehouse = &WarehouseSimpleDTO{
|
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
|
||||||
Id: e.FromWarehouse.Id,
|
|
||||||
Name: e.FromWarehouse.Name,
|
|
||||||
}
|
}
|
||||||
}
|
var destinationWarehouse *WarehouseDetailDTO
|
||||||
var destinationWarehouse *WarehouseSimpleDTO
|
|
||||||
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
||||||
destinationWarehouse = &WarehouseSimpleDTO{
|
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
|
||||||
Id: e.ToWarehouse.Id,
|
|
||||||
Name: e.ToWarehouse.Name,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return TransferBaseDTO{
|
return TransferBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
@@ -94,6 +105,40 @@ func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toAreaDTO(a *entity.Area) *AreaDTO {
|
||||||
|
if a == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &AreaDTO{
|
||||||
|
Id: a.Id,
|
||||||
|
Name: a.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLocationDTO(l *entity.Location) *LocationDTO {
|
||||||
|
if l == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Area selalu diisi jika l.Area ada
|
||||||
|
return &LocationDTO{
|
||||||
|
Id: l.Id,
|
||||||
|
Name: l.Name,
|
||||||
|
Area: toAreaDTO(&l.Area),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
|
||||||
|
if w == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &WarehouseDetailDTO{
|
||||||
|
Id: w.Id,
|
||||||
|
Name: w.Name,
|
||||||
|
Location: toLocationDTO(w.Location),
|
||||||
|
Area: toAreaDTO(&w.Area),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||||
var createdUser *userDTO.UserBaseDTO
|
var createdUser *userDTO.UserBaseDTO
|
||||||
if e.CreatedUser != nil {
|
if e.CreatedUser != nil {
|
||||||
@@ -107,9 +152,6 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
|||||||
Id: d.Id,
|
Id: d.Id,
|
||||||
ProductId: d.ProductId,
|
ProductId: d.ProductId,
|
||||||
Quantity: d.Quantity,
|
Quantity: d.Quantity,
|
||||||
BeforeQuantity: d.BeforeQuantity,
|
|
||||||
AfterQuantity: d.AfterQuantity,
|
|
||||||
Note: d.Note,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Map deliveries
|
// Map deliveries
|
||||||
@@ -133,7 +175,6 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
|||||||
DocumentPath: del.DocumentPath,
|
DocumentPath: del.DocumentPath,
|
||||||
ShippingCostItem: del.ShippingCostItem,
|
ShippingCostItem: del.ShippingCostItem,
|
||||||
ShippingCostTotal: del.ShippingCostTotal,
|
ShippingCostTotal: del.ShippingCostTotal,
|
||||||
Note: del.Note,
|
|
||||||
Items: items,
|
Items: items,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -163,9 +204,6 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
|||||||
Id: d.Id,
|
Id: d.Id,
|
||||||
ProductId: d.ProductId,
|
ProductId: d.ProductId,
|
||||||
Quantity: d.Quantity,
|
Quantity: d.Quantity,
|
||||||
BeforeQuantity: d.BeforeQuantity,
|
|
||||||
AfterQuantity: d.AfterQuantity,
|
|
||||||
Note: d.Note,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Map deliveries
|
// Map deliveries
|
||||||
@@ -180,7 +218,6 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
|||||||
DocumentPath: del.DocumentPath,
|
DocumentPath: del.DocumentPath,
|
||||||
ShippingCostItem: del.ShippingCostItem,
|
ShippingCostItem: del.ShippingCostItem,
|
||||||
ShippingCostTotal: del.ShippingCostTotal,
|
ShippingCostTotal: del.ShippingCostTotal,
|
||||||
Note: del.Note,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return TransferDetailDTO{
|
return TransferDetailDTO{
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
return db.
|
return db.
|
||||||
Preload("CreatedUser").
|
Preload("CreatedUser").
|
||||||
Preload("FromWarehouse").
|
Preload("FromWarehouse").
|
||||||
|
Preload("FromWarehouse.Location").
|
||||||
|
Preload("FromWarehouse.Area").
|
||||||
Preload("ToWarehouse").
|
Preload("ToWarehouse").
|
||||||
|
Preload("ToWarehouse.Location").
|
||||||
|
Preload("ToWarehouse.Area").
|
||||||
Preload("Details").
|
Preload("Details").
|
||||||
Preload("Deliveries.Items")
|
Preload("Deliveries.Items")
|
||||||
}
|
}
|
||||||
@@ -64,7 +68,7 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
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 != "" {
|
if params.Search != "" {
|
||||||
db = db.Where("movement_number LIKE ?", "%"+strings.TrimSpace(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) {
|
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
||||||
var transfer entity.StockTransfer
|
var transfer entity.StockTransfer
|
||||||
|
|
||||||
db := s.StockTransferRepo.DB().WithContext(c.Context())
|
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 err := db.First(&transfer, id).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
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) {
|
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 {
|
for _, product := range req.Products {
|
||||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||||
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
|
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
|
// Generate movement number
|
||||||
// Format: PND-MBU-00001
|
// Format: PND-MBU-00001
|
||||||
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
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),
|
SupplierId: uint64(delivery.SupplierID),
|
||||||
VehiclePlate: delivery.VehiclePlate,
|
VehiclePlate: delivery.VehiclePlate,
|
||||||
DriverName: delivery.DriverName,
|
DriverName: delivery.DriverName,
|
||||||
DocumentPath: delivery.Document,
|
DocumentPath: "dummy duls", // todo: tunggu ada aws baru proses
|
||||||
ShippingCostItem: delivery.DeliveryCostPerItem,
|
ShippingCostItem: delivery.DeliveryCostPerItem,
|
||||||
ShippingCostTotal: delivery.DeliveryCost,
|
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)
|
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// tambahkan insert ke delivery items sebagai fivot
|
// tambahkan insert ke delivery items sebagai pivot
|
||||||
|
|
||||||
detailMap := make(map[uint64]uint64)
|
detailMap := make(map[uint64]uint64)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
detailMap[d.ProductId] = d.Id
|
detailMap[d.ProductId] = d.Id
|
||||||
@@ -185,15 +203,13 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
|
|
||||||
var deliveryItems []*entity.StockTransferDeliveryItem
|
var deliveryItems []*entity.StockTransferDeliveryItem
|
||||||
|
|
||||||
for _, delivery := range deliveries {
|
for i, delivery := range deliveries {
|
||||||
for _, item := range req.Deliveries {
|
item := req.Deliveries[i]
|
||||||
if item.Document == delivery.DocumentPath {
|
|
||||||
for _, prod := range item.Products {
|
for _, prod := range item.Products {
|
||||||
detailID, ok := detailMap[uint64(prod.ProductID)]
|
detailID, ok := detailMap[uint64(prod.ProductID)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
||||||
}
|
}
|
||||||
|
|
||||||
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
||||||
StockTransferDeliveryId: delivery.Id,
|
StockTransferDeliveryId: delivery.Id,
|
||||||
StockTransferDetailId: detailID,
|
StockTransferDetailId: detailID,
|
||||||
@@ -201,8 +217,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil {
|
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)
|
s.Log.Errorf("Failed to create stock transfer delivery items: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -250,6 +264,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
|
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
|
||||||
}
|
}
|
||||||
|
// Update stok di gudang tujuan
|
||||||
destPW.Quantity += product.ProductQty
|
destPW.Quantity += product.ProductQty
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
|
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)
|
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)
|
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return nil
|
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 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type TransferDeliveryProduct struct {
|
|||||||
type TransferDelivery struct {
|
type TransferDelivery struct {
|
||||||
DeliveryCost float64 `json:"delivery_cost" validate:"required"`
|
DeliveryCost float64 `json:"delivery_cost" validate:"required"`
|
||||||
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
|
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
|
||||||
Document string `json:"document"`
|
DocumentIndex int `json:"document_index" validate:"min=0"`
|
||||||
DriverName string `json:"driver_name" validate:"required"`
|
DriverName string `json:"driver_name" validate:"required"`
|
||||||
VehiclePlate string `json:"vehicle_plate" validate:"required"`
|
VehiclePlate string `json:"vehicle_plate" validate:"required"`
|
||||||
SupplierID uint `json:"supplier_id" validate:"required"`
|
SupplierID uint `json:"supplier_id" validate:"required"`
|
||||||
|
|||||||
Reference in New Issue
Block a user