Compare commits

...

16 Commits

Author SHA1 Message Date
Hafizh A. Y. a38491fef1 Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
[CHORE/BE] resolve conflicts development-before-sso ito chickin

See merge request mbugroup/lti-api!53
2025-11-05 08:57:49 +00:00
aguhh18 b234778634 Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-11-05 14:03:45 +07:00
Hafizh A. Y. 59e71856ac Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
[FIX/BE][US#75/TASK#119] : Refactor chickin support chickin Growing and LAYING also have approval flow

See merge request mbugroup/lti-api!50
2025-11-05 05:26:42 +00:00
aguhh18 1ee97b91a5 feat[BE-127]: Createing transfer laying create one, approvals, get one, get all, update, delete, but Still unfinished 2025-11-05 08:56:18 +07:00
aguhh18 3a5c49c511 fix[BE]: fix naming on project_flock_kandang dto to standarized project 2025-11-05 08:40:27 +07:00
aguhh18 48730e1b74 FIX[BE]: fix error handling on chickin service to better handler 2025-11-04 16:34:36 +07:00
aguhh18 8220e34302 FIX[BE]: fix logic on Chickin Laying not convert to layer but still Pullet, and inisiate laying transfer migration and base basic API 2025-11-04 08:24:38 +07:00
aguhh18 c72db5bd18 FIX[BE]: delete redudant kandang response on projectflockkandang getone API 2025-11-03 09:29:00 +07:00
aguhh18 86f37a89c1 Feat[BE]: add multilpple type of chickin growing and laying, make convertion product when chickin approved, add projectflockkandangid on projectflock api 2025-11-03 09:16:29 +07:00
aguhh18 20f1be2ef8 feat[BE]: Refactor Chickin create and approvals support chickin growing and chickin laying, and create get one project flock kandang API 2025-11-02 21:06:03 +07:00
Hafizh A. Y. 672c76d26d Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
[FIX/BE][US#75] Adjust "chickin delete one" code to match backend standard

See merge request mbugroup/lti-api!49
2025-10-31 09:48:27 +00:00
aguhh18 219a6a39ed Feat[BE]: refactored Chickin createone and implement approvals and add more needed constant 2025-10-31 15:33:31 +07:00
aguhh18 c91d84b652 feat[BE-127]: inisiate transfer laying for base template API 2025-10-31 14:30:45 +07:00
aguhh18 bf14ab7865 fix(BE): Change migration chickin and project flock population to refactored one 2025-10-31 14:27:08 +07:00
aguhh18 31bb28f7da Feat(BE-127): create migration for transfer to laying and inisiate module 2025-10-30 09:06:21 +07:00
aguhh18 a390d1d23a FIX[BE]: Fix Delete one on chickin match with BE standard 2025-10-29 14:19:08 +07:00
54 changed files with 3527 additions and 524 deletions
@@ -1,36 +0,0 @@
CREATE TABLE IF NOT EXISTS project_chickins (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
chick_in_date DATE NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
note TEXT,
created_by BIGINT NOT NULL,
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 = 'project_flock_kandangs') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by);
@@ -1,36 +0,0 @@
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
initial_quantity NUMERIC(15, 3) NOT NULL,
current_quantity NUMERIC(15, 3) NOT NULL,
reserved_quantity NUMERIC(15, 3),
created_by BIGINT NOT NULL,
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 = 'project_flock_kandangs') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_chickin_details (
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
@@ -1,22 +1,30 @@
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
ALTER TABLE kandangs
DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE project_chickins
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
-- Only alter if tables exist
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
ALTER TABLE project_chickins
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
ALTER TABLE project_flock_populations
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
ALTER TABLE project_flock_populations
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
END $$;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS laying_transfers CASCADE;
@@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS laying_transfers (
id BIGSERIAL PRIMARY KEY,
transfer_number VARCHAR(50) UNIQUE NOT NULL,
from_project_flock_id BIGINT NOT NULL,
to_project_flock_id BIGINT NOT NULL,
transfer_date DATE NOT NULL,
pending_usage_qty NUMERIC(15, 3),
usage_qty NUMERIC(15, 3),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_from_project_flock
FOREIGN KEY (from_project_flock_id)
REFERENCES project_flocks(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_to_project_flock
FOREIGN KEY (to_project_flock_id)
REFERENCES project_flocks(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE UNIQUE INDEX IF NOT EXISTS idx_laying_transfers_transfer_number ON laying_transfers (transfer_number)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers (created_by);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers (deleted_at);
@@ -0,0 +1,58 @@
-- ============================================
-- MIGRATION: project_chickins
-- ============================================
-- STEP 1: Hapus tabel jika sudah ada
DROP TABLE IF EXISTS project_chickins;
-- STEP 2: Buat tabel project_chickins
CREATE TABLE IF NOT EXISTS project_chickins (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
chick_in_date DATE NOT NULL,
usage_qty NUMERIC(15, 3) NOT NULL,
pending_usage_qty NUMERIC(15, 3) DEFAULT 0,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- STEP 3: FOREIGN KEYS
BEGIN;
-- Relasi ke project_flock_kandangs
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke product_warehouses
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke users
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
COMMIT;
-- STEP 4: INDEXES
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by);
-- Composite index for common queries
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_deleted ON project_chickins (
project_flock_kandang_id,
deleted_at
);
-- Index for soft delete queries
CREATE INDEX IF NOT EXISTS idx_chickins_deleted_at ON project_chickins (deleted_at);
@@ -0,0 +1,62 @@
-- ============================================
-- MIGRATION: project_flock_populations
-- ============================================
-- STEP 1: Hapus tabel jika sudah ada
DROP TABLE IF EXISTS project_flock_populations;
-- STEP 2: Buat tabel project_flock_populations
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_chickin_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
total_qty NUMERIC(15, 3) NOT NULL,
total_used_qty NUMERIC(15, 3) DEFAULT 0,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- STEP 3: FOREIGN KEYS
BEGIN;
-- Relasi ke project_chickins
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_chickin FOREIGN KEY (project_chickin_id) REFERENCES project_chickins (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke product_warehouses
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke users
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
COMMIT;
-- STEP 4: INDEXES
CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by);
-- Composite index for common queries
CREATE INDEX IF NOT EXISTS idx_populations_chickin_deleted ON project_flock_populations (
project_chickin_id,
deleted_at
);
-- Index for soft delete queries
CREATE INDEX IF NOT EXISTS idx_populations_deleted_at ON project_flock_populations (deleted_at);
-- Unique constraint: one population per chickin
CREATE UNIQUE INDEX IF NOT EXISTS idx_populations_chickin_unique ON project_flock_populations (project_chickin_id)
WHERE
deleted_at IS NULL;
@@ -0,0 +1,5 @@
-- Rollback laying_transfer_sources dan laying_transfer_targets tables
DROP TABLE IF EXISTS laying_transfer_targets CASCADE;
DROP TABLE IF EXISTS laying_transfer_sources CASCADE;
@@ -0,0 +1,93 @@
-- Create laying_transfer_sources dan laying_transfer_targets tables
-- 1. Create laying_transfer_sources table (detail sumber - kandang asal growing)
CREATE TABLE laying_transfer_sources (
id BIGSERIAL PRIMARY KEY,
laying_transfer_id BIGINT NOT NULL,
source_project_flock_kandang_id BIGINT NOT NULL,
product_warehouse_id BIGINT,
qty NUMERIC(15, 3) NOT NULL,
note TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Add foreign keys untuk laying_transfer_sources
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
ALTER TABLE laying_transfer_sources
ADD CONSTRAINT fk_laying_transfer_sources_laying_transfer_id
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE laying_transfer_sources
ADD CONSTRAINT fk_laying_transfer_sources_project_flock_kandang_id
FOREIGN KEY (source_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfer_sources
ADD CONSTRAINT fk_laying_transfer_sources_product_warehouse_id
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
END IF;
END $$;
-- 2. Create laying_transfer_targets table (detail tujuan - kandang laying)
CREATE TABLE laying_transfer_targets (
id BIGSERIAL PRIMARY KEY,
laying_transfer_id BIGINT NOT NULL,
target_project_flock_kandang_id BIGINT NOT NULL,
qty NUMERIC(15, 3) NOT NULL,
product_warehouse_id BIGINT,
note TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Add foreign keys untuk laying_transfer_targets
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
ALTER TABLE laying_transfer_targets
ADD CONSTRAINT fk_laying_transfer_targets_laying_transfer_id
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE laying_transfer_targets
ADD CONSTRAINT fk_laying_transfer_targets_project_flock_kandang_id
FOREIGN KEY (target_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfer_targets
ADD CONSTRAINT fk_laying_transfer_targets_product_warehouse_id
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
END IF;
END $$;
-- 3. Create indexes untuk laying_transfer_sources
CREATE INDEX idx_laying_transfer_sources_laying_transfer_id ON laying_transfer_sources (laying_transfer_id);
CREATE INDEX idx_laying_transfer_sources_source_kandang_id ON laying_transfer_sources (
source_project_flock_kandang_id
);
CREATE INDEX idx_laying_transfer_sources_product_warehouse_id ON laying_transfer_sources (product_warehouse_id);
CREATE INDEX idx_laying_transfer_sources_deleted_at ON laying_transfer_sources (deleted_at);
-- 4. Create indexes untuk laying_transfer_targets
CREATE INDEX idx_laying_transfer_targets_laying_transfer_id ON laying_transfer_targets (laying_transfer_id);
CREATE INDEX idx_laying_transfer_targets_target_kandang_id ON laying_transfer_targets (
target_project_flock_kandang_id
);
CREATE INDEX idx_laying_transfer_targets_product_warehouse_id ON laying_transfer_targets (product_warehouse_id);
CREATE INDEX idx_laying_transfer_targets_deleted_at ON laying_transfer_targets (deleted_at);
+39 -37
View File
@@ -571,52 +571,54 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Flags: []utils.FlagType{utils.FlagDOC},
},
{
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Name: "Ayam Pullet",
Brand: "MBU Pullet",
Sku: "PLT0001",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Category: "Pullet",
Price: 15000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPullet},
},
{
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Telur Konsumsi Baik",
Brand: "-",
Sku: "4",
Uom: "Unit",
Category: "Telur",
Price: 1,
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Unit",
Category: "Telur",
Price: 1,
Name: "Telur Konsumsi Baik",
Brand: "-",
Sku: "4",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "281 SPECIAL STARTER",
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingKandangTransfer struct {
Id uint `gorm:"primaryKey"`
KandangId uint
ProductWarehouseId uint
Qty float64 `gorm:"type:numeric(15,3)"`
LayingTransferId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
}
+29
View File
@@ -0,0 +1,29 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransfer struct {
Id uint `gorm:"primaryKey"`
TransferNumber string `gorm:"uniqueIndex;not null"`
FromProjectFlockId uint `gorm:"not null"`
ToProjectFlockId uint `gorm:"not null"`
TransferDate time.Time `gorm:"type:date;not null"`
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
UsageQty *float64 `gorm:"type:numeric(15,3)"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
@@ -0,0 +1,23 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransferSource struct {
Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"`
Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
@@ -0,0 +1,24 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransferTarget struct {
Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"`
TargetProjectFlockKandangId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
ProductWarehouseId *uint `gorm:""`
Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
TargetProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:TargetProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+7 -4
View File
@@ -12,13 +12,16 @@ type ProjectChickin struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
ChickInDate time.Time `gorm:"not null"`
Quantity float64 `gorm:"not null"`
Note string `gorm:"type:text"`
ProductWarehouseId uint `gorm:"not null"`
UsageQty float64 `gorm:"type:numeric(15,3);not null"`
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+13 -12
View File
@@ -7,17 +7,18 @@ import (
)
type ProjectFlockPopulation struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Id uint `gorm:"primaryKey"`
ProjectChickinId uint `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"`
TotalQty float64 `gorm:"type:numeric(15,3);not null"`
TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+1
View File
@@ -28,3 +28,4 @@ type ProjectFlock struct {
LatestApproval *Approval `gorm:"-" json:"-"`
}
+8 -8
View File
@@ -3,13 +3,13 @@ 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_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
@@ -82,6 +82,10 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"LOKASI",
"KANDANG",
},
"stock_log": map[string][]string{
"log_types": []string{"TRANSFER", "ADJUSTMENT"},
"transaction_types": []string{"INCREASE", "DECREASE"},
},
"supplier_categories": []string{
"BOP",
"SAPRONAK",
@@ -19,6 +19,8 @@ type ProductWarehouseRepository interface {
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
}
@@ -78,14 +80,14 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
err := r.DB().WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
Order("product_warehouses.created_at DESC")
// preload relations so nested Product and Warehouse are populated
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
if err != nil {
return nil, err
}
@@ -100,12 +102,12 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c
}
fmt.Println(warehouseId)
err := query.WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC").
Preload("Product").Preload("Warehouse").
First(&productWarehouse).Error
if err != nil {
return nil, err
@@ -146,3 +148,42 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
}
return nil
}
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
var product entity.Product
err := r.DB().WithContext(ctx).
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ?", categoryCode).
First(&product).Error
if err != nil {
return nil, err
}
return &product, nil
}
func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
Order("product_warehouses.created_at DESC").
Preload("Product").Preload("Warehouse").
Find(&productWarehouses).Error
if err != nil {
return nil, err
}
return productWarehouses, nil
}
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) {
var product entity.Product
err := r.DB().WithContext(ctx).
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
Where("flags.name = ?", flagName).
First(&product).Error
if err != nil {
return nil, err
}
return &product, nil
}
@@ -16,6 +16,7 @@ type WarehouseRepository interface {
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error)
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
}
type WarehouseRepositoryImpl struct {
@@ -60,3 +61,16 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
}
return &warehouse, nil
}
func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
var warehouse entity.Warehouse
err := r.db.WithContext(ctx).
Where("kandang_id = ?", kandangId).
Where("deleted_at IS NULL").
Order("id DESC").
First(&warehouse).Error
if err != nil {
return nil, err
}
return &warehouse, nil
}
@@ -1,7 +1,6 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
@@ -22,30 +21,84 @@ func NewChickinController(chickinService service.ChickinService) *ChickinControl
}
}
func (u *ChickinController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
// func (u *ChickinController) GetAll(c *fiber.Ctx) error {
// query := &validation.Query{
// Page: c.QueryInt("page", 1),
// Limit: c.QueryInt("limit", 10),
// ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
// }
// result, totalResults, err := u.ChickinService.GetAll(c, query)
// if err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Get all chickins successfully",
// Meta: response.Meta{
// Page: query.Page,
// Limit: query.Limit,
// TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
// TotalResults: totalResults,
// },
// Data: dto.ToChickinListDTOs(result),
// })
// }
// func (u *ChickinController) 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.ChickinService.GetOne(c, uint(id))
// if err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.Success{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Get chickin successfully",
// Data: dto.ToChickinListDTO(*result),
// })
// }
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, totalResults, err := u.ChickinService.GetAll(c, query)
results, err := u.ChickinService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
Code: fiber.StatusOK,
var (
data interface{}
message = "Create chickin successfully"
)
if len(results) == 1 {
data = dto.ToChickinListDTO(results[0])
} else {
message = "Create chickins successfully"
data = dto.ToChickinListDTOs(results)
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Get all chickins successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToChickinListDTOs(result),
Message: message,
Data: data,
})
}
@@ -67,95 +120,85 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error {
Code: fiber.StatusOK,
Status: "success",
Message: "Get chickin successfully",
Data: dto.ToChickinListDTO(*result),
Data: dto.ToChickinDetailDTO(*result),
})
}
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
// func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
// req := new(validation.Update)
// param := c.Params("id")
// id, err := strconv.Atoi(param)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
// }
// if err := c.BodyParser(req); err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
// }
// result, err := u.ChickinService.UpdateOne(c, req, uint(id))
// if err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.Success{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Update chickin successfully",
// Data: dto.ToChickinListDTO(*result),
// })
// }
// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
// param := c.Params("id")
// id, err := strconv.Atoi(param)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
// }
// if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.Common{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Delete chickin successfully",
// })
// }
func (u *ChickinController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ChickinService.CreateOne(c, req)
results, err := u.ChickinService.Approval(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create chickin successfully",
Data: dto.ToChickinListDTO(*result),
})
}
func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ChickinService.UpdateOne(c, req, uint(id))
if err != nil {
return err
var (
data interface{}
message = "Submit chickin approval successfully"
)
if len(results) == 1 {
data = dto.ToChickinListDTO(results[0])
} else {
message = "Submit chickin approvals successfully"
data = dto.ToChickinListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update chickin successfully",
Data: dto.ToChickinListDTO(*result),
})
}
func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete chickin successfully",
})
}
func (u *ChickinController) Approve(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ChickinService.Approve(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Approve chickin successfully",
Data: nil,
Message: message,
Data: data,
})
}
@@ -16,11 +16,13 @@ import (
// === DTO Structs (ordered) ===
type ChickinBaseDTO struct {
Id uint `json:"id"`
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
ChickInDate time.Time `json:"chick_in_date"`
Quantity float64 `json:"quantity"`
Note string `json:"note"`
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
}
type ProjectFlockDTO struct {
@@ -45,21 +47,32 @@ type ChickinSimpleDTO struct {
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"`
Quantity float64 `json:"quantity"`
Note string `json:"note"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
CreatedBy uint `json:"created_by"`
}
type ChickinListDTO struct {
ChickinBaseDTO
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ChickinDetailDTO struct {
ChickinListDTO
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
CreatedBy uint `json:"created_by"`
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// === Mapper Functions (ordered) ===
@@ -138,17 +151,22 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
}
func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO {
var pfk *ProjectFlockKandangDTO
if e.ProjectFlockKandang.Id != 0 {
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
pfk = &mapped
var projectFlockKandangId uint
// Check if ProjectFlockKandang relation is loaded
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 {
projectFlockKandangId = e.ProjectFlockKandang.Id
} else if e.ProjectFlockKandangId != 0 {
// If relation is not loaded but ID is available, use the ID
projectFlockKandangId = e.ProjectFlockKandangId
}
return ChickinBaseDTO{
Id: e.Id,
ProjectFlockKandang: pfk,
ChickInDate: e.ChickInDate,
Quantity: e.Quantity,
Note: e.Note,
Id: e.Id,
ProjectFlockKandangId: projectFlockKandangId,
ChickInDate: e.ChickInDate,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
}
}
@@ -157,29 +175,25 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO {
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
ChickInDate: e.ChickInDate,
Quantity: e.Quantity,
Note: e.Note,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
CreatedBy: e.CreatedBy,
}
}
func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO {
var createdUser *userBaseDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser)
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
var pfk *ProjectFlockKandangDTO
if e.ProjectFlockKandang.Id != 0 {
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
pfk = &mapped
}
return ChickinListDTO{
ChickinBaseDTO: ToChickinBaseDTO(e),
ProjectFlockKandang: pfk,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
ChickinBaseDTO: ToChickinBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
@@ -200,7 +214,31 @@ func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO {
}
func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO {
var createdUser *userBaseDTO.UserBaseDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
return ChickinDetailDTO{
ChickinListDTO: ToChickinListDTO(e),
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
ChickInDate: e.ChickInDate,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
CreatedBy: e.CreatedBy,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO {
result := make([]ChickinDetailDTO, len(e))
for i, r := range e {
result[i] = ToChickinDetailDTO(r)
}
return result
}
@@ -1,10 +1,15 @@
package chickins
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
@@ -15,6 +20,8 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type ChickinModule struct{}
@@ -32,6 +39,12 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
}
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
@@ -11,21 +11,26 @@ import (
type ProjectChickinRepository interface {
repository.BaseRepository[entity.ProjectChickin]
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
}
type ChickinRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectChickin]
db *gorm.DB
}
func NewChickinRepository(db *gorm.DB) ProjectChickinRepository {
return &ChickinRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db),
db: db,
}
}
func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) {
var chickin entity.ProjectChickin
err := r.DB().WithContext(ctx).
err := r.db.WithContext(ctx).
Where("project_floc_id = ?", projectFlockID).
Where("deleted_at IS NULL").
First(&chickin).Error
@@ -34,3 +39,43 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
}
return &chickin, nil
}
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Order("created_at DESC").
Find(&chickins).Error
if err != nil {
return nil, err
}
return chickins, nil
}
func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where("usage_qty = 0").
Where("pending_usage_qty > 0").
Order("created_at DESC").
Find(&chickins).Error
if err != nil {
return nil, err
}
return chickins, nil
}
func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where("pending_usage_qty > 0").
Select("COALESCE(SUM(pending_usage_qty), 0)").
Row().Scan(&total)
if err != nil {
return 0, err
}
return total, nil
}
@@ -1,6 +1,8 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
@@ -8,6 +10,10 @@ import (
type ProjectChickinDetailRepository interface {
repository.BaseRepository[entity.ProjectChickinDetail]
CreateOne(ctx context.Context, entity *entity.ProjectChickinDetail, modifier func(*gorm.DB) *gorm.DB) error
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error)
WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository
}
type ChickinDetailRepositoryImpl struct {
@@ -19,3 +25,22 @@ func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository {
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
}
}
func (r *ChickinDetailRepositoryImpl) WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository {
return &ChickinDetailRepositoryImpl{BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](tx)}
}
func (r *ChickinDetailRepositoryImpl) DB() *gorm.DB {
return r.BaseRepositoryImpl.DB()
}
func (r *ChickinDetailRepositoryImpl) GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) {
var records []entity.ProjectChickinDetail
if err := r.DB().WithContext(ctx).Where("project_chickin_id = ?", projectChickinID).Find(&records).Error; err != nil {
return nil, err
}
if len(records) == 0 {
return nil, gorm.ErrRecordNotFound
}
return records, nil
}
@@ -20,10 +20,10 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
// route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Post("/:id/approve", ctrl.Approve)
// route.Patch("/:id", ctrl.UpdateOne)
// route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval)
}
@@ -2,7 +2,11 @@ package service
import (
"errors"
"fmt"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -21,10 +25,10 @@ import (
type ChickinService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
Approve(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
}
type chickinService struct {
@@ -76,6 +80,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
}
offset := (params.Page - 1) * params.Limit
chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.ProjectFlockKandangId != 0 {
@@ -103,112 +108,164 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e
return chickin, nil
}
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) {
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return nil, err
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
}
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId)
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId)
if err != nil {
s.Log.Errorf("Failed to get warehouse: %+v", err)
return nil, err
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found")
}
// move complex DB query into repository for cleaner service
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
if err != nil {
s.Log.Errorf("Failed to get product warehouses: %+v", err)
return nil, err
}
if len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
}
totalQuantity := 0.0
for _, pw := range productWarehouses {
totalQuantity += pw.Quantity
var productWarehouses []entity.ProductWarehouse
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
var productCategoryCode string
switch category {
case string(utils.ProjectFlockCategoryGrowing):
productCategoryCode = "DOC"
case string(utils.ProjectFlockCategoryLaying):
productCategoryCode = "PULLET"
default:
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category))
}
if totalQuantity < 1 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id)
if err != nil || len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category)))
}
chickinDate, err := utils.ParseDateString(req.ChickInDate)
if err != nil {
s.Log.Errorf("Failed to parse chickin date: %+v", err)
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format")
}
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: projectflockkandang.Id,
ChickInDate: chickinDate,
Quantity: totalQuantity,
Note: req.Note,
CreatedBy: 1, //todo: ganti dengan user login
}
err = s.Repository.CreateOne(c.Context(), newChickin, nil)
if err != nil {
s.Log.Errorf("Failed to create chickin: %+v", err)
return nil, err
}
// Update semua product warehouse: set quantity jadi 0
for _, pw := range productWarehouses {
err = s.ProductWarehouseRepo.PatchOne(c.Context(), pw.Id, map[string]any{
"quantity": 0,
}, nil)
actorID := uint(1) // todo nanti ambil dari auth context
newChikins := make([]*entity.ProjectChickin, 0)
for _, productWarehouse := range productWarehouses {
availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category)
if err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return nil, err
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", productWarehouse.Id))
}
newChickinDetail := &entity.ProjectChickinDetail{
ProjectChickinId: newChickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: 1, // todo: ganti dengan user login
if availableQty <= 0 {
continue
}
err = s.ProjectChickinDetailRepo.CreateOne(c.Context(), newChickinDetail, nil)
if err != nil {
s.Log.Errorf("Failed to create chickin detail: %+v", err)
return nil, err
}
}
existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to get project flock population: %+v", err)
return nil, err
}
if existingPopulation != nil {
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), existingPopulation.Id, map[string]any{
"reserved_quantity": newChickin.Quantity + existingPopulation.ReservedQuantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return nil, err
}
} else {
newPopulation := &entity.ProjectFlockPopulation{
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: req.ProjectFlockKandangId,
InitialQuantity: 0,
CurrentQuantity: 0,
ReservedQuantity: newChickin.Quantity,
CreatedBy: 1, // todo: ganti dengan user login
}
err = s.ProjectflockPopulationRepo.CreateOne(c.Context(), newPopulation, nil)
if err != nil {
s.Log.Errorf("Failed to create project flock population: %+v", err)
return nil, err
ChickInDate: chickinDate,
UsageQty: 0,
PendingUsageQty: availableQty,
ProductWarehouseId: productWarehouse.Id,
Notes: req.Note,
CreatedBy: actorID,
}
newChikins = append(newChikins, newChickin)
}
return s.GetOne(c, newChickin.Id)
if len(newChikins) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create")
}
existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins")
}
isFirstTime := len(existingChikins) == 0
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
}
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
}
if category == string(utils.ProjectFlockCategoryLaying) {
for _, chickin := range newChikins {
updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity")
}
}
}
var approvalAction entity.ApprovalAction
if isFirstTime {
approvalAction = entity.ApprovalActionCreated
} else {
approvalAction = entity.ApprovalActionUpdated
}
if latest == nil {
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
projectFlockKandang.Id,
utils.ProjectFlockKandangStepPengajuan,
&approvalAction,
actorID,
nil); err != nil {
if !errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
}
}
} else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
projectFlockKandang.Id,
utils.ProjectFlockKandangStepPengajuan,
&approvalAction,
actorID,
nil); err != nil {
if !errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
}
}
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
}
result := make([]entity.ProjectChickin, 0, len(newChikins))
for _, chickin := range newChikins {
loaded, err := s.GetOne(c, chickin.Id)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reload chickin %d with relations: %v", chickin.Id, err))
}
result = append(result, *loaded)
}
if len(result) == 0 {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins")
}
return result, nil
}
func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) {
@@ -222,7 +279,7 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["chick_in_date"] = req.ChickInDate
}
if req.Note != "" {
updateBody["note"] = req.Note
updateBody["notes"] = req.Note
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
@@ -240,174 +297,335 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
}
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
db := s.Repository.DB()
tx := db.WithContext(c.Context()).Begin()
if tx.Error != nil {
s.Log.Errorf("Failed to begin transaction: %+v", tx.Error)
return tx.Error
}
rollback := func(err error) error {
if rerr := tx.Rollback().Error; rerr != nil {
s.Log.Errorf("Rollback failed: %+v", rerr)
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
}
return err
}
chickinRepoTx := s.Repository.WithTx(tx)
pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx)
return nil
}
chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
}
if err != nil {
s.Log.Errorf("Failed get chickin by id: %+v", err)
return rollback(err)
}
func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) {
availableQty := productWarehouse.Quantity
var population entity.ProjectFlockPopulation
if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found"))
}
s.Log.Errorf("Failed to get project flock population: %+v", err)
return rollback(err)
}
if category == string(utils.ProjectFlockCategoryGrowing) {
var totalPendingQty float64
newReserved := population.ReservedQuantity - chickin.Quantity
if newReserved < 0 {
newReserved = 0
}
if err := tx.WithContext(c.Context()).Model(&entity.ProjectFlockPopulation{}).Where("id = ?", population.Id).Updates(map[string]any{"reserved_quantity": newReserved}).Error; err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return rollback(err)
}
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err == nil {
for _, chickin := range chickins {
restoreFromDetails := func() (bool, error) {
var details []entity.ProjectChickinDetail
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
return false, err
}
if len(details) == 0 {
return false, nil
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
}
for _, d := range details {
var pw entity.ProductWarehouse
if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
availableQty = productWarehouse.Quantity - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
} else if category == string(utils.ProjectFlockCategoryLaying) {
var totalPopulation float64
var totalPendingQty float64
populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id)
if err == nil {
for _, pop := range populations {
totalPopulation += pop.TotalQty
}
}
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err == nil {
for _, chickin := range chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
}
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
}
return availableQty, nil
}
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected):
action = entity.ApprovalActionRejected
case string(entity.ApprovalActionApproved):
action = entity.ApprovalActionApproved
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
if len(approvableIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
for _, id := range approvableIDs {
idCopy := id
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
return nil, err
}
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
}
if latestApproval == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
}
if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
}
}
step := utils.ProjectFlockKandangStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.ProjectFlockKandangStepDisetujui
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs {
actorID := uint(1) // todo nanti ambil dari auth context
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
}
if action == entity.ApprovalActionApproved {
chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID))
}
kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
}
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
if category == string(utils.ProjectFlockCategoryGrowing) {
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
}
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
}
} else if category == string(utils.ProjectFlockCategoryLaying) {
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
}
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
}
}
}
if action == entity.ApprovalActionRejected {
chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID))
}
if len(chickins) == 0 {
continue
}
return false, err
}
updatedQuantity := pw.Quantity + d.Quantity
if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
return false, err
kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
}
categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category))
for _, chickin := range chickins {
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId))
}
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id))
}
}
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id))
}
}
}
}
}
return nil
})
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil {
return false, err
}
return true, nil
}
restored, err := restoreFromDetails()
if err != nil {
s.Log.Errorf("Failed to restore from chickin details: %+v", err)
return rollback(err)
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
if !restored {
updated := make([]entity.ProjectChickin, 0)
for _, kandangID := range approvableIDs {
var chickins []entity.ProjectChickin
if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load approved chickins")
}
updated = append(updated, chickins...)
}
projectflockkandang, err := pfkRepoTx.GetByID(c.Context(), population.ProjectFlockKandangId)
return updated, nil
}
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) {
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
if err == nil && len(products) > 0 {
return &products[0], nil
}
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
if err != nil {
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
}
if product == nil {
return nil, fmt.Errorf("no %s product found in system", categoryCode)
}
newPW := &entity.ProductWarehouse{
ProductId: product.Id,
WarehouseId: warehouseId,
Quantity: 0,
CreatedBy: actorID,
}
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err)
}
return newPW, nil
}
func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
if targetPW == nil || targetPW.Id == 0 {
return fmt.Errorf("invalid target product warehouse")
}
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
for _, chickin := range chickins {
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return rollback(err)
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
}
var warehouse entity.Warehouse
if err := tx.WithContext(c.Context()).Where("kandang_id = ?", projectflockkandang.KandangId).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang"))
if populationExists {
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
continue
}
quantityToConvert := chickin.PendingUsageQty
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
"usage_qty": quantityToConvert,
"pending_usage_qty": 0,
}, nil); err != nil {
return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err)
}
if chickin.ProductWarehouseId != targetPW.Id {
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
"quantity": gorm.Expr("quantity - ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
}
return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err)
}
s.Log.Errorf("Failed to get warehouse: %+v", err)
return rollback(err)
}
productWarehouse, err := s.ProductWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(
c.Context(),
"DOC",
warehouse.Id,
tx,
)
if err != nil {
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse"))
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
}
s.Log.Errorf("Failed to get product warehouse: %+v", err)
return rollback(err)
return fmt.Errorf("failed to update target warehouse quantity: %w", err)
}
updatedQuantity := productWarehouse.Quantity + chickin.Quantity
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return rollback(err)
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: targetPW.Id,
TotalQty: quantityToConvert,
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
}
}
// delete chickin (single place)
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
return err
}
s.Log.Errorf("Failed to delete chickin: %+v", err)
return rollback(err)
}
if err := tx.Commit().Error; err != nil {
s.Log.Errorf("Failed to commit transaction: %+v", err)
return rollback(err)
}
return nil
}
func (s *chickinService) Approve(c *fiber.Ctx, id uint) error {
// todo: ini contoh akhir jika sudah approved
chickin, err := s.Repository.GetByID(
c.Context(),
id,
nil,
)
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
}
if err != nil {
s.Log.Errorf("Failed get chickin by id: %+v", err)
return err
}
population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId)
if err != nil {
s.Log.Errorf("Failed to get project flock population: %+v", err)
return err
}
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{
"reserved_quantity": population.ReservedQuantity - chickin.Quantity,
"initial_quantity": population.InitialQuantity + chickin.Quantity,
"current_quantity": population.CurrentQuantity + chickin.Quantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return err
}
return nil
@@ -3,7 +3,7 @@ package validation
type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
Note string `json:"note" validate:"omitempty`
Note string `json:"note" validate:"omitempty"`
}
type Update struct {
@@ -16,3 +16,9 @@ type Query struct {
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
}
type Approve struct {
Action string `json:"action" validate:"required_strict"`
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
@@ -0,0 +1,76 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ProjectFlockKandangController struct {
ProjectFlockKandangService service.ProjectFlockKandangService
}
func NewProjectFlockKandangController(projectFlockKandangService service.ProjectFlockKandangService) *ProjectFlockKandangController {
return &ProjectFlockKandangController{
ProjectFlockKandangService: projectFlockKandangService,
}
}
func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all projectFlockKandangs successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProjectFlockKandangListDTOs(result),
})
}
func (u *ProjectFlockKandangController) 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, availableQtys, err := u.ProjectFlockKandangService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get projectFlockKandang successfully",
Data: dto.ToProjectFlockKandangListDTOWithAvailableQty(*result, availableQtys),
})
}
@@ -0,0 +1,330 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs (ordered) ===
type ProjectFlockKandangBaseDTO struct {
Id uint `json:"id"`
}
type ProjectFlockDTO struct {
Id uint `json:"id"`
Period int `json:"period"`
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type KandangDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
type ProductWarehouseDTO struct {
Id uint `json:"id"`
Product *productDTO.ProductBaseDTO `json:"product,omitempty"`
Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"`
}
type AvailableQtyDTO struct {
AvailableQty float64 `json:"available_qty"`
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
}
type ProjectFlockKandangListDTO struct {
ProjectFlockKandangBaseDTO
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
Kandang *KandangDTO `json:"kandang,omitempty"`
Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"`
AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
type ProjectFlockKandangDetailDTO struct {
ProjectFlockKandangListDTO
}
// === Mapper Functions (ordered) ===
func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO {
return ProjectFlockKandangBaseDTO{
Id: e.Id,
}
}
func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO {
if pf == nil {
return nil
}
return &ProjectFlockDTO{
Id: pf.Id,
Period: pf.Period,
Area: pf.Area,
Category: pf.Category,
Fcr: pf.Fcr,
Location: pf.Location,
CreatedUser: pf.CreatedUser,
CreatedAt: pf.CreatedAt,
UpdatedAt: pf.UpdatedAt,
}
}
func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO {
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
if e.ProjectFlock.Id != 0 {
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
projectFlockSummary = &mapped
}
return ProjectFlockKandangListDTO{
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangDTO(e.Kandang),
Chickins: toChickinDTOs(e.Chickins),
AvailableQtys: toAvailableQtyDTOsFromRaw(availableQtysRaw),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTO(e),
}
}
func toKandangDTO(kandang entity.Kandang) *KandangDTO {
if kandang.Id == 0 {
return nil
}
return &KandangDTO{
Id: kandang.Id,
Name: kandang.Name,
Status: kandang.Status,
}
}
func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO {
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
return &mapped
}
return nil
}
func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKandangListDTO {
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
if e.ProjectFlock.Id != 0 {
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
projectFlockSummary = &mapped
}
return ProjectFlockKandangListDTO{
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangDTO(e.Kandang),
Chickins: toChickinDTOs(e.Chickins),
AvailableQtys: toAvailableQtyDTOs(e.Chickins),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTO(e),
}
}
func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFlockKandangListDTO {
result := make([]ProjectFlockKandangListDTO, len(e))
for i, r := range e {
result[i] = ToProjectFlockKandangListDTO(r)
}
return result
}
func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDetailDTO {
return ProjectFlockKandangDetailDTO{
ProjectFlockKandangListDTO: ToProjectFlockKandangListDTO(e),
}
}
// === Helper Functions (ordered) ===
func toProductWarehouseDTO(pwData map[string]interface{}) *ProductWarehouseDTO {
if pwData == nil {
return nil
}
dto := &ProductWarehouseDTO{}
if id, ok := pwData["id"].(float64); ok {
dto.Id = uint(id)
} else if id, ok := pwData["id"].(uint); ok {
dto.Id = id
}
if pData, ok := pwData["product"].(map[string]interface{}); ok {
dto.Product = toProductDTO(pData)
}
if wData, ok := pwData["warehouse"].(map[string]interface{}); ok {
dto.Warehouse = toWarehouseDTO(wData)
}
return dto
}
func toProductDTO(pData map[string]interface{}) *productDTO.ProductBaseDTO {
if pData == nil {
return nil
}
product := &productDTO.ProductBaseDTO{}
if id, ok := pData["id"].(float64); ok {
product.Id = uint(id)
} else if id, ok := pData["id"].(uint); ok {
product.Id = id
}
if name, ok := pData["name"].(string); ok {
product.Name = name
}
return product
}
func toWarehouseDTO(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO {
if wData == nil {
return nil
}
warehouse := &warehouseDTO.WarehouseBaseDTO{}
if id, ok := wData["id"].(float64); ok {
warehouse.Id = uint(id)
} else if id, ok := wData["id"].(uint); ok {
warehouse.Id = id
}
if name, ok := wData["name"].(string); ok {
warehouse.Name = name
}
if wType, ok := wData["type"].(string); ok {
warehouse.Type = wType
}
return warehouse
}
func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO {
if pf.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(pf.CreatedUser)
return &mapped
} else if pf.CreatedBy != 0 {
return &userDTO.UserBaseDTO{
Id: pf.CreatedBy,
IdUser: int64(pf.CreatedBy),
}
}
return nil
}
func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO {
if len(chickins) == 0 {
return nil
}
result := make([]chickinDTO.ChickinBaseDTO, len(chickins))
for i, ch := range chickins {
result[i] = chickinDTO.ToChickinBaseDTO(ch)
}
return result
}
func toAvailableQtyDTOs(chickins []entity.ProjectChickin) []AvailableQtyDTO {
if len(chickins) == 0 {
return nil
}
availableQtyMap := make(map[uint]AvailableQtyDTO)
for _, ch := range chickins {
if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 {
continue
}
if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists {
continue
}
pwDTO := &ProductWarehouseDTO{
Id: ch.ProductWarehouse.Id,
}
if ch.ProductWarehouse.Product.Id != 0 {
pwDTO.Product = &productDTO.ProductBaseDTO{
Id: ch.ProductWarehouse.Product.Id,
Name: ch.ProductWarehouse.Product.Name,
}
}
if ch.ProductWarehouse.Warehouse.Id != 0 {
pwDTO.Warehouse = &warehouseDTO.WarehouseBaseDTO{
Id: ch.ProductWarehouse.Warehouse.Id,
Name: ch.ProductWarehouse.Warehouse.Name,
Type: ch.ProductWarehouse.Warehouse.Type,
}
}
availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{
ProductWarehouse: pwDTO,
}
}
if len(availableQtyMap) == 0 {
return nil
}
result := make([]AvailableQtyDTO, 0, len(availableQtyMap))
for _, v := range availableQtyMap {
result = append(result, v)
}
return result
}
func toAvailableQtyDTOsFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO {
if len(availableQtysRaw) == 0 {
return nil
}
result := make([]AvailableQtyDTO, len(availableQtysRaw))
for i, v := range availableQtysRaw {
pwData, ok := v["product_warehouse"].(map[string]interface{})
if !ok {
continue
}
pwDTO := toProductWarehouseDTO(pwData)
availableQty := 0.0
if qty, ok := v["available_qty"].(float64); ok {
availableQty = qty
}
result[i] = AvailableQtyDTO{
AvailableQty: availableQty,
ProductWarehouse: pwDTO,
}
}
return result
}
@@ -0,0 +1,43 @@
package project_flock_kandangs
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProjectFlockKandangModule struct{}
func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlockKandang.NewProjectFlockPopulationRepository(db)
userRepo := rUser.NewUserRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
// register workflow steps for project flock kandang approvals
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
}
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
}
@@ -0,0 +1,26 @@
package project_flock_kandangs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/controllers"
projectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlockKandang.ProjectFlockKandangService) {
ctrl := controller.NewProjectFlockKandangController(s)
route := v1.Group("/project-flock-kandangs")
// 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.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,195 @@
package service
import (
"errors"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/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 ProjectFlockKandangService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error)
}
type projectFlockKandangService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
PopulationRepo repository.ProjectFlockPopulationRepository
}
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
return &projectFlockKandangService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
PopulationRepo: populationRepo,
}
}
func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
projectFlockKandangs, err := s.Repository.GetAll(c.Context())
if err != nil {
s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err)
return nil, 0, err
}
total := int64(len(projectFlockKandangs))
return projectFlockKandangs, total, nil
}
func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) {
projectFlockKandang, err := s.Repository.GetByID(c.Context(), id)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
if err != nil {
s.Log.Errorf("Failed get projectFlockKandang by id: %+v", err)
return nil, nil, err
}
if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil {
latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
if err != nil {
s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
}
if latest != nil {
projectFlockKandang.LatestApproval = latest
}
}
availableQtys, err := s.getAvailableQuantities(c, projectFlockKandang)
if err != nil {
s.Log.Errorf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandang.Kandang.Id, err)
availableQtys = nil
}
return projectFlockKandang, availableQtys, nil
}
func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) ([]map[string]interface{}, error) {
if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil {
return nil, nil
}
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.Kandang.Id)
if err != nil || warehouse == nil {
return nil, nil
}
var productCategoryCode string
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
productCategoryCode = "DOC"
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
productCategoryCode = "PULLET"
} else {
return nil, nil
}
products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id)
if err != nil || len(products) == 0 {
return nil, nil
}
var result []map[string]interface{}
for _, pw := range products {
availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
if err != nil {
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
}
// Only include product warehouse if available_qty > 0
if availableQty <= 0 {
continue
}
productData := map[string]interface{}{
"id": pw.Product.Id,
"name": pw.Product.Name,
}
warehouseData := map[string]interface{}{
"id": pw.Warehouse.Id,
"name": pw.Warehouse.Name,
"type": pw.Warehouse.Type,
}
productWarehouseData := map[string]interface{}{
"id": pw.Id,
"product": productData,
"warehouse": warehouseData,
}
result = append(result, map[string]interface{}{
"available_qty": availableQty,
"product_warehouse": productWarehouseData,
})
}
return result, nil
}
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
availableQty := productWarehouse.Quantity
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
var totalPendingQty float64
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
availableQty = productWarehouse.Quantity - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
var totalPopulation float64
var totalPendingQty float64
populations, err := s.PopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(c.Context(), projectFlockKandang.Id, productWarehouse.Id)
if err == nil {
for _, pop := range populations {
totalPopulation += pop.TotalQty
}
}
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
}
return availableQty, nil
}
@@ -0,0 +1,17 @@
package validation
type Create struct {
ProjectFlockId uint `json:"project_flock_id" validate:"required"`
KandangId uint `json:"kandang_id" validate:"required"`
}
type Update struct {
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -10,6 +10,7 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -22,6 +23,13 @@ type ProjectFlockBaseDTO struct {
FlockName string `json:"flock_name"`
}
type KandangWithProjectFlockIdDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
}
type ProjectFlockListDTO struct {
ProjectFlockBaseDTO
// Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
@@ -93,15 +101,15 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
return ProjectFlockListDTO{
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
// Flock: flockSummary,
Area: areaSummary,
Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Approval: latestApproval,
Area: areaSummary,
Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Approval: latestApproval,
}
}
@@ -9,8 +9,19 @@ import (
)
type ProjectFlockPopulationRepository interface {
repository.BaseRepository[entity.ProjectFlockPopulation]
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error)
// domain-specific
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error)
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
// subset of base repository methods used by services
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
// transaction helpers
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
DB() *gorm.DB
}
type projectFlockPopulationRepositoryImpl struct {
@@ -23,13 +34,60 @@ func NewProjectFlockPopulationRepository(db *gorm.DB) ProjectFlockPopulationRepo
}
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) {
var record entity.ProjectFlockPopulation
func (r *projectFlockPopulationRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockPopulationRepository {
return &projectFlockPopulationRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockPopulation](tx),
}
}
func (r *projectFlockPopulationRepositoryImpl) DB() *gorm.DB {
return r.BaseRepositoryImpl.DB()
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) {
var records []entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
First(&record).Error
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Preload("ProjectChickin").
Find(&records).Error
if err != nil {
return nil, err
}
return &record, nil
return records, nil
}
func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) {
var count int64
err := r.DB().WithContext(ctx).
Where("project_chickin_id = ?", projectChickinID).
Model(&entity.ProjectFlockPopulation{}).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) {
var records []entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx).
Where("project_chickin_id = ? AND product_warehouse_id = ?", projectChickinID, productWarehouseID).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) {
var records []entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx).
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ? AND project_flock_populations.product_warehouse_id = ?", projectFlockKandangID, productWarehouseID).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
@@ -4,6 +4,7 @@ import (
"context"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
@@ -19,6 +20,7 @@ type ProjectFlockKandangRepository interface {
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
IdExists(ctx context.Context, id uint) (bool, error)
DB() *gorm.DB
}
@@ -59,6 +61,9 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse").
Order("project_flock_id ASC, created_at ASC").
Find(&records).Error; err != nil {
return nil, err
@@ -73,6 +78,9 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand
func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB {
return r.db
}
func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id)
}
func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
record := new(entity.ProjectFlockKandang)
@@ -85,6 +93,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse").
First(record, id).Error; err != nil {
return nil, err
}
@@ -103,6 +114,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse").
First(record).Error; err != nil {
return nil, err
}
@@ -80,6 +80,18 @@ func NewProjectflockService(
}
}
func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Flock").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs").
Preload("KandangHistory").
Preload("KandangHistory.Kandang")
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
@@ -265,7 +265,7 @@ func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandang
if err != nil {
return 0, err
}
return int64(math.Round(population.InitialQuantity)), nil
return int64(math.Round(population.TotalQty)), nil
}
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
+5 -1
View File
@@ -10,6 +10,8 @@ import (
chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins"
projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks"
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings"
projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs"
// MODULE IMPORTS
)
@@ -20,8 +22,10 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
projectflocks.ProjectflockModule{},
recordings.RecordingModule{},
chickins.ChickinModule{},
transferLayings.TransferLayingModule{},
projectFlockKandangs.ProjectFlockKandangModule{},
// MODULE REGISTRY
}
}
for _, m := range allModules {
m.RegisterRoutes(group, db, validate)
@@ -0,0 +1,182 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransferLayingController struct {
TransferLayingService service.TransferLayingService
}
func NewTransferLayingController(transferLayingService service.TransferLayingService) *TransferLayingController {
return &TransferLayingController{
TransferLayingService: transferLayingService,
}
}
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)),
TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)),
TransferDateFrom: c.Query("transfer_date_from", ""),
TransferDateTo: c.Query("transfer_date_to", ""),
ApprovalStatus: c.Query("approval_status", ""),
TransferNumber: c.Query("transfer_number", ""),
Sort: c.Query("sort", "created_at"),
Order: c.Query("order", "desc"),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.TransferLayingService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transferLayings successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransferLayingListDTOs(result),
})
}
func (u *TransferLayingController) 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, approval, err := u.TransferLayingService.GetOneWithApproval(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transferLaying successfully",
Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, approval),
})
}
func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.TransferLayingService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.TransferLayingService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
func (u *TransferLayingController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.TransferLayingService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete transferLaying successfully",
})
}
func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
results, err := u.TransferLayingService.Approval(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit transfer laying approval successfully"
)
if len(results) == 1 {
data = dto.ToTransferLayingListDTO(results[0])
} else {
message = "Submit transfer laying approvals successfully"
data = dto.ToTransferLayingListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
})
}
@@ -0,0 +1,255 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type TransferLayingBaseDTO struct {
Id uint `json:"id"`
TransferNumber string `json:"transfer_number"`
TransferDate time.Time `json:"transfer_date"`
Notes string `json:"notes"`
}
type ProjectFlockSummaryDTO struct {
Id uint `json:"id"`
Period int `json:"period"`
Category string `json:"category"`
}
type ProductSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type WarehouseSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
type ProductWarehouseSummaryDTO struct {
Product *ProductSummaryDTO `json:"product,omitempty"`
Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"`
}
type ProjectFlockKandangSummaryDTO struct {
Id uint `json:"id"`
KandangId uint `json:"kandang_id"`
}
type LayingTransferSourceDTO struct {
SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"`
Qty float64 `json:"qty"`
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
Note string `json:"note,omitempty"`
}
type LayingTransferTargetDTO struct {
TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"`
Qty float64 `json:"qty"`
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
Note string `json:"note,omitempty"`
}
type TransferLayingListDTO struct {
TransferLayingBaseDTO
FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"`
ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"`
PendingUsageQty *float64 `json:"pending_usage_qty"`
UsageQty *float64 `json:"usage_qty"`
CreatedBy uint `json:"created_by"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
type TransferLayingDetailDTO struct {
TransferLayingListDTO
Sources []LayingTransferSourceDTO `json:"sources,omitempty"`
Targets []LayingTransferTargetDTO `json:"targets,omitempty"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
// === Mapper Functions ===
func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO {
if pf == nil || pf.Id == 0 {
return nil
}
return &ProjectFlockSummaryDTO{
Id: pf.Id,
Period: pf.Period,
Category: pf.Category,
}
}
func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO {
if pfk == nil || pfk.Id == 0 {
return nil
}
return &ProjectFlockKandangSummaryDTO{
Id: pfk.Id,
KandangId: pfk.KandangId,
}
}
func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO {
if product == nil || product.Id == 0 {
return nil
}
return &ProductSummaryDTO{
Id: product.Id,
Name: product.Name,
}
}
func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO {
if warehouse == nil || warehouse.Id == 0 {
return nil
}
return &WarehouseSummaryDTO{
Id: warehouse.Id,
Name: warehouse.Name,
Type: warehouse.Type,
}
}
func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO {
if pw == nil || pw.Id == 0 {
return nil
}
return &ProductWarehouseSummaryDTO{
Product: ToProductSummaryDTO(&pw.Product),
Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse),
}
}
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
return LayingTransferSourceDTO{
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
Qty: source.Qty,
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
Note: source.Note,
}
}
func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingTransferSourceDTO {
if len(sources) == 0 {
return []LayingTransferSourceDTO{}
}
result := make([]LayingTransferSourceDTO, len(sources))
for i, s := range sources {
result[i] = ToLayingTransferSourceDTO(s)
}
return result
}
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
return LayingTransferTargetDTO{
TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang),
Qty: target.Qty,
ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse),
Note: target.Note,
}
}
func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingTransferTargetDTO {
if len(targets) == 0 {
return []LayingTransferTargetDTO{}
}
result := make([]LayingTransferTargetDTO, len(targets))
for i, t := range targets {
result[i] = ToLayingTransferTargetDTO(t)
}
return result
}
func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO {
return TransferLayingBaseDTO{
Id: e.Id,
TransferNumber: e.TransferNumber,
TransferDate: e.TransferDate,
Notes: e.Notes,
}
}
func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
return TransferLayingListDTO{
TransferLayingBaseDTO: ToTransferLayingBaseDTO(e),
FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock),
ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock),
PendingUsageQty: e.PendingUsageQty,
UsageQty: e.UsageQty,
CreatedBy: e.CreatedBy,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
}
}
func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
var latestApproval *approvalDTO.ApprovalBaseDTO
// Use LatestApproval from entity if available
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = &mapped
} else if len(approvals) > 0 {
// Fallback to approvals slice
latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1])
latestApproval = &latest
}
return TransferLayingDetailDTO{
TransferLayingListDTO: ToTransferLayingListDTO(e),
Sources: ToLayingTransferSourceDTOs(e.Sources),
Targets: ToLayingTransferTargetDTOs(e.Targets),
Approval: latestApproval,
}
}
func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO {
var mappedApproval *approvalDTO.ApprovalBaseDTO
// Prefer LatestApproval from entity
if e.LatestApproval != nil && e.LatestApproval.Id != 0 {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
mappedApproval = &mapped
} else if approval != nil && approval.Id != 0 {
// Fallback to passed approval parameter
mapped := approvalDTO.ToApprovalDTO(*approval)
mappedApproval = &mapped
}
return TransferLayingDetailDTO{
TransferLayingListDTO: ToTransferLayingListDTO(e),
Sources: ToLayingTransferSourceDTOs(e.Sources),
Targets: ToLayingTransferTargetDTOs(e.Targets),
Approval: mappedApproval,
}
}
func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingListDTO {
result := make([]TransferLayingListDTO, len(items))
for i, item := range items {
result[i] = ToTransferLayingListDTO(item)
}
return result
}
@@ -0,0 +1,53 @@
package transfer_layings
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type TransferLayingModule struct{}
func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rInventory.NewProductWarehouseRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register transfer to laying approval workflow: %v", err))
}
transferLayingService := sTransferLaying.NewTransferLayingService(
transferLayingRepo,
projectFlockRepo,
projectFlockKandangRepo,
projectFlockPopulationRepo,
productWarehouseRepo,
warehouseRepo,
approvalService,
validate,
)
userService := sUser.NewUserService(userRepo, validate)
TransferLayingRoutes(router, userService, transferLayingService)
}
@@ -0,0 +1,42 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type TransferLayingRepository interface {
repository.BaseRepository[entity.LayingTransfer]
GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error)
IdExists(ctx context.Context, id uint) (bool, error)
}
type TransferLayingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.LayingTransfer]
db *gorm.DB
}
func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository {
return &TransferLayingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransfer](db),
db: db,
}
}
func (r *TransferLayingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.LayingTransfer](ctx, r.db, id)
}
func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) {
var transfer entity.LayingTransfer
err := r.db.WithContext(ctx).
Where("transfer_number = ?", transferNumber).
Where("deleted_at IS NULL").
First(&transfer).Error
if err != nil {
return nil, err
}
return &transfer, nil
}
@@ -0,0 +1,38 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type LayingTransferSourceRepository interface {
repository.BaseRepository[entity.LayingTransferSource]
GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error)
}
type LayingTransferSourceRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.LayingTransferSource]
db *gorm.DB
}
func NewLayingTransferSourceRepository(db *gorm.DB) LayingTransferSourceRepository {
return &LayingTransferSourceRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferSource](db),
db: db,
}
}
func (r *LayingTransferSourceRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error) {
var sources []entity.LayingTransferSource
err := r.db.WithContext(ctx).
Where("laying_transfer_id = ?", layingTransferId).
Order("created_at DESC").
Find(&sources).Error
if err != nil {
return nil, err
}
return sources, nil
}
@@ -0,0 +1,38 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type LayingTransferTargetRepository interface {
repository.BaseRepository[entity.LayingTransferTarget]
GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error)
}
type LayingTransferTargetRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.LayingTransferTarget]
db *gorm.DB
}
func NewLayingTransferTargetRepository(db *gorm.DB) LayingTransferTargetRepository {
return &LayingTransferTargetRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferTarget](db),
db: db,
}
}
func (r *LayingTransferTargetRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error) {
var targets []entity.LayingTransferTarget
err := r.db.WithContext(ctx).
Where("laying_transfer_id = ?", layingTransferId).
Order("created_at DESC").
Find(&targets).Error
if err != nil {
return nil, err
}
return targets, nil
}
@@ -0,0 +1,30 @@
package transfer_layings
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/controllers"
transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.TransferLayingService) {
ctrl := controller.NewTransferLayingController(s)
route := v1.Group("/transfer_layings")
// 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.Post("/approval", m.Auth(u), ctrl.Approval)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval)
}
@@ -0,0 +1,727 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
"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 TransferLayingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error)
GetOneWithApproval(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
}
type transferLayingService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.TransferLayingRepository
ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository
ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository
ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository
ProductWarehouseRepo rInventory.ProductWarehouseRepository
WarehouseRepo rWarehouse.WarehouseRepository
ApprovalService commonSvc.ApprovalService
}
func NewTransferLayingService(
repo repository.TransferLayingRepository,
projectFlockRepo ProjectFlockRepository.ProjectflockRepository,
projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository,
projectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository,
productWarehouseRepo rInventory.ProductWarehouseRepository,
warehouseRepo rWarehouse.WarehouseRepository,
approvalService commonSvc.ApprovalService,
validate *validator.Validate,
) TransferLayingService {
return &transferLayingService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ProductWarehouseRepo: productWarehouseRepo,
WarehouseRepo: warehouseRepo,
ApprovalService: approvalService,
}
}
func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Sources").
Preload("Sources.SourceProjectFlockKandang").
Preload("Sources.ProductWarehouse").
Preload("Sources.ProductWarehouse.Product").
Preload("Sources.ProductWarehouse.Warehouse").
Preload("Targets").
Preload("Targets.TargetProjectFlockKandang").
Preload("Targets.ProductWarehouse").
Preload("Targets.ProductWarehouse.Product").
Preload("Targets.ProductWarehouse.Warehouse")
}
func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.SourceProjectFlockId != 0 {
db = db.Where("from_project_flock_id = ?", params.SourceProjectFlockId)
}
if params.TargetProjectFlockId != 0 {
db = db.Where("to_project_flock_id = ?", params.TargetProjectFlockId)
}
if params.TransferDateFrom != "" {
db = db.Where("transfer_date >= ?", params.TransferDateFrom)
}
if params.TransferDateTo != "" {
db = db.Where("transfer_date <= ?", params.TransferDateTo)
}
if params.TransferNumber != "" {
db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%")
}
sortField := "created_at"
if params.Sort != "" {
sortField = params.Sort
}
sortOrder := "DESC"
if params.Order == "asc" {
sortOrder = "ASC"
}
db = db.Order(fmt.Sprintf("%s %s", sortField, sortOrder))
return db
})
if err != nil {
s.Log.Errorf("Failed to get transferLayings: %+v", err)
return nil, 0, err
}
if params.ApprovalStatus != "" {
var filtered []entity.LayingTransfer
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
for _, transfer := range transferLayings {
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil)
if err == nil && latestApproval != nil && latestApproval.Action != nil {
if string(*latestApproval.Action) == params.ApprovalStatus {
filtered = append(filtered, transfer)
}
}
}
transferLayings = filtered
}
return transferLayings, total, nil
}
func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) {
transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
if err != nil {
s.Log.Errorf("Failed get transferLaying by id: %+v", err)
return nil, err
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil)
if err == nil && latestApproval != nil {
transferLaying.LatestApproval = latestApproval
}
return transferLaying, nil
}
func (s transferLayingService) GetOneWithApproval(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) {
transferLaying, err := s.GetOne(c, id)
if err != nil {
return nil, nil, err
}
return transferLaying, transferLaying.LatestApproval, nil
}
func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate source project flock")
}
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Target Project Flock not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate target project flock")
}
for _, detail := range req.SourceKandangs {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil {
return nil, err
}
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get source project flock kandang")
}
if pfk.ProjectFlockId != req.SourceProjectFlockId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d does not belong to source project flock %d", detail.ProjectFlockKandangId, req.SourceProjectFlockId))
}
}
for _, detail := range req.TargetKandangs {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil {
return nil, err
}
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
}
if pfk.ProjectFlockId != req.TargetProjectFlockId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Target kandang %d does not belong to target project flock %d", detail.ProjectFlockKandangId, req.TargetProjectFlockId))
}
}
transferDate, err := utils.ParseDateString(req.TransferDate)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format")
}
var totalSourceQty, totalTargetQty float64
sourceWarehouseMap := make(map[uint]uint)
for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Source kandang quantity must be greater than 0")
}
totalSourceQty += sourceDetail.Quantity
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
if err != nil {
return nil, err
}
var totalPopulation float64
var productWarehouseId uint
for _, pop := range populations {
totalPopulation += pop.TotalQty
if productWarehouseId == 0 {
productWarehouseId = pop.ProductWarehouseId
}
}
if totalPopulation == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available for transfer", sourceDetail.ProjectFlockKandangId))
}
if totalPopulation < sourceDetail.Quantity {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity))
}
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
}
for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Target kandang quantity must be greater than 0")
}
totalTargetQty += targetDetail.Quantity
}
if totalSourceQty != totalTargetQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total source quantity (%f) must equal total target quantity (%f)", totalSourceQty, totalTargetQty))
}
transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano())
createBody := &entity.LayingTransfer{
TransferNumber: transferNumber,
Notes: req.Reason,
FromProjectFlockId: req.SourceProjectFlockId,
ToProjectFlockId: req.TargetProjectFlockId,
TransferDate: transferDate,
PendingUsageQty: &totalSourceQty,
CreatedBy: 1, //todo : harus diambil dari auth
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record")
}
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
for _, sourceDetail := range req.SourceKandangs {
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
source := entity.LayingTransferSource{
LayingTransferId: createBody.Id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
Qty: sourceDetail.Quantity,
ProductWarehouseId: &productWarehouseId,
}
if err := dbTransaction.Create(&source).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source")
}
if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reduce project flock population")
}
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("quantity - ?", sourceDetail.Quantity)}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity")
}
}
for _, targetDetail := range req.TargetKandangs {
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
}
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
}
target := entity.LayingTransferTarget{
LayingTransferId: createBody.Id,
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
Qty: targetDetail.Quantity,
ProductWarehouseId: &targetWarehouse.Id,
}
if err := dbTransaction.Create(&target).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
}
}
if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval")
}
return nil
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying")
}
return s.GetOne(c, createBody.Id)
}
func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
_, err := s.Repository.GetByID(c.Context(), id, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
}
if latestApproval != nil && latestApproval.Action != nil {
action := string(*latestApproval.Action)
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot update transfer laying with status %s", action))
}
}
updateBody := make(map[string]any)
if req.TransferDate != nil {
updateBody["transfer_date"] = *req.TransferDate
}
if req.Reason != nil {
updateBody["notes"] = *req.Reason
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
s.Log.Errorf("Failed to update transferLaying: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying")
}
return s.GetOne(c, id)
}
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
_, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return db.Preload("Sources.ProductWarehouse").Preload("Targets")
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
}
if latestApproval != nil && latestApproval.Action != nil {
action := string(*latestApproval.Action)
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action))
}
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
}
for _, source := range sources {
if source.ProductWarehouseId != nil && source.Qty > 0 {
if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{
"quantity": gorm.Expr("quantity + ?", source.Qty),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity")
}
}
}
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
for _, source := range sources {
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration")
}
remainingToRestore := source.Qty
for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- {
pop := populations[i]
restoreAmount := remainingToRestore
if remainingToRestore < pop.TotalQty {
restoreAmount = remainingToRestore
}
newQty := pop.TotalQty + restoreAmount
if err := projectFlockPopulationRepoTx.PatchOne(c.Context(), pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore population quantity")
}
remainingToRestore -= restoreAmount
}
}
if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return fiberErr
}
s.Log.Errorf("Failed to delete transferLaying: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
}
return nil
}
func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID := uint(1) // TODO: change from auth context
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected):
action = entity.ApprovalActionRejected
case string(entity.ApprovalActionApproved):
action = entity.ApprovalActionApproved
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
if len(approvableIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
step := utils.TransferToLayingStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.TransferToLayingStepDisetujui
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs {
transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID))
}
return err
}
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowTransferToLaying,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
if action == entity.ApprovalActionApproved && transfer.PendingUsageQty != nil && *transfer.PendingUsageQty > 0 {
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
}
targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer targets")
}
if len(sources) > 0 && len(targets) > 0 {
firstSource := sources[0]
if firstSource.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
}
sourceWarehouse, err := productWarehouseRepoTx.GetByID(c.Context(), *firstSource.ProductWarehouseId, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse")
}
for _, target := range targets {
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), target.TargetProjectFlockKandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
}
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
}
if _, err := s.getOrCreateProductWarehouse(
c.Context(),
dbTransaction,
sourceWarehouse.ProductId,
targetWarehouse.Id,
target.Qty,
actorID,
); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse")
}
}
}
usageQty := *transfer.PendingUsageQty
updateData := map[string]any{
"usage_qty": usageQty,
"pending_usage_qty": nil,
}
if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), approvableID, updateData, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status")
}
}
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
updated := make([]entity.LayingTransfer, 0, len(approvableIDs))
for _, approvableID := range approvableIDs {
transfer, err := s.GetOne(c, approvableID)
if err != nil {
return nil, err
}
updated = append(updated, *transfer)
}
return updated, nil
}
func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayingID uint, actorID uint) error {
if transferLayingID == 0 || actorID == 0 {
return nil
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
action := entity.ApprovalActionCreated
_, err := approvalSvc.CreateApproval(
ctx,
utils.ApprovalWorkflowTransferToLaying,
transferLayingID,
utils.TransferToLayingStepPengajuan,
&action,
actorID,
nil,
)
return err
}
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint) (*entity.ProductWarehouse, error) {
productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx)
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil && existing != nil {
if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"quantity": gorm.Expr("quantity + ?", quantity)}, nil); err != nil {
return nil, err
}
return existing, nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
newWarehouse := &entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouseID,
Quantity: quantity,
CreatedBy: actorID,
}
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
return nil, err
}
return newWarehouse, nil
}
func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToReduce float64) error {
populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err != nil {
return err
}
if len(populations) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "No populations found for reduction")
}
remainingToReduce := quantityToReduce
for i := len(populations) - 1; i >= 0; i-- {
if remainingToReduce <= 0 {
break
}
pop := populations[i]
reductionAmount := remainingToReduce
if pop.TotalQty < remainingToReduce {
reductionAmount = pop.TotalQty
}
newQty := pop.TotalQty - reductionAmount
if err := populationRepo.PatchOne(ctx, pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return err
}
remainingToReduce -= reductionAmount
}
if remainingToReduce > 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient population to reduce. Still need to reduce: %.0f", remainingToReduce))
}
return nil
}
@@ -0,0 +1,44 @@
package validation
type SourceKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
}
type TargetKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
}
type Create struct {
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"`
TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"`
SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,dive,required"`
TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"`
Reason string `json:"reason" validate:"omitempty,max=1000"`
}
type Update struct {
TransferDate *string `json:"transfer_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Reason *string `json:"reason,omitempty" validate:"omitempty,max=1000"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
SourceProjectFlockId uint `query:"source_project_flock_id" validate:"omitempty"`
TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"`
TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"`
TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"`
ApprovalStatus string `query:"approval_status" validate:"omitempty,oneof=PENDING APPROVED REJECTED"` // Filter by latest approval status
TransferNumber string `query:"transfer_number" validate:"omitempty"` // Search by transfer number
Sort string `query:"sort" validate:"omitempty,oneof=created_at transfer_date"` // Sort by field
Order string `query:"order" validate:"omitempty,oneof=asc desc"` // Sort order
}
type Approve struct {
Action string `json:"action" validate:"required_strict"`
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
+52
View File
@@ -18,6 +18,8 @@ const (
FlagIsActive FlagType = "IS_ACTIVE"
FlagDOC FlagType = "DOC"
FlagPullet FlagType = "PULLET"
FlagLayer FlagType = "LAYER"
FlagPakan FlagType = "PAKAN"
FlagPreStarter FlagType = "PRE-STARTER"
FlagStarter FlagType = "STARTER"
@@ -37,6 +39,8 @@ const (
var flagGroupOptions = map[FlagGroup][]FlagType{
FlagGroupProduct: {
FlagDOC,
FlagPullet,
FlagLayer,
FlagPakan,
FlagPreStarter,
FlagStarter,
@@ -79,6 +83,24 @@ const (
WarehouseTypeKandang WarehouseType = "KANDANG"
)
// -------------------------------------------------------------------
// Stock log
// -------------------------------------------------------------------
type StockLogTransactionType string
const (
StockLogTransactionTypeIncrease StockLogTransactionType = "INCREASE"
StockLogTransactionTypeDecrease StockLogTransactionType = "DECREASE"
)
type StockLogType string
const (
StockLogTypeAdjustment StockLogType = "ADJUSTMENT"
StockLogTypeTransfer StockLogType = "TRANSFER"
)
// -------------------------------------------------------------------
// WarehouseType
// -------------------------------------------------------------------
@@ -140,6 +162,36 @@ var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{
ProjectFlockStepAktif: "Aktif",
}
// -------------------------------------------------------------------
// Project Flock Kandang Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowProjectFlockKandang approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCK_KANDANGS")
ProjectFlockKandangStepPengajuan approvalutils.ApprovalStep = 1
ProjectFlockKandangStepDisetujui approvalutils.ApprovalStep = 2
)
var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{
ProjectFlockKandangStepPengajuan: "Pengajuan",
ProjectFlockKandangStepDisetujui: "Disetujui",
}
// -------------------------------------------------------------------
// Transfer To laying Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowTransferToLaying approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("TRANSFER_TO_LAYINGS")
TransferToLayingStepPengajuan approvalutils.ApprovalStep = 1
TransferToLayingStepDisetujui approvalutils.ApprovalStep = 2
)
var TransferToLayingApprovalSteps = map[approvalutils.ApprovalStep]string{
TransferToLayingStepPengajuan: "Pengajuan",
TransferToLayingStepDisetujui: "Disetujui",
}
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// Recording Approval
// -------------------------------------------------------------------