Merge branch 'dev/teguh' into 'feat/BE/US-35/stock-transfer'

[FEAT/BE][US#35/TASK#58,59,60,61] Complete stock transfer API, DB schema, validation, and audit log

See merge request mbugroup/lti-api!15
This commit is contained in:
Hafizh A. Y.
2025-10-16 08:38:03 +00:00
39 changed files with 1355 additions and 224 deletions
@@ -316,7 +316,7 @@ CREATE TABLE stock_logs (
before_quantity NUMERIC(15, 3) NOT NULL, before_quantity NUMERIC(15, 3) NOT NULL,
after_quantity NUMERIC(15, 3) NOT NULL, after_quantity NUMERIC(15, 3) NOT NULL,
log_type VARCHAR(50) NOT NULL, log_type VARCHAR(50) NOT NULL,
log_id BIGINT , log_id BIGINT,
note TEXT, note TEXT,
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE, 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, created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
@@ -0,0 +1,4 @@
-- DROP TABLE: STOCK_TRANSFERS DAN SEQUENCE-NYA
DROP TABLE IF EXISTS stock_transfers CASCADE;
DROP SEQUENCE IF EXISTS stock_transfer_seq CASCADE;
@@ -0,0 +1,57 @@
-- ===============================================================
-- STOCK TRANSFERS (HEADER)
-- ===============================================================
CREATE SEQUENCE IF NOT EXISTS stock_transfer_seq START 1;
CREATE TABLE IF NOT EXISTS stock_transfers (
id BIGSERIAL PRIMARY KEY,
movement_number VARCHAR(50) UNIQUE NOT NULL,
from_warehouse_id BIGINT NOT NULL,
to_warehouse_id BIGINT NOT NULL,
area_id BIGINT,
reason TEXT,
transfer_date DATE NOT NULL,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_from_warehouse
FOREIGN KEY (from_warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_to_warehouse
FOREIGN KEY (to_warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'areas') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_area
FOREIGN KEY (area_id)
REFERENCES areas(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfers_from_warehouse_id ON stock_transfers(from_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfers_to_warehouse_id ON stock_transfers(to_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfers_transfer_date ON stock_transfers(transfer_date);
@@ -0,0 +1,2 @@
-- DROP TABLE: STOCK_TRANSFER_DETAILS
DROP TABLE IF EXISTS stock_transfer_details CASCADE;
@@ -0,0 +1,48 @@
-- ===============================================================
-- STOCK TRANSFER DETAILS (PRODUK)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_details (
id BIGSERIAL PRIMARY KEY,
stock_transfer_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0),
before_quantity NUMERIC(15, 3),
after_quantity NUMERIC(15, 3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- ===============================================================
-- FOREIGN KEYS (dengan pengecekan tabel agar anti gagal)
-- ===============================================================
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_transfer
FOREIGN KEY (stock_transfer_id)
REFERENCES stock_transfers(id)
ON DELETE CASCADE ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_product
FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
END $$;
-- ===============================================================
-- INDEXES
-- ===============================================================
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_transfer_id ON stock_transfer_details (stock_transfer_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_product_id ON stock_transfer_details (product_id);
@@ -0,0 +1,2 @@
-- DROP TABLE: STOCK_TRANSFER_DELIVERIES
DROP TABLE IF EXISTS stock_transfer_deliveries CASCADE;
@@ -0,0 +1,42 @@
-- ===============================================================
-- STOCK TRANSFER DELIVERIES (EKSPEDISI)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_deliveries (
id BIGSERIAL PRIMARY KEY,
stock_transfer_id BIGINT NOT NULL,
supplier_id BIGINT,
vehicle_plate VARCHAR(20),
driver_name VARCHAR(100),
document_number VARCHAR(50),
document_path TEXT,
shipping_cost_item NUMERIC(15,3),
shipping_cost_total NUMERIC(15,3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
ALTER TABLE stock_transfer_deliveries
ADD CONSTRAINT fk_stock_transfer_deliveries_transfer
FOREIGN KEY (stock_transfer_id)
REFERENCES stock_transfers(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
ALTER TABLE stock_transfer_deliveries
ADD CONSTRAINT fk_stock_transfer_deliveries_supplier
FOREIGN KEY (supplier_id)
REFERENCES suppliers(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_transfer_id ON stock_transfer_deliveries(stock_transfer_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_supplier_id ON stock_transfer_deliveries(supplier_id);
@@ -0,0 +1,2 @@
-- DROP PIVOT TABLE: STOCK_TRANSFER_DELIVERY_ITEMS
DROP TABLE IF EXISTS stock_transfer_delivery_items CASCADE;
@@ -0,0 +1,35 @@
-- ===============================================================
-- STOCK TRANSFER DELIVERY ITEMS (PIVOT)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_delivery_items (
id BIGSERIAL PRIMARY KEY,
stock_transfer_delivery_id BIGINT NOT NULL,
stock_transfer_detail_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0)
);
-- FOREIGN KEYS
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_deliveries') THEN
ALTER TABLE stock_transfer_delivery_items
ADD CONSTRAINT fk_delivery_items_delivery
FOREIGN KEY (stock_transfer_delivery_id)
REFERENCES stock_transfer_deliveries(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_details') THEN
ALTER TABLE stock_transfer_delivery_items
ADD CONSTRAINT fk_delivery_items_detail
FOREIGN KEY (stock_transfer_detail_id)
REFERENCES stock_transfer_details(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_delivery_id ON stock_transfer_delivery_items (stock_transfer_delivery_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_detail_id ON stock_transfer_delivery_items (stock_transfer_detail_id);
+83 -1
View File
@@ -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
} }
+23
View File
@@ -0,0 +1,23 @@
package entities
import "time"
// HEADER
type StockTransfer struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
MovementNumber string `gorm:"uniqueIndex;not null"`
FromWarehouseId uint64
ToWarehouseId uint64
TransferDate time.Time
Reason string
CreatedBy uint64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
FromWarehouse *Warehouse `gorm:"foreignKey:FromWarehouseId"`
ToWarehouse *Warehouse `gorm:"foreignKey:ToWarehouseId"`
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
}
@@ -0,0 +1,23 @@
package entities
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"`
}
@@ -0,0 +1,12 @@
package entities
// PIVOT TABLE TRANSFER
type StockTransferDeliveryItem struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferDeliveryId uint64
StockTransferDetailId uint64
Quantity float64
// Relations
StockTransferDelivery *StockTransferDelivery `gorm:"foreignKey:StockTransferDeliveryId"`
StockTransferDetail *StockTransferDetail `gorm:"foreignKey:StockTransferDetailId"`
}
@@ -0,0 +1,18 @@
package entities
import "time"
// DETAIL PRODUK
type StockTransferDetail struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64
ProductId uint64
Quantity float64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Product *Product `gorm:"foreignKey:ProductId"`
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
}
@@ -2,6 +2,7 @@ package controller
import ( import (
"math" "math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
@@ -78,3 +79,25 @@ func (u *AdjustmentController) AdjustmentHistory(c *fiber.Ctx) error {
Data: adjustmentDTOs, Data: adjustmentDTOs,
}) })
} }
func (u *AdjustmentController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
stockLog, err := u.AdjustmentService.GetOne(c, uint(id))
if err != nil {
return err
}
// Use DTO for response
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get adjustment successfully",
Data: dto.ToAdjustmentDetailDTO(stockLog),
})
}
@@ -4,15 +4,17 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
// === DTO Structs === // === DTO Structs ===
type ProductBaseDTO struct { type ProductBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
SKU string `json:"sku"` SKU string `json:"sku"`
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
} }
type WarehouseBaseDTO struct { type WarehouseBaseDTO struct {
@@ -61,10 +63,18 @@ func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO {
if e.Sku != nil { if e.Sku != nil {
sku = *e.Sku sku = *e.Sku
} }
var category *productCategoryDTO.ProductCategoryBaseDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
category = &mapped
}
return &ProductBaseDTO{ return &ProductBaseDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, Name: e.Name,
SKU: sku, SKU: sku,
ProductCategory: category,
} }
} }
@@ -7,6 +7,7 @@ import (
sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories" rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
@@ -21,8 +22,9 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
warehouseRepo := rWarehouse.NewWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
productRepo := rproduct.NewProductRepository(db)
adjustmentService := sAdjustment.NewAdjustmentService(stockLogsRepo, warehouseRepo, productWarehouseRepo, validate) adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
AdjustmentRoutes(router, userService, adjustmentService) AdjustmentRoutes(router, userService, adjustmentService)
@@ -17,5 +17,6 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme
// Standard CRUD routes following master data pattern // Standard CRUD routes following master data pattern
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
route.Post("/", ctrl.Adjustment) // Create adjustment route.Post("/", ctrl.Adjustment) // Create adjustment
route.Get("/:id", ctrl.GetOne)
} }
@@ -7,6 +7,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories" stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -29,15 +30,17 @@ type adjustmentService struct {
StockLogsRepository stockLogsRepo.StockLogRepository StockLogsRepository stockLogsRepo.StockLogRepository
WarehouseRepo warehouseRepo.WarehouseRepository WarehouseRepo warehouseRepo.WarehouseRepository
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
ProductRepo productRepo.ProductRepository
} }
func NewAdjustmentService(stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService { func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService {
return &adjustmentService{ return &adjustmentService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
StockLogsRepository: stockLogsRepo, StockLogsRepository: stockLogsRepo,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
ProductRepo: productRepo,
} }
} }
@@ -50,7 +53,9 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB {
} }
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) { func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) {
stockLog, err := s.StockLogsRepository.GetByID(c.Context(), id, s.withRelations) stockLog, err := s.StockLogsRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db).Preload("ProductWarehouse.Product.ProductCategory")
})
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found") return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
@@ -72,14 +77,27 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
ctx := c.Context() ctx := c.Context()
productWarehouseExists, err := s.ProductWarehouseRepo.ProductWarehouseExists(ctx, uint(req.ProductID), uint(req.WarehouseID), nil) isProductExist, err := s.ProductRepo.IdExists(c.Context(), uint(req.ProductID))
if err != nil { if err != nil {
return nil, err s.Log.Errorf("Failed to check product existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product")
} }
if !productWarehouseExists { if !isProductExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse not found") return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found")
} }
isWarehouseExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
}
if !isWarehouseExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not found")
}
if req.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
}
transactionType := strings.ToUpper(req.TransactionType) transactionType := strings.ToUpper(req.TransactionType)
if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease { if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
@@ -87,16 +105,33 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
var createdLogId uint var createdLogId uint
isProductWarehouseExist, err := s.ProductWarehouseRepo.ProductWarehouseExistByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to check product warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
}
if !isProductWarehouseExist {
newPW := &entity.ProductWarehouse{
ProductId: uint(req.ProductID),
WarehouseId: uint(req.WarehouseID),
Quantity: 0,
CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
s.Log.Errorf("Failed to create product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
}
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
}
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// Get product warehouse by product id and warehouse id (read operation, no transaction needed)
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID)) productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil { if err != nil {
return err s.Log.Errorf("Failed to get product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
} }
if productWarehouse == nil {
return fiber.NewError(fiber.StatusBadRequest, "Product warehouse not found")
}
s.Log.Infof("Product Warehouse found: %+v", productWarehouse.Id)
afterQuantity := productWarehouse.Quantity afterQuantity := productWarehouse.Quantity
if transactionType == entity.TransactionTypeIncrease { if transactionType == entity.TransactionTypeIncrease {
@@ -133,7 +168,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
s.Log.Infof("Product warehouse quantity updated: %+v", productWarehouse.Id) s.Log.Infof("Product warehouse quantity updated: %+v", productWarehouse.Id)
// Set createdLogId to get the log with relations after transaction
createdLogId = newLog.Id createdLogId = newLog.Id
return nil return nil
}) })
@@ -72,70 +72,4 @@ func (u *ProductWarehouseController) GetOne(c *fiber.Ctx) error {
}) })
} }
func (u *ProductWarehouseController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProductWarehouseService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create productWarehouse successfully",
Data: dto.ToProductWarehouseListDTO(*result),
})
}
func (u *ProductWarehouseController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProductWarehouseService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update productWarehouse successfully",
Data: dto.ToProductWarehouseListDTO(*result),
})
}
func (u *ProductWarehouseController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ProductWarehouseService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete productWarehouse successfully",
})
}
@@ -10,10 +10,10 @@ import (
// === DTO Structs === // === DTO Structs ===
type ProductWarehouseBaseDTO struct { type ProductWarehouseBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
ProductId uint `json:"product_id"` ProductId uint `json:"product_id"`
WarehouseId uint `json:"warehouse_id"` WarehouseId uint `json:"warehouse_id"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
} }
type ProductWarehouseListDTO struct { type ProductWarehouseListDTO struct {
@@ -31,9 +31,10 @@ type ProductWarehouseDetailDTO struct {
// Nested DTOs for relations // Nested DTOs for relations
type ProductBaseDTO struct { type ProductBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Sku string `json:"sku"` Sku string `json:"sku"`
Flags []string `json:"flags"`
} }
type WarehouseBaseDTO struct { type WarehouseBaseDTO struct {
@@ -68,6 +69,12 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
if e.Product.Sku != nil { if e.Product.Sku != nil {
product.Sku = *e.Product.Sku product.Sku = *e.Product.Sku
} }
// Map flags from Product relation
if len(e.Product.Flags) > 0 {
for _, f := range e.Product.Flags {
product.Flags = append(product.Flags, f.Name)
}
}
dto.Product = &product dto.Product = &product
} }
@@ -13,6 +13,7 @@ type ProductWarehouseRepository interface {
ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error)
IsProductExist(ctx context.Context, productId uint) (bool, error) IsProductExist(ctx context.Context, productId uint) (bool, error)
IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error)
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
ExistsByID(ctx context.Context, id uint) (bool, error) ExistsByID(ctx context.Context, id uint) (bool, error)
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
} }
@@ -53,6 +54,17 @@ func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id) return repository.Exists[entity.ProductWarehouse](ctx, r.db, id)
} }
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
var count int64
if err := r.db.WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) { func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
if err := r.DB().WithContext(ctx).Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).First(&productWarehouse).Error; err != nil { if err := r.DB().WithContext(ctx).Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).First(&productWarehouse).Error; err != nil {
@@ -21,8 +21,6 @@ func ProductWarehouseRoutes(v1 fiber.Router, u user.UserService, s productWareho
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne) route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
} }
@@ -17,9 +17,6 @@ import (
type ProductWarehouseService interface { type ProductWarehouseService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductWarehouse, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductWarehouse, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductWarehouse, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
} }
type productWarehouseService struct { type productWarehouseService struct {
@@ -37,7 +34,7 @@ func NewProductWarehouseService(repo repository.ProductWarehouseRepository, vali
} }
func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB { func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Product").Preload("Warehouse").Preload("CreatedUser") return db.Preload("Product.Flags").Preload("Product").Preload("Warehouse").Preload("CreatedUser")
} }
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) { func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
@@ -79,125 +76,3 @@ func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductW
} }
return productWarehouse, nil return productWarehouse, nil
} }
func (s *productWarehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProductWarehouse, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
isProductExist, err := s.Repository.IsProductExist(c.Context(), req.ProductId)
if err != nil {
s.Log.Errorf("Failed to check product existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product existence")
}
if !isProductExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found")
}
isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), req.WarehouseId)
if err != nil {
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse existence")
}
if !isWarehouseExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not found")
}
// chceking if productWarehouse with same product_id and warehouse_id already
exists, err := s.Repository.ProductWarehouseExists(c.Context(), req.ProductId, req.WarehouseId, nil)
if err != nil {
s.Log.Errorf("Failed to check productWarehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check productWarehouse existence")
}
if exists {
return nil, fiber.NewError(fiber.StatusConflict, "ProductWarehouse already exists")
}
createBody := &entity.ProductWarehouse{
ProductId: req.ProductId,
WarehouseId: req.WarehouseId,
Quantity: req.Quantity,
CreatedBy: 1, // TODO: Ganti dengan user ID dari context setelah middleware auth diimplementasi
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create productWarehouse: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s productWarehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductWarehouse, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
// validation Id exist
if exists, err := s.Repository.ExistsByID(c.Context(), id); err != nil {
s.Log.Errorf("Failed to check productWarehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check productWarehouse existence")
} else if !exists {
return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
}
// validation productId and warehouseId exist
if req.ProductId != nil {
isProductExist, err := s.Repository.IsProductExist(c.Context(), *req.ProductId)
if err != nil {
s.Log.Errorf("Failed to check product existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product existence")
}
if !isProductExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found")
}
}
if req.WarehouseId != nil {
isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), *req.WarehouseId)
if err != nil {
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse existence")
}
if !isWarehouseExist {
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
}
updateBody := make(map[string]any)
if req.ProductId != nil {
updateBody["product_id"] = *req.ProductId
}
if req.WarehouseId != nil {
updateBody["warehouse_id"] = *req.WarehouseId
}
if req.Quantity != nil {
updateBody["quantity"] = *req.Quantity
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
}
s.Log.Errorf("Failed to update productWarehouse: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s productWarehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
}
s.Log.Errorf("Failed to delete productWarehouse: %+v", err)
return err
}
return nil
}
+2
View File
@@ -9,6 +9,7 @@ import (
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses" productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments" adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -19,6 +20,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
productWarehouses.ProductWarehouseModule{}, productWarehouses.ProductWarehouseModule{},
adjustments.AdjustmentModule{}, adjustments.AdjustmentModule{},
transfers.TransferModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }
@@ -0,0 +1,103 @@
package controller
import (
"encoding/json"
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransferController struct {
TransferService service.TransferService
}
func NewTransferController(transferService service.TransferService) *TransferController {
return &TransferController{
TransferService: transferService,
}
}
func (u *TransferController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.TransferService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transfers successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransferListDTOs(result),
})
}
func (u *TransferController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.TransferService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transfer successfully",
Data: dto.ToTransferListDTO(*result),
})
}
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
data := c.FormValue("data")
var req validation.TransferRequest
if err := json.Unmarshal([]byte(data), &req); err != nil {
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)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create transfer successfully",
Data: dto.ToTransferListDTO(*result),
})
}
@@ -0,0 +1,225 @@
package dto
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"`
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
}
// Only id and name for warehouse simple view
type WarehouseSimpleDTO struct {
Id uint `json:"id"`
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"`
}
type WarehouseDetailDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Location *LocationDTO `json:"location"`
Area *AreaDTO `json:"area"`
}
type TransferListDTO struct {
TransferBaseDTO
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
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"`
}
// 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"`
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 *WarehouseDetailDTO
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
}
var destinationWarehouse *WarehouseDetailDTO
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
}
return TransferBaseDTO{
Id: e.Id,
TransferReason: e.Reason,
TransferDate: e.CreatedAt.Format("2006-01-02"),
SourceWarehouse: sourceWarehouse,
DestinationWarehouse: destinationWarehouse,
}
}
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
}
return &LocationDTO{
Id: l.Id,
Name: l.Name,
}
}
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), // Ambil area langsung dari warehouse (area_id)
}
}
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,
})
}
// 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,
Items: items,
})
}
return TransferListDTO{
TransferBaseDTO: ToTransferBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Details: details,
Deliveries: deliveries,
}
}
func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
result := make([]TransferListDTO, len(e))
for i, r := range e {
result[i] = ToTransferListDTO(r)
}
return result
}
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,
})
}
// 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,
})
}
return TransferDetailDTO{
TransferListDTO: ToTransferListDTO(e),
Details: details,
Deliveries: deliveries,
}
}
@@ -0,0 +1,33 @@
package transfers
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
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"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type TransferModule struct{}
func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
stockTransferRepo := rStockTransfer.NewStockTransferRepository(db)
stockTransferDetailRepo := rStockTransfer.NewStockTransferDetailRepository(db)
stockTransferDeliveryRepo := rStockTransfer.NewStockTransferDeliveryRepository(db)
StockTransferDeliveryItemRepo := rStockTransfer.NewStockTransferDeliveryItemRepository(db)
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo)
userService := sUser.NewUserService(userRepo, validate)
TransferRoutes(router, userService, transferService)
}
@@ -0,0 +1,34 @@
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"
)
type StockTransferRepository interface {
repository.BaseRepository[entity.StockTransfer]
// get sequence for movement number
GetNextMovementNumber(ctx context.Context) (int64, error)
}
type StockTransferRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransfer]
}
func NewStockTransferRepository(db *gorm.DB) StockTransferRepository {
return &StockTransferRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransfer](db),
}
}
func (r *StockTransferRepositoryImpl) GetNextMovementNumber(ctx context.Context) (int64, error) {
var seq int64
err := r.DB().WithContext(ctx).Raw("SELECT nextval('stock_transfer_seq')").Scan(&seq).Error
if err != nil {
return 0, err
}
return seq, nil
}
@@ -0,0 +1,22 @@
package repositories
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferDeliveryRepository interface {
repository.BaseRepository[entity.StockTransferDelivery]
// Tambahkan custom method jika perlu
}
type StockTransferDeliveryRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransferDelivery]
}
func NewStockTransferDeliveryRepository(db *gorm.DB) StockTransferDeliveryRepository {
return &StockTransferDeliveryRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDelivery](db),
}
}
@@ -0,0 +1,22 @@
package repositories
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferDeliveryItemRepository interface {
repository.BaseRepository[entity.StockTransferDeliveryItem]
// Tambahkan custom method jika perlu
}
type StockTransferDeliveryItemRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransferDeliveryItem]
}
func NewStockTransferDeliveryItemRepository(db *gorm.DB) StockTransferDeliveryItemRepository {
return &StockTransferDeliveryItemRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDeliveryItem](db),
}
}
@@ -0,0 +1,29 @@
// 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"
)
type StockTransferDetailRepository interface {
repository.BaseRepository[entity.StockTransferDetail]
FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error
}
type StockTransferDetailRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransferDetail]
}
func NewStockTransferDetailRepository(db *gorm.DB) StockTransferDetailRepository {
return &StockTransferDetailRepositoryImpl{
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
}
@@ -0,0 +1,27 @@
package transfers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/controllers"
transfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferService) {
ctrl := controller.NewTransferController(s)
route := v1.Group("/transfers")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,313 @@
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"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
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)
}
type transferService struct {
Log *logrus.Logger
Validate *validator.Validate
StockTransferRepo rStockTransfer.StockTransferRepository
StockTransferDetailRepo rStockTransfer.StockTransferDetailRepository
StockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository
StockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository
StockLogsRepository rStockLogs.StockLogRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
SupplierRepo rSupplier.SupplierRepository
}
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) TransferService {
return &transferService{
Log: utils.Log,
Validate: validate,
StockTransferRepo: stockTransferRepo,
StockTransferDetailRepo: stockTransferDetailRepo,
StockTransferDeliveryRepo: stockTransferDeliveryRepo,
StockTransferDeliveryItemRepo: stockTransferDeliveryItemRepo,
StockLogsRepository: stockLogsRepo,
ProductWarehouseRepo: productWarehouseRepo,
SupplierRepo: supplierRepo,
}
}
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")
}
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
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
}
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
var transfer entity.StockTransfer
// gunakan repo secara langsung
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db)
})
if err != nil {
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)
return transferPtr, nil
}
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
// 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),
)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
}
if sourcePW.Quantity < product.ProductQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
}
}
// 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))
}
}
// cek suplier id caegory BOP cek by id
for _, delivery := range req.Deliveries {
supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
}
if supplier.Category != "BOP" {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
}
}
// Generate movement number
// Format: PND-MBU-00001
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)
transferDate, _ := utils.ParseDateString(req.TransferDate)
entityTransfer := &entity.StockTransfer{
FromWarehouseId: uint64(req.SourceWarehouseID),
ToWarehouseId: uint64(req.DestinationWarehouseID),
Reason: req.TransferReason,
TransferDate: transferDate,
MovementNumber: movementNumber,
CreatedBy: 1, //todo: get from token
}
// Save the transfer entity to the database
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
// Insert header
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)
// insert ke details
var details []*entity.StockTransferDetail
for _, product := range req.Products {
details = append(details, &entity.StockTransferDetail{
StockTransferId: entityTransfer.Id,
ProductId: uint64(product.ProductID),
Quantity: product.ProductQty,
})
}
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)
// Tambahkan proses insert delivery
var deliveries []*entity.StockTransferDelivery
for _, delivery := range req.Deliveries {
deliveries = append(deliveries, &entity.StockTransferDelivery{
StockTransferId: entityTransfer.Id,
SupplierId: uint64(delivery.SupplierID),
VehiclePlate: delivery.VehiclePlate,
DriverName: delivery.DriverName,
DocumentPath: "dummy duls", // todo: tunggu ada aws baru proses
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
}
// tambahkan insert ke delivery items sebagai pivot
detailMap := make(map[uint64]uint64)
for _, d := range details {
detailMap[d.ProductId] = d.Id
}
var deliveryItems []*entity.StockTransferDeliveryItem
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 {
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)
// Proses pengurangan stok di gudang asal dan penambahan stok di gudang tujuan
for _, product := range req.Products {
// Kurangi stok di gudang asal
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)
// Tambah stok di gudang tujuan
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
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) {
// Jika belum ada record untuk produk di gudang tujuan, buat baru
destPW = &entity.ProductWarehouse{
ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0,
CreatedBy: 1, // TODO: should Get from auth middleware
}
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)
}
// 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)
return err
}
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
}
return nil
})
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
}
// 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
}
@@ -0,0 +1,40 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
type TransferProduct struct {
ProductID uint `json:"product_id" validate:"required"`
ProductQty float64 `json:"product_qty" validate:"required,gt=0"`
}
type TransferDeliveryProduct struct {
ProductID uint `json:"product_id" validate:"required"`
ProductQty float64 `json:"product_qty" validate:"required,gt=0"`
}
type TransferDelivery struct {
DeliveryCost float64 `json:"delivery_cost" validate:"required"`
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
DocumentIndex int `json:"document_index" validate:"min=0"`
DriverName string `json:"driver_name" validate:"required"`
VehiclePlate string `json:"vehicle_plate" validate:"required"`
SupplierID uint `json:"supplier_id" validate:"required"`
Products []TransferDeliveryProduct `json:"products" validate:"required,dive"`
}
type TransferRequest struct {
TransferReason string `json:"transfer_reason" validate:"required"`
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
SourceWarehouseID uint `json:"source_warehouse_id" validate:"required"`
DestinationWarehouseID uint `json:"destination_warehouse_id" validate:"required"`
Products []TransferProduct `json:"products" validate:"required,dive"`
Deliveries []TransferDelivery `json:"deliveries" validate:"required,dive"`
}
@@ -13,6 +13,7 @@ type ProductRepository interface {
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error) SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error)
UomExists(ctx context.Context, uomID uint) (bool, error) UomExists(ctx context.Context, uomID uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error)
CategoryExists(ctx context.Context, categoryID uint) (bool, error) CategoryExists(ctx context.Context, categoryID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
@@ -194,3 +195,7 @@ func (r *ProductRepositoryImpl) GetFlags(ctx context.Context, productID uint) ([
} }
return flags, nil return flags, nil
} }
func (r *ProductRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.DB(), id)
}
@@ -11,6 +11,7 @@ import (
type SupplierRepository interface { type SupplierRepository interface {
repository.BaseRepository[entity.Supplier] repository.BaseRepository[entity.Supplier]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
} }
type SupplierRepositoryImpl struct { type SupplierRepositoryImpl struct {
@@ -14,6 +14,7 @@ type WarehouseRepository interface {
LocationExists(ctx context.Context, locationId uint) (bool, error) LocationExists(ctx context.Context, locationId uint) (bool, error)
KandangExists(ctx context.Context, kandangId uint) (bool, error) KandangExists(ctx context.Context, kandangId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error)
} }
type WarehouseRepositoryImpl struct { type WarehouseRepositoryImpl struct {
@@ -43,3 +44,6 @@ func (r *WarehouseRepositoryImpl) KandangExists(ctx context.Context, kandangId u
func (r *WarehouseRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { func (r *WarehouseRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Warehouse](ctx, r.db, name, excludeID) return repository.ExistsByName[entity.Warehouse](ctx, r.db, name, excludeID)
} }
func (r *WarehouseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Warehouse](ctx, r.db, id)
}
+25
View File
@@ -0,0 +1,25 @@
package utils
import (
"time"
"errors"
)
// ParseDateString mengubah string "YYYY-MM-DD" menjadi time.Time
func ParseDateString(dateStr string) (time.Time, error) {
if dateStr == "" {
return time.Time{}, errors.New("date string is empty")
}
parsed, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return time.Time{}, errors.New("invalid date format, expected YYYY-MM-DD")
}
return parsed, nil
}
// FormatDate mengubah time.Time menjadi string "YYYY-MM-DD"
func FormatDate(t time.Time) string {
return t.Format("2006-01-02")
}