mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-22 14:25:45 +00:00
Merge branch 'dev/ragil-before-sso' into 'feat/BE/US-74/pengajuan-flock'
FIX(BE-74): if project flocs deleted kandangs reset to non_active, add filter get all project_flock by area,kandangs,period and location and make table pivot in project flocs - kandangs See merge request mbugroup/lti-api!20
This commit is contained in:
@@ -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);
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- DROP PIVOT TABLE: STOCK_TRANSFER_DELIVERY_ITEMS
|
||||||
|
DROP TABLE IF EXISTS stock_transfer_delivery_items CASCADE;
|
||||||
+35
@@ -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);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS project_flock_kandangs;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE project_flock_kandangs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_id BIGINT NOT NULL REFERENCES project_flocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
kandang_id BIGINT NOT NULL REFERENCES kandangs (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
detached_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_project_flock_kandangs_project ON project_flock_kandangs (project_flock_id);
|
||||||
|
CREATE INDEX idx_project_flock_kandangs_kandang ON project_flock_kandangs (kandang_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_project_flock_kandangs_active ON project_flock_kandangs (project_flock_id, kandang_id)
|
||||||
|
WHERE
|
||||||
|
detached_at IS NULL;
|
||||||
@@ -89,6 +89,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
|
||||||
})
|
})
|
||||||
@@ -374,6 +378,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
if err := tx.Create(&kandang).Error; err != nil {
|
if err := tx.Create(&kandang).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := syncPivotRelation(tx, projectFlockID, kandang.Id, createdBy); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
@@ -390,6 +397,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
|
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := syncPivotRelation(tx, projectFlockID, kandang.Id, createdBy); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result[seed.Name] = kandang.Id
|
result[seed.Name] = kandang.Id
|
||||||
}
|
}
|
||||||
@@ -397,6 +407,40 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint, createdBy uint) error {
|
||||||
|
if err := detachActivePivot(tx, kandangID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if projectFlockID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ensureActivePivot(tx, *projectFlockID, kandangID, createdBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func detachActivePivot(tx *gorm.DB, kandangID uint) error {
|
||||||
|
return tx.Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("kandang_id = ? AND detached_at IS NULL", kandangID).
|
||||||
|
Updates(map[string]any{"detached_at": time.Now()}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) error {
|
||||||
|
var pivot entity.ProjectFlockKandang
|
||||||
|
err := tx.Where("project_flock_id = ? AND kandang_id = ? AND detached_at IS NULL", projectFlockID, kandangID).
|
||||||
|
First(&pivot).Error
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newRecord := entity.ProjectFlockKandang{
|
||||||
|
ProjectFlockId: projectFlockID,
|
||||||
|
KandangId: kandangID,
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
}
|
||||||
|
return tx.Create(&newRecord).Error
|
||||||
|
}
|
||||||
|
|
||||||
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
|
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
|
||||||
seeds := []struct {
|
seeds := []struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -936,7 +980,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 {
|
||||||
@@ -960,6 +1004,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,22 +7,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectFlock struct {
|
type ProjectFlock struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
FlockId uint `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:1"`
|
FlockId uint `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:1"`
|
||||||
AreaId uint `gorm:"not null"`
|
AreaId uint `gorm:"not null"`
|
||||||
ProductCategoryId uint `gorm:"not null"`
|
ProductCategoryId uint `gorm:"not null"`
|
||||||
FcrId uint `gorm:"not null"`
|
FcrId uint `gorm:"not null"`
|
||||||
LocationId uint `gorm:"not null"`
|
LocationId uint `gorm:"not null"`
|
||||||
Period int `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:2"`
|
Period int `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:2"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
|
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
|
||||||
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
||||||
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
||||||
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
|
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
|
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ProjectFlockKandang struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_active,priority:1,where:detached_at IS NULL"`
|
||||||
|
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_active,priority:2,where:detached_at IS NULL"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
AssignedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
DetachedAt *time.Time `gorm:"index"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+22
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||||
@@ -23,10 +26,58 @@ func NewProjectflockController(projectflockService service.ProjectflockService)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
|
func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
|
||||||
|
parseUintList := func(raw string) ([]uint, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []uint
|
||||||
|
if strings.HasPrefix(raw, "[") {
|
||||||
|
if err := json.Unmarshal([]byte(raw), &ids); err == nil {
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
for _, part := range parts {
|
||||||
|
part = strings.Trim(part, " \"[]")
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v, err := strconv.Atoi(part)
|
||||||
|
if err != nil || v <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid kandang id: %s", part)
|
||||||
|
}
|
||||||
|
ids = append(ids, uint(v))
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
|
SortBy: c.Query("sort_by", ""),
|
||||||
|
SortOrder: c.Query("sort_order", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if area := c.QueryInt("area_id", 0); area > 0 {
|
||||||
|
query.AreaId = uint(area)
|
||||||
|
}
|
||||||
|
if location := c.QueryInt("location_id", 0); location > 0 {
|
||||||
|
query.LocationId = uint(location)
|
||||||
|
}
|
||||||
|
if period := c.QueryInt("period", 0); period > 0 {
|
||||||
|
query.Period = period
|
||||||
|
}
|
||||||
|
|
||||||
|
if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" {
|
||||||
|
ids, err := parseUintList(kandangRaw)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
query.KandangIds = ids
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.ProjectflockService.GetAll(c, query)
|
result, totalResults, err := u.ProjectflockService.GetAll(c, query)
|
||||||
|
|||||||
@@ -8,24 +8,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectFlockBaseDTO struct {
|
type ProjectFlockBaseDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
// FlockId uint `json:"flock_id"`
|
// FlockId uint `json:"flock_id"`
|
||||||
// AreaId uint `json:"area_id"`
|
// AreaId uint `json:"area_id"`
|
||||||
// ProductCategoryId uint `json:"product_category_id"`
|
// ProductCategoryId uint `json:"product_category_id"`
|
||||||
// FcrId uint `json:"fcr_id"`
|
// FcrId uint `json:"fcr_id"`
|
||||||
// LocationId uint `json:"location_id"`
|
// LocationId uint `json:"location_id"`
|
||||||
Period int `json:"period"`
|
Period int `json:"period"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
|
func ToProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
|
||||||
return ProjectFlockBaseDTO{
|
return ProjectFlockBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
// FlockId: e.FlockId,
|
// FlockId: e.FlockId,
|
||||||
// AreaId: e.AreaId,
|
// AreaId: e.AreaId,
|
||||||
// ProductCategoryId: e.ProductCategoryId,
|
// ProductCategoryId: e.ProductCategoryId,
|
||||||
// FcrId: e.FcrId,
|
// FcrId: e.FcrId,
|
||||||
// LocationId: e.LocationId,
|
// LocationId: e.LocationId,
|
||||||
Period: e.Period,
|
Period: e.Period,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
|||||||
flockRepo := rFlock.NewFlockRepository(db)
|
flockRepo := rFlock.NewFlockRepository(db)
|
||||||
kandangRepo := rKandang.NewKandangRepository(db)
|
kandangRepo := rKandang.NewKandangRepository(db)
|
||||||
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
||||||
|
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, validate)
|
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ProjectflockRoutes(router, userService, projectflockService)
|
ProjectflockRoutes(router, userService, projectflockService)
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectFlockKandangRepository interface {
|
||||||
|
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
|
||||||
|
MarkDetached(ctx context.Context, projectFlockID uint, kandangIDs []uint, detachedAt time.Time) error
|
||||||
|
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
|
||||||
|
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||||
|
DB() *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type projectFlockKandangRepositoryImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository {
|
||||||
|
return &projectFlockKandangRepositoryImpl{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error {
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Create(&records).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) MarkDetached(ctx context.Context, projectFlockID uint, kandangIDs []uint, detachedAt time.Time) error {
|
||||||
|
if len(kandangIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("project_flock_id = ? AND kandang_id IN ? AND detached_at IS NULL", projectFlockID, kandangIDs).
|
||||||
|
Updates(map[string]any{"detached_at": detachedAt}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) {
|
||||||
|
var records []entity.ProjectFlockKandang
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Preload("ProjectFlock").
|
||||||
|
Preload("ProjectFlock.Flock").
|
||||||
|
Preload("Kandang").
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Order("project_flock_id ASC, assigned_at ASC").
|
||||||
|
Find(&records).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository {
|
||||||
|
return &projectFlockKandangRepositoryImpl{db: tx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB {
|
||||||
|
return r.db
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
@@ -35,6 +37,7 @@ type projectflockService struct {
|
|||||||
Repository repository.ProjectflockRepository
|
Repository repository.ProjectflockRepository
|
||||||
FlockRepo flockRepository.FlockRepository
|
FlockRepo flockRepository.FlockRepository
|
||||||
KandangRepo kandangRepository.KandangRepository
|
KandangRepo kandangRepository.KandangRepository
|
||||||
|
PivotRepo repository.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlockPeriodSummary struct {
|
type FlockPeriodSummary struct {
|
||||||
@@ -46,6 +49,7 @@ func NewProjectflockService(
|
|||||||
repo repository.ProjectflockRepository,
|
repo repository.ProjectflockRepository,
|
||||||
flockRepo flockRepository.FlockRepository,
|
flockRepo flockRepository.FlockRepository,
|
||||||
kandangRepo kandangRepository.KandangRepository,
|
kandangRepo kandangRepository.KandangRepository,
|
||||||
|
pivotRepo repository.ProjectFlockKandangRepository,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) ProjectflockService {
|
) ProjectflockService {
|
||||||
return &projectflockService{
|
return &projectflockService{
|
||||||
@@ -54,6 +58,7 @@ func NewProjectflockService(
|
|||||||
Repository: repo,
|
Repository: repo,
|
||||||
FlockRepo: flockRepo,
|
FlockRepo: flockRepo,
|
||||||
KandangRepo: kandangRepo,
|
KandangRepo: kandangRepo,
|
||||||
|
PivotRepo: pivotRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,11 +78,81 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.Page <= 0 {
|
||||||
|
params.Page = 1
|
||||||
|
}
|
||||||
|
if params.Limit <= 0 {
|
||||||
|
params.Limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
|
||||||
|
if params.AreaId > 0 {
|
||||||
|
db = db.Where("project_flocks.area_id = ?", params.AreaId)
|
||||||
|
}
|
||||||
|
if params.LocationId > 0 {
|
||||||
|
db = db.Where("project_flocks.location_id = ?", params.LocationId)
|
||||||
|
}
|
||||||
|
if params.Period > 0 {
|
||||||
|
db = db.Where("project_flocks.period = ?", params.Period)
|
||||||
|
}
|
||||||
|
if len(params.KandangIds) > 0 {
|
||||||
|
db = db.Where("EXISTS (SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Search != "" {
|
||||||
|
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
|
||||||
|
if normalizedSearch == "" {
|
||||||
|
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
|
||||||
|
db = db.Order(expr)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
likeQuery := "%" + normalizedSearch + "%"
|
||||||
|
db = db.
|
||||||
|
Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id").
|
||||||
|
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
|
||||||
|
Joins("LEFT JOIN product_categories ON product_categories.id = project_flocks.product_category_id").
|
||||||
|
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
|
||||||
|
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
|
||||||
|
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
|
||||||
|
Where(`
|
||||||
|
LOWER(flocks.name) LIKE ?
|
||||||
|
OR LOWER(areas.name) LIKE ?
|
||||||
|
OR LOWER(product_categories.name) LIKE ?
|
||||||
|
OR LOWER(product_categories.code) LIKE ?
|
||||||
|
OR LOWER(fcrs.name) LIKE ?
|
||||||
|
OR LOWER(locations.name) LIKE ?
|
||||||
|
OR LOWER(locations.address) LIKE ?
|
||||||
|
OR LOWER(created_users.name) LIKE ?
|
||||||
|
OR LOWER(created_users.email) LIKE ?
|
||||||
|
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM kandangs
|
||||||
|
WHERE kandangs.project_flock_id = project_flocks.id
|
||||||
|
AND LOWER(kandangs.name) LIKE ?
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
likeQuery,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
|
||||||
|
db = db.Order(expr)
|
||||||
|
}
|
||||||
|
return db
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -167,12 +242,10 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(&entity.Kandang{}).
|
if err := s.attachKandangs(c.Context(), tx, createBody.Id, kandangIDs, createBody.CreatedBy); err != nil {
|
||||||
Where("id IN ?", kandangIDs).
|
|
||||||
Updates(map[string]any{"project_flock_id": createBody.Id}).Error; err != nil {
|
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
s.Log.Errorf("Failed to assign kandangs to projectflock: %+v", err)
|
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", createBody.Id, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to assign kandangs")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
if err := tx.Commit().Error; err != nil {
|
||||||
@@ -315,22 +388,18 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(toDetach) > 0 {
|
if len(toDetach) > 0 {
|
||||||
if err := tx.Model(&entity.Kandang{}).
|
if err := s.detachKandangs(c.Context(), tx, id, toDetach, false); err != nil {
|
||||||
Where("id IN ?", toDetach).
|
|
||||||
Updates(map[string]any{"project_flock_id": nil}).Error; err != nil {
|
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
s.Log.Errorf("Failed to detach kandangs: %+v", err)
|
s.Log.Errorf("Failed to detach kandangs from projectflock %d: %+v", id, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(toAttach) > 0 {
|
if len(toAttach) > 0 {
|
||||||
if err := tx.Model(&entity.Kandang{}).
|
if err := s.attachKandangs(c.Context(), tx, id, toAttach, existing.CreatedBy); err != nil {
|
||||||
Where("id IN ?", toAttach).
|
|
||||||
Updates(map[string]any{"project_flock_id": id}).Error; err != nil {
|
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
s.Log.Errorf("Failed to attach kandangs: %+v", err)
|
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", id, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,12 +432,10 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
for i, k := range existing.Kandangs {
|
for i, k := range existing.Kandangs {
|
||||||
ids[i] = k.Id
|
ids[i] = k.Id
|
||||||
}
|
}
|
||||||
if err := tx.Model(&entity.Kandang{}).
|
if err := s.detachKandangs(c.Context(), tx, id, ids, true); err != nil {
|
||||||
Where("id IN ?", ids).
|
|
||||||
Updates(map[string]any{"project_flock_id": nil}).Error; err != nil {
|
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
s.Log.Errorf("Failed to detach kandangs before delete: %+v", err)
|
s.Log.Errorf("Failed to detach kandangs before deleting projectflock %d: %+v", id, err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,3 +498,93 @@ func relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool
|
|||||||
return commonRepo.Exists[T](ctx, db, id)
|
return commonRepo.Exists[T](ctx, db, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []string {
|
||||||
|
direction := "ASC"
|
||||||
|
if strings.ToLower(sortOrder) == "desc" {
|
||||||
|
direction = "DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sortBy {
|
||||||
|
case "area":
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
|
||||||
|
fmt.Sprintf("project_flocks.id %s", direction),
|
||||||
|
}
|
||||||
|
case "location":
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
|
||||||
|
fmt.Sprintf("project_flocks.id %s", direction),
|
||||||
|
}
|
||||||
|
case "kandangs":
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction),
|
||||||
|
fmt.Sprintf("project_flocks.id %s", direction),
|
||||||
|
}
|
||||||
|
case "period":
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("project_flocks.period %s", direction),
|
||||||
|
fmt.Sprintf("project_flocks.id %s", direction),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return []string{
|
||||||
|
"project_flocks.created_at DESC",
|
||||||
|
"project_flocks.updated_at DESC",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint, createdBy uint) error {
|
||||||
|
if len(kandangIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(&entity.Kandang{}).
|
||||||
|
Where("id IN ?", kandangIDs).
|
||||||
|
Updates(map[string]any{"project_flock_id": projectFlockID}).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
||||||
|
}
|
||||||
|
|
||||||
|
pivotRepo := s.pivotRepoWithTx(tx)
|
||||||
|
records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
|
||||||
|
for i, id := range kandangIDs {
|
||||||
|
records[i] = &entity.ProjectFlockKandang{
|
||||||
|
ProjectFlockId: projectFlockID,
|
||||||
|
KandangId: id,
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := pivotRepo.CreateMany(ctx, records); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error {
|
||||||
|
if len(kandangIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]any{"project_flock_id": nil}
|
||||||
|
if resetStatus {
|
||||||
|
updates["status"] = string(utils.KandangStatusNonActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(&entity.Kandang{}).
|
||||||
|
Where("id IN ?", kandangIDs).
|
||||||
|
Updates(updates).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.pivotRepoWithTx(tx).MarkDetached(ctx, projectFlockID, kandangIDs, time.Now()); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) pivotRepoWithTx(tx *gorm.DB) repository.ProjectFlockKandangRepository {
|
||||||
|
if s.PivotRepo == nil {
|
||||||
|
return repository.NewProjectFlockKandangRepository(tx)
|
||||||
|
}
|
||||||
|
return s.PivotRepo.WithTx(tx)
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
SortBy string `query:"sort_by" validate:"omitempty,oneof=area location kandangs period"`
|
||||||
|
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
|
||||||
|
AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"`
|
||||||
|
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
|
||||||
|
Period int `query:"period" validate:"omitempty,number,gt=0"`
|
||||||
|
KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ func setupIntegrationApp(t *testing.T) (*fiber.App, *gorm.DB) {
|
|||||||
&entities.Location{},
|
&entities.Location{},
|
||||||
&entities.Flock{},
|
&entities.Flock{},
|
||||||
&entities.ProjectFlock{},
|
&entities.ProjectFlock{},
|
||||||
|
&entities.ProjectFlockKandang{},
|
||||||
&entities.Kandang{},
|
&entities.Kandang{},
|
||||||
&entities.Warehouse{},
|
&entities.Warehouse{},
|
||||||
&entities.Uom{},
|
&entities.Uom{},
|
||||||
@@ -191,6 +192,15 @@ func fetchCustomer(t *testing.T, db *gorm.DB, id uint) entities.Customer {
|
|||||||
return customer
|
return customer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchKandang(t *testing.T, db *gorm.DB, id uint) entities.Kandang {
|
||||||
|
t.Helper()
|
||||||
|
var kandang entities.Kandang
|
||||||
|
if err := db.Preload("ProjectFlock").First(&kandang, id).Error; err != nil {
|
||||||
|
t.Fatalf("failed to fetch kandang: %v", err)
|
||||||
|
}
|
||||||
|
return kandang
|
||||||
|
}
|
||||||
|
|
||||||
func createSupplier(t *testing.T, app *fiber.App, name, alias, category string) uint {
|
func createSupplier(t *testing.T, app *fiber.App, name, alias, category string) uint {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
identifier := strings.ToLower(strings.ReplaceAll(name, " ", "_"))
|
identifier := strings.ToLower(strings.ReplaceAll(name, " ", "_"))
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProjectFlockSummary(t *testing.T) {
|
func TestProjectFlockSummary(t *testing.T) {
|
||||||
app, _ := setupIntegrationApp(t)
|
app, db := setupIntegrationApp(t)
|
||||||
|
|
||||||
areaID := createArea(t, app, "Area Project")
|
areaID := createArea(t, app, "Area Project")
|
||||||
locationID := createLocation(t, app, "Location Project", "Address", areaID)
|
locationID := createLocation(t, app, "Location Project", "Address", areaID)
|
||||||
@@ -95,6 +99,21 @@ func TestProjectFlockSummary(t *testing.T) {
|
|||||||
t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period)
|
t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pivotRecords []entities.ProjectFlockKandang
|
||||||
|
if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil {
|
||||||
|
t.Fatalf("failed to fetch pivot records: %v", err)
|
||||||
|
}
|
||||||
|
if len(pivotRecords) != 1 {
|
||||||
|
t.Fatalf("expected 1 pivot record, got %d", len(pivotRecords))
|
||||||
|
}
|
||||||
|
firstPivotRecord := pivotRecords[0]
|
||||||
|
if firstPivotRecord.KandangId != kandangID {
|
||||||
|
t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId)
|
||||||
|
}
|
||||||
|
if firstPivotRecord.DetachedAt != nil {
|
||||||
|
t.Fatalf("expected pivot DetachedAt to be nil for active assignment, got %v", firstPivotRecord.DetachedAt)
|
||||||
|
}
|
||||||
|
|
||||||
secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1)
|
secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1)
|
||||||
secondPayload := map[string]any{
|
secondPayload := map[string]any{
|
||||||
"flock_id": flockID,
|
"flock_id": flockID,
|
||||||
@@ -121,6 +140,21 @@ func TestProjectFlockSummary(t *testing.T) {
|
|||||||
t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period)
|
t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pivotRecords = nil
|
||||||
|
if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil {
|
||||||
|
t.Fatalf("failed to fetch second pivot records: %v", err)
|
||||||
|
}
|
||||||
|
if len(pivotRecords) != 1 {
|
||||||
|
t.Fatalf("expected 1 pivot record for second project, got %d", len(pivotRecords))
|
||||||
|
}
|
||||||
|
secondPivotRecord := pivotRecords[0]
|
||||||
|
if secondPivotRecord.KandangId != secondKandangID {
|
||||||
|
t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId)
|
||||||
|
}
|
||||||
|
if secondPivotRecord.DetachedAt != nil {
|
||||||
|
t.Fatalf("expected second pivot DetachedAt to be nil, got %v", secondPivotRecord.DetachedAt)
|
||||||
|
}
|
||||||
|
|
||||||
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
|
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
|
||||||
if resp.StatusCode != fiber.StatusOK {
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
t.Fatalf("expected 200 when fetching summary, got %d: %s", resp.StatusCode, string(body))
|
t.Fatalf("expected 200 when fetching summary, got %d: %s", resp.StatusCode, string(body))
|
||||||
@@ -144,11 +178,49 @@ func TestProjectFlockSummary(t *testing.T) {
|
|||||||
t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body))
|
t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
firstKandang := fetchKandang(t, db, kandangID)
|
||||||
|
if firstKandang.ProjectFlockId != nil {
|
||||||
|
t.Fatalf("expected project_flock_id to be nil after delete, got %v", *firstKandang.ProjectFlockId)
|
||||||
|
}
|
||||||
|
if firstKandang.Status != string(utils.KandangStatusNonActive) {
|
||||||
|
t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstPivot entities.ProjectFlockKandang
|
||||||
|
if err := db.First(&firstPivot, firstPivotRecord.Id).Error; err != nil {
|
||||||
|
t.Fatalf("failed to reload first pivot record: %v", err)
|
||||||
|
}
|
||||||
|
if firstPivot.DetachedAt == nil {
|
||||||
|
t.Fatalf("expected first pivot DetachedAt to be set after delete")
|
||||||
|
}
|
||||||
|
if firstPivot.ProjectFlockId != createResp.Data.Id {
|
||||||
|
t.Fatalf("expected first pivot project_flock_id %d, got %d", createResp.Data.Id, firstPivot.ProjectFlockId)
|
||||||
|
}
|
||||||
|
|
||||||
resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil)
|
resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil)
|
||||||
if resp.StatusCode != fiber.StatusOK {
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body))
|
t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secondKandang := fetchKandang(t, db, secondKandangID)
|
||||||
|
if secondKandang.ProjectFlockId != nil {
|
||||||
|
t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId)
|
||||||
|
}
|
||||||
|
if secondKandang.Status != string(utils.KandangStatusNonActive) {
|
||||||
|
t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondPivot entities.ProjectFlockKandang
|
||||||
|
if err := db.First(&secondPivot, secondPivotRecord.Id).Error; err != nil {
|
||||||
|
t.Fatalf("failed to reload second pivot record: %v", err)
|
||||||
|
}
|
||||||
|
if secondPivot.DetachedAt == nil {
|
||||||
|
t.Fatalf("expected second pivot DetachedAt to be set after delete")
|
||||||
|
}
|
||||||
|
if secondPivot.ProjectFlockId != createRespSecond.Data.Id {
|
||||||
|
t.Fatalf("expected second pivot project_flock_id %d, got %d", createRespSecond.Data.Id, secondPivot.ProjectFlockId)
|
||||||
|
}
|
||||||
|
|
||||||
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
|
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
|
||||||
if resp.StatusCode != fiber.StatusOK {
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body))
|
t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body))
|
||||||
@@ -166,3 +238,178 @@ func TestProjectFlockSummary(t *testing.T) {
|
|||||||
func uintToString(v uint) string {
|
func uintToString(v uint) string {
|
||||||
return fmt.Sprintf("%d", v)
|
return fmt.Sprintf("%d", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProjectFlockSearchByRelatedFields(t *testing.T) {
|
||||||
|
app, _ := setupIntegrationApp(t)
|
||||||
|
|
||||||
|
areaID := createArea(t, app, "Area Search Target")
|
||||||
|
locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID)
|
||||||
|
flockID := createFlock(t, app, "Flock Search Target")
|
||||||
|
categoryID := createProductCategory(t, app, "Category Search Target", "CATGT")
|
||||||
|
fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{
|
||||||
|
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
|
||||||
|
})
|
||||||
|
kandangID := createKandang(t, app, "Kandang Search Target", locationID, 1)
|
||||||
|
|
||||||
|
createPayload := map[string]any{
|
||||||
|
"flock_id": flockID,
|
||||||
|
"area_id": areaID,
|
||||||
|
"product_category_id": categoryID,
|
||||||
|
"fcr_id": fcrID,
|
||||||
|
"location_id": locationID,
|
||||||
|
"kandang_ids": []uint{kandangID},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
|
||||||
|
if resp.StatusCode != fiber.StatusCreated {
|
||||||
|
t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var createResp struct {
|
||||||
|
Data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &createResp); err != nil {
|
||||||
|
t.Fatalf("failed to parse create response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTerms := []string{
|
||||||
|
"Flock Search Target",
|
||||||
|
"Area Search Target",
|
||||||
|
"Category Search Target",
|
||||||
|
"CATGT",
|
||||||
|
"FCR Search Target",
|
||||||
|
"Kandang Search Target",
|
||||||
|
"Location Search Target",
|
||||||
|
"Location Address Target",
|
||||||
|
"Tester",
|
||||||
|
"1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, term := range searchTerms {
|
||||||
|
path := "/api/production/project_flocks?search=" + url.QueryEscape(term)
|
||||||
|
resp, body := doJSONRequest(t, app, http.MethodGet, path, nil)
|
||||||
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
|
t.Fatalf("expected 200 when searching for %q, got %d: %s", term, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp struct {
|
||||||
|
Data []struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
Meta struct {
|
||||||
|
TotalResults int64 `json:"total_results"`
|
||||||
|
} `json:"meta"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &listResp); err != nil {
|
||||||
|
t.Fatalf("failed to parse list response for %q: %v", term, err)
|
||||||
|
}
|
||||||
|
if listResp.Meta.TotalResults == 0 {
|
||||||
|
t.Fatalf("expected at least one result when searching for %q", term)
|
||||||
|
}
|
||||||
|
if len(listResp.Data) == 0 {
|
||||||
|
t.Fatalf("expected data when searching for %q", term)
|
||||||
|
}
|
||||||
|
if listResp.Data[0].Id != createResp.Data.Id {
|
||||||
|
t.Fatalf("expected project flock id %d for search term %q, got %d", createResp.Data.Id, term, listResp.Data[0].Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProjectFlockSorting(t *testing.T) {
|
||||||
|
app, _ := setupIntegrationApp(t)
|
||||||
|
|
||||||
|
areaA := createArea(t, app, "Area Alpha")
|
||||||
|
areaB := createArea(t, app, "Area Beta")
|
||||||
|
|
||||||
|
locationA := createLocation(t, app, "Location Alpha", "Address Alpha", areaA)
|
||||||
|
locationB := createLocation(t, app, "Location Beta", "Address Beta", areaB)
|
||||||
|
|
||||||
|
flockOne := createFlock(t, app, "Flock Sort One")
|
||||||
|
flockTwo := createFlock(t, app, "Flock Sort Two")
|
||||||
|
|
||||||
|
categoryID := createProductCategory(t, app, "Category Sort", "CSORT")
|
||||||
|
fcrID := createFcr(t, app, "FCR Sort", []map[string]any{
|
||||||
|
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
|
||||||
|
})
|
||||||
|
|
||||||
|
kandangOne := createKandang(t, app, "Kandang Sort One", locationA, 1)
|
||||||
|
kandangTwo := createKandang(t, app, "Kandang Sort Two", locationB, 1)
|
||||||
|
kandangThree := createKandang(t, app, "Kandang Sort Three", locationB, 1)
|
||||||
|
|
||||||
|
projectOnePayload := map[string]any{
|
||||||
|
"flock_id": flockOne,
|
||||||
|
"area_id": areaA,
|
||||||
|
"product_category_id": categoryID,
|
||||||
|
"fcr_id": fcrID,
|
||||||
|
"location_id": locationA,
|
||||||
|
"kandang_ids": []uint{kandangOne},
|
||||||
|
}
|
||||||
|
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload)
|
||||||
|
if resp.StatusCode != fiber.StatusCreated {
|
||||||
|
t.Fatalf("expected 201 for project one, got %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
projectOneID := parseProjectFlockID(t, body)
|
||||||
|
|
||||||
|
projectTwoPayload := map[string]any{
|
||||||
|
"flock_id": flockTwo,
|
||||||
|
"area_id": areaB,
|
||||||
|
"product_category_id": categoryID,
|
||||||
|
"fcr_id": fcrID,
|
||||||
|
"location_id": locationB,
|
||||||
|
"kandang_ids": []uint{kandangTwo, kandangThree},
|
||||||
|
}
|
||||||
|
resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload)
|
||||||
|
if resp.StatusCode != fiber.StatusCreated {
|
||||||
|
t.Fatalf("expected 201 for project two, got %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
projectTwoID := parseProjectFlockID(t, body)
|
||||||
|
|
||||||
|
updatePeriodPayload := map[string]any{"period": 5}
|
||||||
|
resp, body = doJSONRequest(t, app, http.MethodPatch, "/api/production/project_flocks/"+uintToString(projectTwoID), updatePeriodPayload)
|
||||||
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
|
t.Fatalf("expected 200 when updating period, got %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertOrder := func(t *testing.T, app *fiber.App, query string, expectedFirst uint) {
|
||||||
|
t.Helper()
|
||||||
|
resp, body := doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks?"+query, nil)
|
||||||
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
|
t.Fatalf("expected 200 for query %q, got %d: %s", query, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
var listResp struct {
|
||||||
|
Data []struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &listResp); err != nil {
|
||||||
|
t.Fatalf("failed to parse list response for %q: %v", query, err)
|
||||||
|
}
|
||||||
|
if len(listResp.Data) == 0 {
|
||||||
|
t.Fatalf("expected data for query %q", query)
|
||||||
|
}
|
||||||
|
if listResp.Data[0].Id != expectedFirst {
|
||||||
|
t.Fatalf("expected first id %d for query %q, got %d", expectedFirst, query, listResp.Data[0].Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertOrder(t, app, "sort_by=area&sort_order=asc", projectOneID)
|
||||||
|
assertOrder(t, app, "sort_by=location&sort_order=desc", projectTwoID)
|
||||||
|
assertOrder(t, app, "sort_by=period&sort_order=desc", projectTwoID)
|
||||||
|
assertOrder(t, app, "sort_by=kandangs&sort_order=desc", projectTwoID)
|
||||||
|
assertOrder(t, app, "sort_by=kandangs&sort_order=asc", projectOneID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProjectFlockID(t *testing.T, body []byte) uint {
|
||||||
|
t.Helper()
|
||||||
|
var resp struct {
|
||||||
|
Data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &resp); err != nil {
|
||||||
|
t.Fatalf("failed to parse project flock response: %v", err)
|
||||||
|
}
|
||||||
|
return resp.Data.Id
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user