feat/BE/US-76/US-78/US-79/TASK-112,120,133,121-Recording growing/TASK-187,189,202,190-Recording Laying/TASK-191,192,194,197,203-Grading Telur

This commit is contained in:
ragilap
2025-10-31 16:03:05 +07:00
parent 614da067f7
commit f869943573
38 changed files with 2808 additions and 1259 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -0,0 +1,98 @@
BEGIN;
DROP INDEX IF EXISTS project_flocks_base_period_unique;
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS flock_id BIGINT;
WITH normalized AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name,
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
FROM project_flocks pf
),
seed_flocks AS (
SELECT DISTINCT
n.normalized_name,
MIN(n.created_by) AS created_by
FROM normalized n
GROUP BY n.normalized_name
)
INSERT INTO flocks (name, created_by, created_at, updated_at)
SELECT sf.normalized_name, sf.created_by, NOW(), NOW()
FROM seed_flocks sf
ON CONFLICT DO NOTHING;
WITH normalized AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name
FROM project_flocks pf
),
resolved AS (
SELECT
n.id,
f.id AS flock_id
FROM normalized n
JOIN flocks f ON LOWER(f.name) = LOWER(n.normalized_name)
)
UPDATE project_flocks pf
SET flock_id = resolved.flock_id
FROM resolved
WHERE pf.id = resolved.id;
WITH missing AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name,
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
FROM project_flocks pf
WHERE pf.flock_id IS NULL
),
seed_missing AS (
SELECT DISTINCT normalized_name, created_by FROM missing
)
INSERT INTO flocks (name, created_by, created_at, updated_at)
SELECT sm.normalized_name, sm.created_by, NOW(), NOW()
FROM seed_missing sm
ON CONFLICT DO NOTHING;
WITH missing AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name
FROM project_flocks pf
WHERE pf.flock_id IS NULL
)
UPDATE project_flocks pf
SET flock_id = f.id
FROM missing m
JOIN flocks f ON LOWER(f.name) = LOWER(m.normalized_name)
WHERE pf.id = m.id;
ALTER TABLE project_flocks
ALTER COLUMN flock_id SET NOT NULL;
DROP INDEX IF EXISTS project_flocks_flock_name_unique;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS flock_name;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_period_unique
ON project_flocks (flock_id, period)
WHERE deleted_at IS NULL;
COMMIT;
@@ -0,0 +1,55 @@
BEGIN;
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS flock_name VARCHAR(255);
WITH generated_names AS (
SELECT
pf.id,
COALESCE(f.name, CONCAT('Project Flock ', pf.id)) AS base_name,
pf.period,
ROW_NUMBER() OVER (PARTITION BY COALESCE(f.name, CONCAT('Project Flock ', pf.id)) ORDER BY pf.id) AS rn
FROM project_flocks pf
LEFT JOIN flocks f ON f.id = pf.flock_id
)
UPDATE project_flocks pf
SET flock_name = CASE
WHEN gn.period IS NOT NULL THEN
CASE
WHEN gn.rn = 1 THEN CONCAT(gn.base_name, ' ', gn.period)
ELSE CONCAT(gn.base_name, ' ', gn.period, ' ', gn.rn)
END
ELSE
CASE
WHEN gn.rn = 1 THEN gn.base_name
ELSE CONCAT(gn.base_name, ' ', gn.rn)
END
END
FROM generated_names gn
WHERE pf.id = gn.id
AND (pf.flock_name IS NULL OR pf.flock_name = '');
UPDATE project_flocks
SET flock_name = CONCAT('Project Flock ', id)
WHERE flock_name IS NULL OR flock_name = '';
ALTER TABLE project_flocks
ALTER COLUMN flock_name SET NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_name_unique
ON project_flocks (flock_name)
WHERE deleted_at IS NULL;
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique
ON project_flocks (
LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))),
period
)
WHERE deleted_at IS NULL;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS flock_id;
COMMIT;
@@ -0,0 +1,143 @@
BEGIN;
-- Drop newly introduced egg tables
DROP TABLE IF EXISTS grading_eggs;
DROP TABLE IF EXISTS recording_eggs;
-- Revert recording_stocks structure
ALTER TABLE recording_stocks
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
ALTER TABLE recording_stocks
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS pending_qty;
ALTER TABLE recording_stocks
ADD COLUMN increase NUMERIC(10,3),
ADD COLUMN decrease NUMERIC(10,3),
ADD COLUMN usage_amount BIGINT,
ADD COLUMN notes VARCHAR;
ALTER TABLE recording_stocks
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
(increase IS NULL OR increase >= 0) AND
(decrease IS NULL OR decrease >= 0) AND
(usage_amount IS NULL OR usage_amount >= 0)
);
-- Revert recording_depletions structure
ALTER TABLE recording_depletions
DROP CONSTRAINT IF EXISTS chk_recording_depl_qty;
ALTER TABLE recording_depletions
ALTER COLUMN qty TYPE BIGINT USING COALESCE(qty, 0)::BIGINT;
ALTER TABLE recording_depletions
RENAME COLUMN qty TO total;
ALTER TABLE recording_depletions
ADD COLUMN notes VARCHAR;
ALTER TABLE recording_depletions
ADD CONSTRAINT chk_recording_depl_total CHECK (total >= 0);
-- Revert recording_bws structure
ALTER TABLE recording_bws
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
ALTER TABLE recording_bws
ALTER COLUMN qty TYPE INT USING COALESCE(qty, 0)::INT;
ALTER TABLE recording_bws
DROP COLUMN IF EXISTS total_weight;
ALTER TABLE recording_bws
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
ALTER TABLE recording_bws
RENAME COLUMN avg_weight TO weight;
ALTER TABLE recording_bws
ADD COLUMN notes VARCHAR;
UPDATE recording_bws
SET qty = GREATEST(qty, 1);
ALTER TABLE recording_bws
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (weight >= 0 AND qty >= 1);
-- Revert recordings header
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock_kandang,
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2;
ALTER TABLE recordings
ALTER COLUMN total_depletion_qty TYPE INT USING COALESCE(total_depletion_qty, 0)::INT,
ALTER COLUMN total_chick_qty TYPE BIGINT USING COALESCE(total_chick_qty, 0)::BIGINT;
ALTER TABLE recordings
RENAME COLUMN total_depletion_qty TO total_depletion;
ALTER TABLE recordings
RENAME COLUMN total_chick_qty TO total_chick;
ALTER TABLE recordings
ADD COLUMN record_date DATE,
ADD COLUMN status INT NOT NULL DEFAULT 0,
ADD COLUMN ontime INT NOT NULL DEFAULT 0,
ADD COLUMN daily_depletion_rate NUMERIC(7,3),
ADD COLUMN cum_depletion INT;
ALTER TABLE recordings
RENAME COLUMN project_flock_kandangs_id TO project_flock_id;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_status CHECK (status IN (0,1,2,3));
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_ontime CHECK (ontime IN (0,1));
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives CHECK (
(total_depletion IS NULL OR total_depletion >= 0) AND
(cum_depletion IS NULL OR cum_depletion >= 0) AND
(total_chick IS NULL OR total_chick >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(fcr_value IS NULL OR fcr_value > 0) AND
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
);
-- Ensure new columns carry derived data
UPDATE recordings
SET record_date = (record_datetime AT TIME ZONE 'Asia/Jakarta')::date
WHERE record_date IS NULL;
-- Restore helper trigger/function and indexes
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
BEGIN
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER recordings_set_record_date_trg
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
CREATE INDEX idx_recordings_flock_datetime
ON recordings (project_flock_id, record_datetime);
CREATE UNIQUE INDEX uq_recordings_flock_record_date
ON recordings (project_flock_id, record_date)
WHERE deleted_at IS NULL;
COMMIT;
@@ -0,0 +1,168 @@
BEGIN;
-- Drop trigger & helper function tied to record_date before removing the column
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
DROP FUNCTION IF EXISTS trg_set_record_date();
-- Drop indexes and constraints that reference legacy columns
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock,
DROP CONSTRAINT IF EXISTS chk_recordings_status,
DROP CONSTRAINT IF EXISTS chk_recordings_ontime,
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives;
-- Align recordings header with the new schema
ALTER TABLE recordings
RENAME COLUMN project_flock_id TO project_flock_kandangs_id;
ALTER TABLE recordings
DROP COLUMN IF EXISTS record_date,
DROP COLUMN IF EXISTS status,
DROP COLUMN IF EXISTS ontime,
DROP COLUMN IF EXISTS daily_depletion_rate,
DROP COLUMN IF EXISTS cum_depletion;
ALTER TABLE recordings
RENAME COLUMN total_depletion TO total_depletion_qty;
ALTER TABLE recordings
RENAME COLUMN total_chick TO total_chick_qty;
ALTER TABLE recordings
ALTER COLUMN total_depletion_qty TYPE NUMERIC(15,3) USING COALESCE(total_depletion_qty, 0)::NUMERIC(15,3),
ALTER COLUMN total_chick_qty TYPE NUMERIC(15,3) USING COALESCE(total_chick_qty, 0)::NUMERIC(15,3),
ALTER COLUMN cum_intake TYPE INT USING COALESCE(cum_intake, 0)::INT;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock_kandang
FOREIGN KEY (project_flock_kandangs_id) REFERENCES project_flock_kandangs(id);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0)
);
CREATE INDEX idx_recordings_flock_datetime
ON recordings (project_flock_kandangs_id, record_datetime);
-- recording_bws reshape
ALTER TABLE recording_bws
RENAME COLUMN weight TO avg_weight;
ALTER TABLE recording_bws
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
ALTER TABLE recording_bws
ADD COLUMN total_weight NUMERIC(10,3);
UPDATE recording_bws
SET total_weight = COALESCE(avg_weight, 0) * COALESCE(qty, 0);
ALTER TABLE recording_bws
ALTER COLUMN total_weight SET NOT NULL;
ALTER TABLE recording_bws
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
ALTER TABLE recording_bws
DROP COLUMN IF EXISTS notes;
ALTER TABLE recording_bws
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
ALTER TABLE recording_bws
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (
avg_weight >= 0 AND qty >= 0 AND total_weight >= 0
);
-- recording_depletions reshape
ALTER TABLE recording_depletions
RENAME COLUMN total TO qty;
ALTER TABLE recording_depletions
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
ALTER TABLE recording_depletions
DROP COLUMN IF EXISTS notes;
ALTER TABLE recording_depletions
DROP CONSTRAINT IF EXISTS chk_recording_depl_total;
ALTER TABLE recording_depletions
ADD CONSTRAINT chk_recording_depl_qty CHECK (qty >= 0);
-- recording_stocks reshape
ALTER TABLE recording_stocks
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
ALTER TABLE recording_stocks
DROP COLUMN IF EXISTS increase,
DROP COLUMN IF EXISTS decrease,
DROP COLUMN IF EXISTS usage_amount,
DROP COLUMN IF EXISTS notes;
ALTER TABLE recording_stocks
ADD COLUMN usage_qty NUMERIC(15,3),
ADD COLUMN pending_qty NUMERIC(15,3);
ALTER TABLE recording_stocks
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
(usage_qty IS NULL OR usage_qty >= 0) AND
(pending_qty IS NULL OR pending_qty >= 0)
);
-- recording_eggs table
CREATE TABLE recording_eggs (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
qty INT NOT NULL,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_recording_eggs_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT fk_recording_eggs_product_warehouse
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
CONSTRAINT fk_recording_eggs_created_by
FOREIGN KEY (created_by) REFERENCES users(id),
CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0)
);
CREATE INDEX idx_recording_eggs_recording
ON recording_eggs (recording_id);
CREATE INDEX idx_recording_eggs_product
ON recording_eggs (product_warehouse_id);
-- grading_eggs table
CREATE TABLE grading_eggs (
id BIGSERIAL PRIMARY KEY,
recording_egg_id BIGINT NOT NULL,
qty NUMERIC(15,3) NOT NULL,
grade VARCHAR,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_grading_eggs_recording_egg
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
CONSTRAINT fk_grading_eggs_created_by
FOREIGN KEY (created_by) REFERENCES users(id),
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
);
CREATE INDEX idx_grading_eggs_recording_egg
ON grading_eggs (recording_egg_id);
COMMIT;
+64 -274
View File
@@ -8,7 +8,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -41,21 +40,14 @@ func Run(db *gorm.DB) error {
return err return err
} }
flocks, err := seedFlocks(tx, adminID) if _, err := seedFlocks(tx, adminID); err != nil {
if err != nil {
return err return err
} }
fcrs, err := seedFcr(tx, adminID) if _, err := seedFcr(tx, adminID); err != nil {
if err != nil {
return err return err
} }
if err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations); err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users) kandangs, err := seedKandangs(tx, adminID, locations, users)
if err != nil { if err != nil {
return err return err
@@ -93,10 +85,6 @@ func Run(db *gorm.DB) error {
if err := seedTransferStock(tx, adminID); err != nil { if err := seedTransferStock(tx, adminID); err != nil {
return err return err
} }
if err := seedChickin(tx, adminID); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed") fmt.Println("✅ Master data seeding completed")
return nil return nil
}) })
@@ -243,158 +231,12 @@ func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil return result, nil
} }
func seedProjectFlocks(
tx *gorm.DB,
createdBy uint,
flocks, areas, fcrs, locations map[string]uint,
) error {
seeds := []struct {
Key string
Flock string
Area string
Category utils.ProjectFlockCategory
Fcr string
Location string
Period int
}{
{
Key: "Singaparna Period 1",
Flock: "Flock Priangan",
Area: "Priangan",
Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer",
Location: "Singaparna",
Period: 1,
},
{
Key: "Cikaum Period 1",
Flock: "Flock Banten",
Area: "Banten",
Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer",
Location: "Cikaum",
Period: 1,
},
}
for _, seed := range seeds {
flockID, ok := flocks[seed.Flock]
if !ok {
return fmt.Errorf("floc %s not seeded", seed.Flock)
}
areaID, ok := areas[seed.Area]
if !ok {
return fmt.Errorf("area %s not seeded", seed.Area)
}
fcrID, ok := fcrs[seed.Fcr]
if !ok {
return fmt.Errorf("fcr %s not seeded", seed.Fcr)
}
locationID, ok := locations[seed.Location]
if !ok {
return fmt.Errorf("location %s not seeded", seed.Location)
}
var projectFlock entity.ProjectFlock
err := tx.Where(
"flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
flockID, areaID, seed.Category, fcrID, locationID, seed.Period,
).First(&projectFlock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
projectFlock = entity.ProjectFlock{
FlockId: flockID,
AreaId: areaID,
Category: string(seed.Category),
FcrId: fcrID,
LocationId: locationID,
Period: seed.Period,
CreatedBy: createdBy,
}
if err := tx.Create(&projectFlock).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
"flock_id": flockID,
"area_id": areaID,
"category": string(seed.Category),
"fcr_id": fcrID,
"location_id": locationID,
"period": seed.Period,
}).Error; err != nil {
return err
}
}
if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil {
return err
}
}
return nil
}
func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint) error {
if projectFlockID == 0 || actorID == 0 {
return nil
}
workflow := utils.ApprovalWorkflowProjectFlock.String()
steps := []struct {
step approvalutils.ApprovalStep
action entity.ApprovalAction
}{
{step: utils.ProjectFlockStepPengajuan, action: entity.ApprovalActionCreated},
{step: utils.ProjectFlockStepAktif, action: entity.ApprovalActionApproved},
}
for _, cfg := range steps {
var count int64
if err := tx.Model(&entity.Approval{}).
Where("approvable_type = ? AND approvable_id = ? AND step_number = ?", workflow, projectFlockID, uint16(cfg.step)).
Count(&count).Error; err != nil {
return err
}
if count > 0 {
continue
}
stepName, ok := utils.ProjectFlockApprovalSteps[cfg.step]
if !ok || strings.TrimSpace(stepName) == "" {
stepName = fmt.Sprintf("Step %d", cfg.step)
}
var actionPtr *entity.ApprovalAction
action := cfg.action
actionPtr = &action
record := entity.Approval{
ApprovableType: workflow,
ApprovableId: projectFlockID,
StepNumber: uint16(cfg.step),
StepName: stepName,
Action: actionPtr,
ActionBy: uintPtr(actorID),
}
if err := tx.Create(&record).Error; err != nil {
return err
}
}
return nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) { func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
seeds := []struct { seeds := []struct {
Name string Name string
Status utils.KandangStatus Status utils.KandangStatus
Location string Location string
PicKey string PicKey string
}{ }{
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"}, {Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"}, {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
@@ -414,16 +256,15 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return nil, fmt.Errorf("user %s not seeded", seed.PicKey) return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
} }
var kandang entity.Kandang var kandang entity.Kandang
err := tx.Where("name = ?", seed.Name).First(&kandang).Error err := tx.Where("name = ?", seed.Name).First(&kandang).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
kandang = entity.Kandang{ kandang = entity.Kandang{
Name: seed.Name, Name: seed.Name,
Status: string(seed.Status), Status: string(seed.Status),
LocationId: locID, LocationId: locID,
PicId: picID, PicId: picID,
CreatedBy: createdBy, CreatedBy: createdBy,
} }
if err := tx.Create(&kandang).Error; err != nil { if err := tx.Create(&kandang).Error; err != nil {
return nil, err return nil, err
@@ -446,7 +287,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return result, nil return result, nil
} }
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error { func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -525,6 +365,7 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error)
}{ }{
{"Bahan Baku", "RAW"}, {"Bahan Baku", "RAW"},
{"Day Old Chick", "DOC"}, {"Day Old Chick", "DOC"},
{"Telur", "EGG"},
} }
result := make(map[string]uint, len(seeds)) result := make(map[string]uint, len(seeds))
@@ -739,6 +580,22 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
}, },
{
Name: "Telur Konsumsi Baik",
Brand: "Layer Farm",
Sku: "EGG-GOOD",
Uom: "Unit",
Category: "Telur",
Price: 1800,
},
{
Name: "Telur Pecah",
Brand: "Layer Farm",
Sku: "EGG-CRACK",
Uom: "Unit",
Category: "Telur",
Price: 900,
},
} }
for _, seed := range seeds { for _, seed := range seeds {
@@ -978,25 +835,44 @@ func seedBanks(tx *gorm.DB, createdBy uint) error {
} }
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error { func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
seeds := []struct { seeds := []struct {
ProductID uint ProductName string
WarehouseID uint WarehouseName string
Quantity float64 Quantity float64
}{ }{
{ProductID: 1, WarehouseID: 1, Quantity: 100}, {ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 100},
{ProductID: 2, WarehouseID: 2, Quantity: 200}, {ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
{ProductID: 2, WarehouseID: 1, Quantity: 300}, {ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
{ProductID: 1, WarehouseID: 3, Quantity: 5000}, {ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
} }
for _, seed := range seeds { for _, seed := range seeds {
var product entity.Product
if err := tx.Where("name = ?", seed.ProductName).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("product %q not found for product warehouse seeding", seed.ProductName)
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("name = ?", seed.WarehouseName).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("warehouse %q not found for product warehouse seeding", seed.WarehouseName)
}
return err
}
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
err := tx.Where("product_id = ? AND warehouse_id = ?", seed.ProductID, seed.WarehouseID).First(&productWarehouse).Error err := tx.Where("product_id = ? AND warehouse_id = ?", product.Id, warehouse.Id).First(&productWarehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
productWarehouse = entity.ProductWarehouse{ productWarehouse = entity.ProductWarehouse{
ProductId: seed.ProductID, ProductId: product.Id,
WarehouseId: seed.WarehouseID, WarehouseId: warehouse.Id,
Quantity: seed.Quantity, Quantity: seed.Quantity,
CreatedBy: createdBy, CreatedBy: createdBy,
} }
@@ -1005,6 +881,12 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
} }
} else if err != nil { } else if err != nil {
return err return err
} else {
if err := tx.Model(&productWarehouse).Updates(map[string]any{
"quantity": seed.Quantity,
}).Error; err != nil {
return err
}
} }
} }
@@ -1085,71 +967,6 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error {
return nil return nil
} }
func seedChickin(tx *gorm.DB, createdBy uint) error {
// gunakan identitas yang stabil, bukan ID pivot
seeds := []struct {
KandangName string
LocationName string
Period int
ChickInDate string
Quantity float64
Note string
}{
{"Singaparna 1", "Singaparna", 1, "2025-10-20", 100, "Seeder chickin 1"},
{"Cikaum 1", "Cikaum", 1, "2025-10-21", 200, "Seeder chickin 2"},
}
for _, s := range seeds {
pfkID, err := ensurePFK(tx, s.KandangName, s.LocationName, s.Period)
if err != nil { return err }
date, err := time.Parse("2006-01-02", s.ChickInDate)
if err != nil { return err }
// upsert project_chickin (idempotent)
var chickin entity.ProjectChickin
err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", pfkID, date).First(&chickin).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
chickin = entity.ProjectChickin{
ProjectFlockKandangId: pfkID,
ChickInDate: date,
Quantity: s.Quantity,
Note: s.Note,
CreatedBy: createdBy,
}
if err := tx.Create(&chickin).Error; err != nil { return err }
} else if err != nil {
return err
}
// upsert population
var pop entity.ProjectFlockPopulation
err = tx.Where("project_flock_kandang_id = ?", pfkID).First(&pop).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
pop = entity.ProjectFlockPopulation{
ProjectFlockKandangId: pfkID,
InitialQuantity: s.Quantity,
CurrentQuantity: s.Quantity,
ReservedQuantity: 0,
CreatedBy: createdBy,
}
if err := tx.Create(&pop).Error; err != nil { return err }
} else if err != nil {
return err
} else {
if err := tx.Model(&entity.ProjectFlockPopulation{}).
Where("id = ?", pop.Id).
Updates(map[string]any{
"initial_quantity": pop.InitialQuantity + s.Quantity,
"current_quantity": pop.CurrentQuantity + s.Quantity,
"reserved_quantity": 0,
}).Error; err != nil { return err }
}
}
return nil
}
func ptr[T any](v T) *T { func ptr[T any](v T) *T {
return &v return &v
} }
@@ -1165,30 +982,3 @@ func intPtr(v int) *int {
func uintPtr(v uint) *uint { func uintPtr(v uint) *uint {
return &v return &v
} }
func ensurePFK(tx *gorm.DB, kandangName, locationName string, period int) (uint, error) {
var kandang entity.Kandang
if err := tx.Where("name = ?", kandangName).First(&kandang).Error; err != nil {
return 0, fmt.Errorf("kandang %q not found: %w", kandangName, err)
}
var loc entity.Location
if err := tx.Where("name = ?", locationName).First(&loc).Error; err != nil {
return 0, fmt.Errorf("location %q not found: %w", locationName, err)
}
var pf entity.ProjectFlock
if err := tx.Where("location_id = ? AND period = ?", loc.Id, period).First(&pf).Error; err != nil {
return 0, fmt.Errorf("project_flock for %s period %d not found: %w", locationName, period, err)
}
var pfk entity.ProjectFlockKandang
if err := tx.Where("project_flock_id = ? AND kandang_id = ?", pf.Id, kandang.Id).First(&pfk).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
pfk = entity.ProjectFlockKandang{ ProjectFlockId: pf.Id, KandangId: kandang.Id }
if err := tx.Create(&pfk).Error; err != nil {
return 0, fmt.Errorf("create pivot pfk(%d,%d) failed: %w", pf.Id, kandang.Id, err)
}
} else {
return 0, err
}
}
return pfk.Id, nil
}
+2 -3
View File
@@ -8,18 +8,17 @@ import (
type ProjectFlock struct { type ProjectFlock struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` FlockName string `gorm:"type:varchar(255);not null;uniqueIndex"`
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
Category string `gorm:"type:varchar(20);not null"` Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"` FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` Period int `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"` Area Area `gorm:"foreignKey:AreaId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
+7 -8
View File
@@ -8,20 +8,16 @@ import (
type Recording struct { type Recording struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"column:project_flock_id;not null;index"` ProjectFlockKandangId uint `gorm:"column:project_flock_kandangs_id;not null;index"`
RecordDatetime time.Time `gorm:"column:record_datetime;not null"` RecordDatetime time.Time `gorm:"column:record_datetime;not null"`
RecordDate *time.Time `gorm:"column:record_date"`
Ontime int `gorm:"column:ontime;not null;default:0"`
Day *int `gorm:"column:day"` Day *int `gorm:"column:day"`
TotalDepletion *int `gorm:"column:total_depletion"` TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"` CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
DailyGain *float64 `gorm:"column:daily_gain"` DailyGain *float64 `gorm:"column:daily_gain"`
AvgDailyGain *float64 `gorm:"column:avg_daily_gain"` AvgDailyGain *float64 `gorm:"column:avg_daily_gain"`
CumIntake *int64 `gorm:"column:cum_intake"` CumIntake *int `gorm:"column:cum_intake"`
FcrValue *float64 `gorm:"column:fcr_value"` FcrValue *float64 `gorm:"column:fcr_value"`
TotalChick *int64 `gorm:"column:total_chick"` TotalChickQty *float64 `gorm:"column:total_chick_qty"`
DailyDepletionRate *float64 `gorm:"column:daily_depletion_rate"`
CumDepletion *int `gorm:"column:cum_depletion"`
CreatedBy uint `gorm:"column:created_by"` CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
@@ -32,4 +28,7 @@ type Recording struct {
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"` BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"` Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"` Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
} }
+8 -9
View File
@@ -1,16 +1,15 @@
package entities package entities
import "time" import "time"
type RecordingBW struct { type RecordingBW struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"` RecordingId uint `gorm:"column:recording_id;not null;index"`
Weight float64 `gorm:"column:weight;not null"` AvgWeight float64 `gorm:"column:avg_weight;not null"`
Qty int `gorm:"column:qty;not null;default:1"` Qty float64 `gorm:"column:qty;not null"`
Notes *string `gorm:"column:notes"` TotalWeight float64 `gorm:"column:total_weight;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
} }
+6 -8
View File
@@ -1,13 +1,11 @@
package entities package entities
type RecordingDepletion struct { type RecordingDepletion struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"` RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Total int64 `gorm:"column:total;not null"` Qty float64 `gorm:"column:qty;not null"`
Notes *string `gorm:"column:notes"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
} }
+30
View File
@@ -0,0 +1,30 @@
package entities
import "time"
type RecordingEgg struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty int `gorm:"column:qty;not null"`
CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
}
type GradingEgg struct {
Id uint `gorm:"primaryKey"`
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
Qty float64 `gorm:"column:qty;not null"`
Grade string `gorm:"column:grade;type:varchar(50)"`
CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+7 -9
View File
@@ -1,14 +1,12 @@
package entities package entities
type RecordingStock struct { type RecordingStock struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"` RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Increase *float64 `gorm:"column:increase"` UsageQty *float64 `gorm:"column:usage_qty"`
Decrease *float64 `gorm:"column:decrease"` PendingQty *float64 `gorm:"column:pending_qty"`
UsageAmount *int64 `gorm:"column:usage_amount"`
Notes *string `gorm:"column:notes"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
} }
@@ -202,21 +202,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
if query.TransactionType != "" { if query.TransactionType != "" {
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType)) db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
} }
if query.ProductID > 0 { db = s.StockLogsRepository.ApplyProductWarehouseFilters(db, uint(query.ProductID), uint(query.WarehouseID))
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
Where("product_warehouses.product_id = ?", query.ProductID)
}
if query.WarehouseID > 0 {
if query.ProductID > 0 {
db = db.Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
} else {
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
}
}
return db.Order("created_at DESC") return db.Order("created_at DESC")
}) })
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -17,34 +18,35 @@ type ProductWarehouseRepository interface {
ExistsByID(ctx context.Context, id uint) (bool, error) ExistsByID(ctx context.Context, id uint) (bool, error)
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, 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)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
} }
type ProductWarehouseRepositoryImpl struct { type ProductWarehouseRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProductWarehouse] *repository.BaseRepositoryImpl[entity.ProductWarehouse]
db *gorm.DB
} }
func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository { func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
return &ProductWarehouseRepositoryImpl{ return &ProductWarehouseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db),
db: db,
} }
} }
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.db, productId) return repository.Exists[entity.Product](ctx, r.DB(), productId)
} }
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
return repository.Exists[entity.Warehouse](ctx, r.db, warehouseId) return repository.Exists[entity.Warehouse](ctx, r.DB(), warehouseId)
} }
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id) return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
} }
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
var count int64 var count int64
query := r.db.WithContext(ctx).Model(&entity.ProductWarehouse{}). query := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId) Where("product_id = ? AND warehouse_id = ?", productId, warehouseId)
if excludeID != nil { if excludeID != nil {
query = query.Where("id != ?", *excludeID) query = query.Where("id != ?", *excludeID)
@@ -57,7 +59,7 @@ func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Cont
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
var count int64 var count int64
if err := r.db.WithContext(ctx). if err := r.DB().WithContext(ctx).
Model(&entity.ProductWarehouse{}). Model(&entity.ProductWarehouse{}).
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId). Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
Count(&count).Error; err != nil { Count(&count).Error; err != nil {
@@ -76,7 +78,7 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) { func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse var productWarehouses []entity.ProductWarehouse
err := r.db.WithContext(ctx). err := r.DB().WithContext(ctx).
Table("product_warehouses"). Table("product_warehouses").
Select("product_warehouses.*"). Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN products ON products.id = product_warehouses.product_id").
@@ -89,3 +91,58 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con
} }
return productWarehouses, nil return productWarehouses, nil
} }
func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
query := r.DB()
if db != nil {
query = db
}
fmt.Println(warehouseId)
err := query.WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
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").
First(&productWarehouse).Error
if err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB {
if len(flags) == 0 {
return db
}
return db.
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 IN ?", flags)
}
func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error {
if len(deltas) == 0 {
return nil
}
base := r.DB().WithContext(ctx)
if modifier != nil {
base = modifier(base)
}
for id, delta := range deltas {
if delta == 0 {
continue
}
if err := base.Model(&entity.ProductWarehouse{}).
Where("id = ?", id).
Update("quantity", gorm.Expr("COALESCE(quantity,0) + ?", delta)).Error; err != nil {
return err
}
}
return nil
}
@@ -84,11 +84,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
db = db.Where("warehouse_id = ?", params.WarehouseId) db = db.Where("warehouse_id = ?", params.WarehouseId)
} }
if len(cleanFlags) > 0 { db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
db = db.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 IN ?", cleanFlags)
}
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -11,6 +11,7 @@ import (
type FlockRepository interface { type FlockRepository interface {
repository.BaseRepository[entity.Flock] repository.BaseRepository[entity.Flock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
GetByName(ctx context.Context, name string) (*entity.Flock, error)
} }
type FlockRepositoryImpl struct { type FlockRepositoryImpl struct {
@@ -28,3 +29,15 @@ func NewFlockRepository(db *gorm.DB) FlockRepository {
func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID) return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID)
} }
func (r *FlockRepositoryImpl) GetByName(ctx context.Context, name string) (*entity.Flock, error) {
var flock entity.Flock
err := r.db.WithContext(ctx).
Where("LOWER(name) = LOWER(?)", name).
Where("deleted_at IS NULL").
First(&flock).Error
if err != nil {
return nil, err
}
return &flock, nil
}
@@ -20,7 +20,7 @@ type KandangRepository interface {
HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error)
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error
UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error
} }
type KandangRepositoryImpl struct { type KandangRepositoryImpl struct {
@@ -61,15 +61,15 @@ func (r *KandangRepositoryImpl) ProjectFlockExists(ctx context.Context, projectF
func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) { func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) {
var count int64 var count int64
q := r.db.WithContext(ctx). q := r.db.WithContext(ctx).
Table("kandangs k"). Table("kandangs k").
Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id"). Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
Where("pfk.project_flock_id = ?", projectFlockID). Where("pfk.project_flock_id = ?", projectFlockID).
Where("k.status = ?", utils.KandangStatusActive). Where("k.status = ?", utils.KandangStatusActive).
Where("k.deleted_at IS NULL") Where("k.deleted_at IS NULL")
if excludeID != nil { if excludeID != nil {
q = q.Where("k.id <> ?", *excludeID) q = q.Where("k.id <> ?", *excludeID)
} }
if err := q.Count(&count).Error; err != nil { if err := q.Count(&count).Error; err != nil {
return false, err return false, err
} }
@@ -78,49 +78,59 @@ func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Cont
func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) { func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) {
kandang := new(entity.Kandang) kandang := new(entity.Kandang)
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Table("kandangs k"). Table("kandangs k").
Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id"). Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
Where("pfk.project_flock_id = ?", projectFlockID). Where("pfk.project_flock_id = ?", projectFlockID).
Where("k.deleted_at IS NULL"). Where("k.deleted_at IS NULL").
Order("k.id ASC"). Order("k.id ASC").
Limit(1). Limit(1).
Find(kandang).Error Find(kandang).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
if kandang.Id == 0 { if kandang.Id == 0 {
return nil, gorm.ErrRecordNotFound return nil, gorm.ErrRecordNotFound
} }
return kandang, nil return kandang, nil
} }
func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error { func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error {
sub := r.db.WithContext(ctx). sub := r.db.WithContext(ctx).
Table("project_flock_kandangs"). Table("project_flock_kandangs").
Select("kandang_id"). Select("kandang_id").
Where("project_flock_id = ?", projectFlockID) Where("project_flock_id = ?", projectFlockID)
return r.db.WithContext(ctx). return r.db.WithContext(ctx).
Model(&entity.Kandang{}). Model(&entity.Kandang{}).
Where("id IN (?)", sub). Where("id IN (?)", sub).
Where("deleted_at IS NULL"). Where("deleted_at IS NULL").
Update("status", string(status)).Error Update("status", string(status)).Error
} }
func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error { func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error {
var link entity.ProjectFlockKandang var link entity.ProjectFlockKandang
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID). Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&link).Error First(&link).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
link = entity.ProjectFlockKandang{ link = entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID, ProjectFlockId: projectFlockID,
KandangId: kandangID, KandangId: kandangID,
} }
return r.db.WithContext(ctx).Create(&link).Error return r.db.WithContext(ctx).Create(&link).Error
} }
return err return err
} }
func (r *KandangRepositoryImpl) UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error {
if len(kandangIDs) == 0 {
return nil
}
return r.db.WithContext(ctx).
Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs).
Where("deleted_at IS NULL").
Update("status", string(status)).Error
}
@@ -9,6 +9,7 @@ import (
flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
@@ -88,9 +89,9 @@ func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO {
func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
var flock *flockBaseDTO.FlockBaseDTO var flock *flockBaseDTO.FlockBaseDTO
if e.Flock.Id != 0 { if base := pfutils.DeriveBaseName(e.FlockName); base != "" {
mapped := flockBaseDTO.ToFlockBaseDTO(e.Flock) summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base}
flock = &mapped flock = &summary
} }
var area *areaBaseDTO.AreaBaseDTO var area *areaBaseDTO.AreaBaseDTO
if e.Area.Id != 0 { if e.Area.Id != 0 {
@@ -63,7 +63,6 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB {
Preload("ProjectFlockKandang.Kandang.Location.Area"). Preload("ProjectFlockKandang.Kandang.Location.Area").
Preload("ProjectFlockKandang.Kandang.Pic"). Preload("ProjectFlockKandang.Kandang.Pic").
Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.Flock").
Preload("ProjectFlockKandang.ProjectFlock.Area"). Preload("ProjectFlockKandang.ProjectFlock.Area").
Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("ProjectFlockKandang.ProjectFlock.Fcr").
Preload("ProjectFlockKandang.ProjectFlock.Location"). Preload("ProjectFlockKandang.ProjectFlock.Location").
@@ -340,15 +339,12 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
return rollback(err) return rollback(err)
} }
var productWarehouse entity.ProductWarehouse productWarehouse, err := s.ProductWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(
err = tx.WithContext(c.Context()).Table("product_warehouses"). c.Context(),
Select("product_warehouses.*"). "DOC",
Joins("JOIN products ON products.id = product_warehouses.product_id"). warehouse.Id,
Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). tx,
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id). )
Order("product_warehouses.created_at DESC").
First(&productWarehouse).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")) return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse"))
@@ -10,14 +10,16 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/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" 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" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
) )
type ProjectFlockBaseDTO struct { type ProjectFlockBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Period int `json:"period"` Period int `json:"period"`
FlockName string `json:"flock_name"`
} }
type ProjectFlockListDTO struct { type ProjectFlockListDTO struct {
@@ -59,9 +61,9 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
} }
var flockSummary *flockDTO.FlockBaseDTO var flockSummary *flockDTO.FlockBaseDTO
if e.Flock.Id != 0 { if baseName := pfutils.DeriveBaseName(e.FlockName); baseName != "" {
mapped := flockDTO.ToFlockBaseDTO(e.Flock) summary := flockDTO.FlockBaseDTO{Id: 0, Name: baseName}
flockSummary = &mapped flockSummary = &summary
} }
var areaSummary *areaDTO.AreaBaseDTO var areaSummary *areaDTO.AreaBaseDTO
@@ -144,8 +146,9 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv
func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
return ProjectFlockBaseDTO{ return ProjectFlockBaseDTO{
Id: e.Id, Id: e.Id,
Period: e.Period, Period: e.Period,
FlockName: e.FlockName,
} }
} }
@@ -7,6 +7,7 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/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" 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" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
@@ -48,15 +49,16 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
pfLocal := ProjectFlockWithPivotDTO{ pfLocal := ProjectFlockWithPivotDTO{
ProjectFlockBaseDTO: ProjectFlockBaseDTO{ ProjectFlockBaseDTO: ProjectFlockBaseDTO{
Id: e.ProjectFlock.Id, Id: e.ProjectFlock.Id,
Period: e.ProjectFlock.Period, Period: e.ProjectFlock.Period,
FlockName: e.ProjectFlock.FlockName,
}, },
Category: e.ProjectFlock.Category, Category: e.ProjectFlock.Category,
} }
if e.ProjectFlock.Flock.Id != 0 { if base := pfutils.DeriveBaseName(e.ProjectFlock.FlockName); base != "" {
mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock) summary := flockDTO.FlockBaseDTO{Id: 0, Name: base}
pfLocal.Flock = &mapped pfLocal.Flock = &summary
} }
if e.ProjectFlock.Area.Id != 0 { if e.ProjectFlock.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.ProjectFlock.Area) mapped := areaDTO.ToAreaBaseDTO(e.ProjectFlock.Area)
@@ -75,11 +77,6 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
pfLocal.CreatedUser = &mapped pfLocal.CreatedUser = &mapped
} }
pivotMap := make(map[uint]uint)
for _, ph := range e.ProjectFlock.KandangHistory {
pivotMap[ph.KandangId] = ph.Id
}
for _, k := range e.ProjectFlock.Kandangs { for _, k := range e.ProjectFlock.Kandangs {
kb := kandangDTO.ToKandangBaseDTO(k) kb := kandangDTO.ToKandangBaseDTO(k)
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{ pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
@@ -3,19 +3,30 @@ package repository
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))"
type ProjectflockRepository interface { type ProjectflockRepository interface {
repository.BaseRepository[entity.ProjectFlock] repository.BaseRepository[entity.ProjectFlock]
GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error)
GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error)
GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) GetNextSequenceForBase(ctx context.Context, baseName string) (int, error)
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error)
WithDefaultRelations() func(*gorm.DB) *gorm.DB
ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error)
AreaExists(ctx context.Context, id uint) (bool, error)
FcrExists(ctx context.Context, id uint) (bool, error)
LocationExists(ctx context.Context, id uint) (bool, error)
} }
type ProjectflockRepositoryImpl struct { type ProjectflockRepositoryImpl struct {
@@ -28,11 +39,11 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
} }
} }
func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) { func (r *ProjectflockRepositoryImpl) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) {
var records []entity.ProjectFlock var records []entity.ProjectFlock
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
Unscoped(). Unscoped().
Where("flock_id = ?", flockID). Where(baseNameExpression+" = LOWER(?)", baseName).
Order("period ASC"). Order("period ASC").
Find(&records).Error; err != nil { Find(&records).Error; err != nil {
return nil, err return nil, err
@@ -40,10 +51,10 @@ func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID
return records, nil return records, nil
} }
func (r *ProjectflockRepositoryImpl) GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) { func (r *ProjectflockRepositoryImpl) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) {
var record entity.ProjectFlock var record entity.ProjectFlock
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
Where("flock_id = ?", flockID). Where(baseNameExpression+" = LOWER(?)", baseName).
Order("period DESC"). Order("period DESC").
First(&record).Error First(&record).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -55,11 +66,11 @@ func (r *ProjectflockRepositoryImpl) GetActiveByFlock(ctx context.Context, flock
return &record, nil return &record, nil
} }
func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) { func (r *ProjectflockRepositoryImpl) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
var max int var max int
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlock{}). Model(&entity.ProjectFlock{}).
Where("flock_id = ?", flockID). Where(baseNameExpression+" = LOWER(?)", baseName).
Select("COALESCE(MAX(period), 0)"). Select("COALESCE(MAX(period), 0)").
Scan(&max).Error; err != nil { Scan(&max).Error; err != nil {
return 0, err return 0, err
@@ -67,13 +78,13 @@ func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, fl
return max, nil return max, nil
} }
func (r *ProjectflockRepositoryImpl) GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) { func (r *ProjectflockRepositoryImpl) GetNextSequenceForBase(ctx context.Context, baseName string) (int, error) {
var payload struct { var payload struct {
Period int Period int
} }
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlock{}). Model(&entity.ProjectFlock{}).
Where("flock_id = ?", flockID). Where(baseNameExpression+" = LOWER(?)", baseName).
Clauses(clause.Locking{Strength: "UPDATE"}). Clauses(clause.Locking{Strength: "UPDATE"}).
Order("period DESC"). Order("period DESC").
Limit(1). Limit(1).
@@ -86,3 +97,164 @@ func (r *ProjectflockRepositoryImpl) GetNextPeriodForFlock(ctx context.Context,
} }
return payload.Period + 1, nil return payload.Period + 1, nil
} }
func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
db = r.withDefaultRelations(db)
return r.applyQueryFilters(db, params)
})
}
func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withDefaultRelations(db)
}
}
func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs")
}
func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB {
if params == nil {
return db
}
if params.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", params.AreaId)
}
if params.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", params.LocationId)
}
if params.Period > 0 {
db = db.Where("project_flocks.period = ?", params.Period)
}
if len(params.KandangIds) > 0 {
db = db.Where(`
EXISTS (
SELECT 1
FROM project_flock_kandangs pfk
WHERE pfk.project_flock_id = project_flocks.id
AND pfk.kandang_id IN ?
)`, params.KandangIds)
}
db = r.applySearchFilters(db, params.Search)
for _, expr := range r.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
}
func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB {
if rawSearch == "" {
return db
}
normalized := strings.ToLower(strings.TrimSpace(rawSearch))
if normalized == "" {
return db
}
likeQuery := "%" + normalized + "%"
return db.
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(`
LOWER(areas.name) LIKE ?
OR LOWER(project_flocks.category) LIKE ?
OR LOWER(fcrs.name) LIKE ?
OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ?
OR LOWER(created_users.name) LIKE ?
OR LOWER(created_users.email) LIKE ?
OR LOWER(project_flocks.flock_name) LIKE ?
OR LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))) LIKE ?
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
OR EXISTS (
SELECT 1 FROM kandangs
WHERE kandangs.project_flock_id = project_flocks.id
AND LOWER(kandangs.name) LIKE ?
)
`,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
)
}
func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Area](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) FcrExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Fcr](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) LocationExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Location](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder string) []string {
direction := "ASC"
if strings.ToLower(sortOrder) == "desc" {
direction = "DESC"
}
switch sortBy {
case "area":
return []string{
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "location":
return []string{
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "kandangs":
return []string{
fmt.Sprintf("(SELECT COUNT(*) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "period":
return []string{
fmt.Sprintf("project_flocks.period %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
default:
return []string{
"project_flocks.created_at DESC",
"project_flocks.updated_at DESC",
}
}
}
func (r *ProjectflockRepositoryImpl) ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) {
var count int64
q := r.DB().WithContext(ctx).Model(&entity.ProjectFlock{}).Where("flock_name = ?", flockName)
if excludeID != nil && *excludeID != 0 {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
@@ -13,6 +13,9 @@ type ProjectFlockKandangRepository interface {
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB DB() *gorm.DB
} }
@@ -45,7 +48,6 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
var records []entity.ProjectFlockKandang var records []entity.ProjectFlockKandang
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
@@ -72,7 +74,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
record := new(entity.ProjectFlockKandang) record := new(entity.ProjectFlockKandang)
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
@@ -91,7 +92,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID). Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
@@ -104,3 +104,48 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
} }
return record, nil return record, nil
} }
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
if len(kandangIDs) == 0 {
return nil, nil
}
var existing []uint
err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
Pluck("kandang_id", &existing).Error
return existing, err
}
func (r *projectFlockKandangRepositoryImpl) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) {
if len(kandangIDs) == 0 {
return false, nil
}
q := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("kandang_id IN ?", kandangIDs)
if exceptProjectID != nil {
q = q.Where("project_flock_id <> ?", *exceptProjectID)
}
var count int64
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *projectFlockKandangRepositoryImpl) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) {
if len(kandangIDs) == 0 {
return nil, nil
}
var kandangs []entity.Kandang
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("pfk.kandang_id AS id, COALESCE(k.name, '') AS name").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("LEFT JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("pfk.project_flock_id = ? AND pfk.kandang_id IN ?", projectFlockID, kandangIDs).
Group("pfk.kandang_id, k.name").
Scan(&kandangs).Error
return kandangs, err
}
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
@@ -14,6 +15,7 @@ import (
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -29,24 +31,24 @@ type ProjectflockService interface {
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
} }
type projectflockService struct { type projectflockService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.ProjectflockRepository Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository KandangRepo kandangRepository.KandangRepository
WarehouseRepo warehouseRepository.WarehouseRepository WarehouseRepo warehouseRepository.WarehouseRepository
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
PivotRepo repository.ProjectFlockKandangRepository PivotRepo repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey approvalWorkflow approvalutils.ApprovalWorkflowKey
} }
type FlockPeriodSummary struct { type FlockPeriodSummary struct {
@@ -65,29 +67,19 @@ func NewProjectflockService(
validate *validator.Validate, validate *validator.Validate,
) ProjectflockService { ) ProjectflockService {
return &projectflockService{ return &projectflockService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
FlockRepo: flockRepo, FlockRepo: flockRepo,
KandangRepo: kandangRepo, KandangRepo: kandangRepo,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
PivotRepo: pivotRepo, PivotRepo: pivotRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock, approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
} }
} }
func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Flock").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs")
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return nil, 0, err return nil, 0, err
@@ -102,79 +94,11 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
db = s.withRelations(db)
if params.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", params.AreaId)
}
if params.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", params.LocationId)
}
if params.Period > 0 {
db = db.Where("project_flocks.period = ?", params.Period)
}
if len(params.KandangIds) > 0 {
db = db.Where(`
EXISTS (
SELECT 1
FROM project_flock_kandangs pfk
WHERE pfk.project_flock_id = project_flocks.id
AND pfk.kandang_id IN ?
)`, params.KandangIds)
}
if params.Search != "" {
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
if normalizedSearch == "" {
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
}
likeQuery := "%" + normalizedSearch + "%"
db = db.
Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id").
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(`
LOWER(flocks.name) LIKE ?
OR LOWER(areas.name) LIKE ?
OR LOWER(project_flocks.category) LIKE ?
OR LOWER(fcrs.name) LIKE ?
OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ?
OR LOWER(created_users.name) LIKE ?
OR LOWER(created_users.email) LIKE ?
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
OR EXISTS (
SELECT 1 FROM kandangs
WHERE kandangs.project_flock_id = project_flocks.id
AND LOWER(kandangs.name) LIKE ?
)
`,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
)
}
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
})
if err != nil { if err != nil {
s.Log.Errorf("Failed to get projectflocks: %+v", err) s.Log.Errorf("Failed to get projectflocks: %+v", err)
return nil, 0, err return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks")
} }
if s.ApprovalSvc != nil && len(projectflocks) > 0 { if s.ApprovalSvc != nil && len(projectflocks) > 0 {
@@ -201,13 +125,13 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
} }
func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
projectflock, err := s.Repository.GetByID(c.Context(), id, s.withRelations) projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
if err != nil { if err != nil {
s.Log.Errorf("Failed get projectflock by id: %+v", err) s.Log.Errorf("Failed get projectflock by id: %+v", err)
return nil, err return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
if s.ApprovalSvc != nil { if s.ApprovalSvc != nil {
@@ -243,15 +167,28 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
} }
baseName := strings.TrimSpace(req.FlockName)
if baseName == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists},
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
canonicalBase := baseName
if s.FlockRepo != nil {
baseFlock, err := s.ensureFlockByName(c.Context(), baseName)
if err != nil {
return nil, err
}
canonicalBase = baseFlock.Name
}
kandangIDs := uniqueUintSlice(req.KandangIds) kandangIDs := uniqueUintSlice(req.KandangIds)
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil) kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
if err != nil { if err != nil {
@@ -264,14 +201,14 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
} }
// larang kalau ada yg sudah terikat ke project lain // larang kalau ada yg sudah terikat ke project lain
if linked, err := s.anyKandangLinkedToOtherProject(c.Context(), s.Repository.DB(), kandangIDs, nil); err != nil { if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
} else if linked { } else if linked {
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain") return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
} }
createBody := &entity.ProjectFlock{ createBody := &entity.ProjectFlock{
FlockId: req.FlockId, FlockName: "",
AreaId: req.AreaId, AreaId: req.AreaId,
Category: cat, Category: cat,
FcrId: req.FcrId, FcrId: req.FcrId,
@@ -282,11 +219,16 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction) projectRepo := repository.NewProjectflockRepository(dbTransaction)
period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId) nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), canonicalBase)
if err != nil { if err != nil {
return err return err
} }
createBody.Period = period generatedName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, nextSeq, nil)
if err != nil {
return err
}
createBody.FlockName = generatedName
createBody.Period = seq
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
return err return err
@@ -312,11 +254,14 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
}) })
if err != nil { if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
if errors.Is(err, gorm.ErrDuplicatedKey) { if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists") return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
} }
s.Log.Errorf("Failed to create projectflock: %+v", err) s.Log.Errorf("Failed to create projectflock: %+v", err)
return nil, err return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project flock")
} }
return s.GetOne(c, createBody.Id) return s.GetOne(c, createBody.Id)
@@ -327,7 +272,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, err return nil, err
} }
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations) existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
@@ -338,15 +283,28 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
updateBody := make(map[string]any) updateBody := make(map[string]any)
hasBodyChanges := false hasBodyChanges := false
var relationChecks []commonSvc.RelationCheck var relationChecks []commonSvc.RelationCheck
existingBase := pfutils.DeriveBaseName(existing.FlockName)
targetBaseName := existingBase
needFlockNameRegenerate := false
if req.FlockId != nil { if req.FlockName != nil {
updateBody["flock_id"] = *req.FlockId trimmed := strings.TrimSpace(*req.FlockName)
hasBodyChanges = true if trimmed == "" {
relationChecks = append(relationChecks, commonSvc.RelationCheck{ return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
Name: "Flock", }
ID: req.FlockId, canonicalBase := trimmed
Exists: relationExistsChecker[entity.Flock](s.Repository.DB()), if s.FlockRepo != nil {
}) flockEntity, err := s.ensureFlockByName(c.Context(), trimmed)
if err != nil {
return nil, err
}
canonicalBase = flockEntity.Name
}
if !strings.EqualFold(canonicalBase, existingBase) {
needFlockNameRegenerate = true
targetBaseName = canonicalBase
hasBodyChanges = true
}
} }
if req.AreaId != nil { if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId updateBody["area_id"] = *req.AreaId
@@ -354,7 +312,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
relationChecks = append(relationChecks, commonSvc.RelationCheck{ relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Area", Name: "Area",
ID: req.AreaId, ID: req.AreaId,
Exists: relationExistsChecker[entity.Area](s.Repository.DB()), Exists: s.Repository.AreaExists,
}) })
} }
if req.Category != nil { if req.Category != nil {
@@ -371,7 +329,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
relationChecks = append(relationChecks, commonSvc.RelationCheck{ relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "FCR", Name: "FCR",
ID: req.FcrId, ID: req.FcrId,
Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()), Exists: s.Repository.FcrExists,
}) })
} }
if req.LocationId != nil { if req.LocationId != nil {
@@ -380,7 +338,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
relationChecks = append(relationChecks, commonSvc.RelationCheck{ relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Location", Name: "Location",
ID: req.LocationId, ID: req.LocationId,
Exists: relationExistsChecker[entity.Location](s.Repository.DB()), Exists: s.Repository.LocationExists,
}) })
} }
@@ -408,7 +366,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
if len(kandangs) != len(newKandangIDs) { if len(kandangs) != len(newKandangIDs) {
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
} }
if linked, err := s.anyKandangLinkedToOtherProject(c.Context(), s.Repository.DB(), newKandangIDs, &id); err != nil { if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
} else if linked { } else if linked {
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain") return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
@@ -424,6 +382,29 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction) projectRepo := repository.NewProjectflockRepository(dbTransaction)
baseForGeneration := targetBaseName
if strings.TrimSpace(baseForGeneration) == "" {
baseForGeneration = existingBase
}
if strings.TrimSpace(baseForGeneration) == "" {
baseForGeneration = strings.TrimSpace(existing.FlockName)
}
if needFlockNameRegenerate {
nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), baseForGeneration)
if err != nil {
return err
}
newName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, nextSeq, &id)
if err != nil {
return err
}
updateBody["flock_name"] = newName
if seq != existing.Period {
updateBody["period"] = seq
}
}
if len(updateBody) > 0 { if len(updateBody) > 0 {
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return err return err
@@ -512,7 +493,10 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
s.Log.Errorf("Failed to update projectflock %d: %+v", id, err) s.Log.Errorf("Failed to update projectflock %d: %+v", id, err)
return nil, err if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock")
} }
return s.GetOne(c, id) return s.GetOne(c, id)
@@ -616,7 +600,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
} }
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error { func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations) existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
@@ -650,22 +634,70 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
return fiberErr return fiberErr
} }
s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err) s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err)
return err return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete project flock")
} }
return nil return nil
} }
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) { func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) {
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID) pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
} }
return nil, err s.Log.Errorf("Failed to fetch project_flock_kandang by project %d and kandang %d: %+v", projectFlockID, kandangID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
} }
return pfk, nil
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
if err != nil {
return nil, 0, err
}
return pfk, availableQuantity, nil
}
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
idStr = strings.TrimSpace(idStr)
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
kandangIdStr = strings.TrimSpace(kandangIdStr)
if idStr != "" {
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", id, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
if err != nil {
return nil, 0, err
}
return pfk, availableQuantity, nil
}
if projectFlockIdStr == "" || kandangIdStr == "" {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
}
pfid, err := strconv.Atoi(projectFlockIdStr)
if err != nil || pfid <= 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
kid, err := strconv.Atoi(kandangIdStr)
if err != nil || kid <= 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
} }
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) { func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
@@ -675,14 +707,7 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
return 0, err return 0, err
} }
var productWarehouses []entity.ProductWarehouse productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "DOC", wh.Id)
err = s.ProductWarehouseRepo.DB().
WithContext(ctx.Context()).
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 = ?", "DOC", wh.Id).
Order("created_at DESC").
Find(&productWarehouses).Error
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -706,7 +731,7 @@ func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
} }
maxPeriod, err := s.Repository.GetMaxPeriodByFlock(c.Context(), flockID) maxPeriod, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), flock.Name)
if err != nil { if err != nil {
s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err) s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period")
@@ -731,45 +756,64 @@ func uniqueUintSlice(values []uint) []uint {
return result return result
} }
func relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool, error) { func (s projectflockService) generateSequentialFlockName(ctx context.Context, repo repository.ProjectflockRepository, baseName string, startNumber int, excludeID *uint) (string, int, error) {
return func(ctx context.Context, id uint) (bool, error) { name := strings.TrimSpace(baseName)
return commonRepo.Exists[T](ctx, db, id) if name == "" {
return "", 0, fiber.NewError(fiber.StatusBadRequest, "Base flock name cannot be empty")
}
number := startNumber
if number <= 0 {
number = 1
}
attempts := 0
for {
candidate := fmt.Sprintf("%s %03d", name, number)
exists, err := repo.ExistsByFlockName(ctx, candidate, excludeID)
if err != nil {
s.Log.Errorf("Failed checking project flock name uniqueness for %q: %+v", candidate, err)
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate flock name")
}
if !exists {
return candidate, number, nil
}
number++
attempts++
if attempts > 9999 {
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Unable to generate unique flock name")
}
} }
} }
func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []string { func (s projectflockService) ensureFlockByName(ctx context.Context, name string) (*entity.Flock, error) {
direction := "ASC" trimmed := strings.TrimSpace(name)
if strings.ToLower(sortOrder) == "desc" { if trimmed == "" {
direction = "DESC" return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
} }
switch sortBy { flock, err := s.FlockRepo.GetByName(ctx, trimmed)
case "area": if err == nil {
return []string{ return flock, nil
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "location":
return []string{
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "kandangs":
return []string{
fmt.Sprintf("(SELECT COUNT(*) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "period":
return []string{
fmt.Sprintf("project_flocks.period %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
default:
return []string{
"project_flocks.created_at DESC",
"project_flocks.updated_at DESC",
}
} }
if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch flock by name %q: %+v", trimmed, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
}
newFlock := &entity.Flock{
Name: trimmed,
CreatedBy: 1, // TODO: replace with authenticated user
}
if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return s.FlockRepo.GetByName(ctx, trimmed)
}
s.Log.Errorf("Failed to create flock %q: %+v", trimmed, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
}
return newFlock, nil
} }
func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error { func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error {
@@ -777,20 +821,12 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
return nil return nil
} }
if err := dbTransaction. if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusPengajuan); err != nil {
Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs).
Updates(map[string]any{
"status": string(utils.KandangStatusPengajuan),
}).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
} }
var already []uint already, err := s.pivotRepoWithTx(dbTransaction).ListExistingKandangIDs(ctx, projectFlockID, kandangIDs)
if err := dbTransaction. if err != nil {
Table("project_flock_kandangs").
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
Pluck("kandang_id", &already).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot") return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot")
} }
exists := make(map[uint]struct{}, len(already)) exists := make(map[uint]struct{}, len(already))
@@ -799,7 +835,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
} }
var toAttach []uint var toAttach []uint
seen := make(map[uint]struct{}, len(kandangIDs)) seen := make(map[uint]struct{}, len(kandangIDs))
for _, id := range kandangIDs { for _, id := range kandangIDs {
if _, ok := seen[id]; ok { if _, ok := seen[id]; ok {
continue continue
@@ -821,6 +857,9 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
}) })
} }
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil { if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terhubung dengan project flock ini")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
} }
return nil return nil
@@ -831,13 +870,25 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return nil return nil
} }
blocked, err := s.pivotRepoWithTx(dbTransaction).FindKandangsWithRecordings(ctx, projectFlockID, kandangIDs)
if err != nil {
s.Log.Errorf("Failed to check recordings before detaching kandangs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandang detachment")
}
if len(blocked) > 0 {
names := make([]string, 0, len(blocked))
for _, item := range blocked {
label := fmt.Sprintf("ID %d", item.Id)
if strings.TrimSpace(item.Name) != "" {
label = fmt.Sprintf("%s (%s)", label, item.Name)
}
names = append(names, label)
}
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
}
if resetStatus { if resetStatus {
if err := dbTransaction. if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs).
Updates(map[string]any{
"status": string(utils.KandangStatusNonActive),
}).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
} }
} }
@@ -849,23 +900,25 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
} }
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository { func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
if s.PivotRepo == nil { if dbTransaction == nil {
return repository.NewProjectFlockKandangRepository(dbTransaction) return s.pivotRepo()
} }
return s.PivotRepo.WithTx(dbTransaction) return s.pivotRepo().WithTx(dbTransaction)
} }
func (s projectflockService) anyKandangLinkedToOtherProject(ctx context.Context, db *gorm.DB, kandangIDs []uint, exceptProjectID *uint) (bool, error) { func (s projectflockService) pivotRepo() repository.ProjectFlockKandangRepository {
q := db.WithContext(ctx). if s.PivotRepo != nil {
Table("project_flock_kandangs"). return s.PivotRepo
Where("kandang_id IN ?", kandangIDs)
if exceptProjectID != nil {
q = q.Where("project_flock_id <> ?", *exceptProjectID)
} }
var count int64 return repository.NewProjectFlockKandangRepository(s.Repository.DB())
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
} }
func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.KandangRepository {
if tx != nil {
return kandangRepository.NewKandangRepository(tx)
}
if s.KandangRepo != nil {
return s.KandangRepo
}
return kandangRepository.NewKandangRepository(s.Repository.DB())
}
@@ -0,0 +1,25 @@
package utils
import (
"strconv"
"strings"
)
// DeriveBaseName removes trailing numeric tokens from the flock name.
func DeriveBaseName(name string) string {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return ""
}
parts := strings.Fields(trimmed)
for len(parts) > 0 {
if _, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
parts = parts[:len(parts)-1]
continue
}
break
}
return strings.TrimSpace(strings.Join(parts, " "))
}
@@ -1,7 +1,7 @@
package validation package validation
type Create struct { type Create struct {
FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` FlockName string `json:"flock_name" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"` Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
@@ -10,7 +10,7 @@ type Create struct {
} }
type Update struct { type Update struct {
FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` FlockName *string `json:"flock_name,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
Category *string `json:"category,omitempty" validate:"omitempty"` Category *string `json:"category,omitempty" validate:"omitempty"`
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"` FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
@@ -146,6 +146,60 @@ func (u *RecordingController) UpdateOne(c *fiber.Ctx) error {
}) })
} }
func (u *RecordingController) SubmitGrading(c *fiber.Ctx) error {
req := new(validation.SubmitGrading)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.RecordingService.SubmitGrading(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Submit grading eggs successfully",
Data: dto.ToRecordingDetailDTO(*result),
})
}
func (u *RecordingController) Approve(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.RecordingService.Approval(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit recording approvals successfully"
)
if len(results) == 1 {
message = "Submit recording approval successfully"
data = dto.ToRecordingDetailDTO(results[0])
} else {
data = dto.ToRecordingListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
})
}
func (u *RecordingController) DeleteOne(c *fiber.Ctx) error { func (u *RecordingController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
@@ -1,30 +1,34 @@
package dto package dto
import ( import (
"math"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" 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" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
) )
// === DTO Structs === // === DTO Structs ===
type RecordingBaseDTO struct { type RecordingBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
RecordDatetime time.Time `json:"record_datetime"` RecordDatetime time.Time `json:"record_datetime"`
RecordDate *time.Time `json:"record_date,omitempty"` Day *int `json:"day,omitempty"`
Ontime bool `json:"ontime"` ProjectFlockCategory *string `json:"project_flock_category,omitempty"`
Day *int `json:"day,omitempty"` TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"`
TotalDepletion *int `json:"total_depletion,omitempty"` CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"`
CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"` DailyGain *float64 `json:"daily_gain,omitempty"`
DailyGain *float64 `json:"daily_gain,omitempty"` AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"`
AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"` CumIntake *int `json:"cum_intake,omitempty"`
CumIntake *int64 `json:"cum_intake,omitempty"` FcrValue *float64 `json:"fcr_value,omitempty"`
FcrValue *float64 `json:"fcr_value,omitempty"` TotalChickQty *float64 `json:"total_chick_qty,omitempty"`
TotalChick *int64 `json:"total_chick,omitempty"` Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
DailyDepletionRate *float64 `json:"daily_depletion_rate,omitempty"` EggGradingStatus *string `json:"egg_grading_status,omitempty"`
CumDepletion *int `json:"cum_depletion,omitempty"` EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"`
} }
type RecordingListDTO struct { type RecordingListDTO struct {
@@ -39,30 +43,35 @@ type RecordingDetailDTO struct {
BodyWeights []RecordingBodyWeightDTO `json:"body_weights"` BodyWeights []RecordingBodyWeightDTO `json:"body_weights"`
Depletions []RecordingDepletionDTO `json:"depletions"` Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"` Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"`
} }
type RecordingBodyWeightDTO struct { type RecordingBodyWeightDTO struct {
Weight float64 `json:"weight"` AvgWeight float64 `json:"avg_weight"`
Qty int `json:"qty"` Qty float64 `json:"qty"`
Notes *string `json:"notes,omitempty"` TotalWeight float64 `json:"total_weight"`
} }
type RecordingDepletionDTO struct { type RecordingDepletionDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"` ProductWarehouseId uint `json:"product_warehouse_id"`
Total int64 `json:"total"` Qty float64 `json:"qty"`
Notes *string `json:"notes,omitempty"`
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"` ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
} }
type RecordingStockDTO struct { type RecordingStockDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"` ProductWarehouseId uint `json:"product_warehouse_id"`
Increase *float64 `json:"increase,omitempty"` UsageAmount *float64 `json:"usage_amount,omitempty"`
Decrease *float64 `json:"decrease,omitempty"` PendingQty *float64 `json:"pending_qty,omitempty"`
UsageAmount *int64 `json:"usage_amount,omitempty"`
Notes *string `json:"notes,omitempty"`
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"` ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
} }
type RecordingEggDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"`
Qty int `json:"qty"`
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"`
}
type RecordingProductWarehouseDTO struct { type RecordingProductWarehouseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
ProductId uint `json:"product_id"` ProductId uint `json:"product_id"`
@@ -71,36 +80,46 @@ type RecordingProductWarehouseDTO struct {
WarehouseName string `json:"warehouse_name"` WarehouseName string `json:"warehouse_name"`
} }
type RecordingEggGradingDTO struct {
Grade string `json:"grade,omitempty"`
Qty float64 `json:"qty"`
}
// === Mapper Functions === // === Mapper Functions ===
func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
recordDate := e.RecordDate var projectFlockCategory *string
if recordDate == nil { if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
rd := time.Date( category := e.ProjectFlockKandang.ProjectFlock.Category
e.RecordDatetime.Year(), if category != "" {
e.RecordDatetime.Month(), projectFlockCategory = &category
e.RecordDatetime.Day(), }
0, 0, 0, 0,
e.RecordDatetime.Location(),
)
recordDate = &rd
} }
latestApproval := defaultRecordingLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = snapshot
}
gradingStatus, gradingPending := computeEggGradingStatus(e)
return RecordingBaseDTO{ return RecordingBaseDTO{
Id: e.Id, Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId, ProjectFlockKandangId: e.ProjectFlockKandangId,
RecordDatetime: e.RecordDatetime, RecordDatetime: e.RecordDatetime,
RecordDate: recordDate,
Ontime: e.Ontime == 1,
Day: e.Day, Day: e.Day,
TotalDepletion: e.TotalDepletion, ProjectFlockCategory: projectFlockCategory,
TotalDepletionQty: e.TotalDepletionQty,
CumDepletionRate: e.CumDepletionRate, CumDepletionRate: e.CumDepletionRate,
DailyGain: e.DailyGain, DailyGain: e.DailyGain,
AvgDailyGain: e.AvgDailyGain, AvgDailyGain: e.AvgDailyGain,
CumIntake: e.CumIntake, CumIntake: e.CumIntake,
FcrValue: e.FcrValue, FcrValue: e.FcrValue,
TotalChick: e.TotalChick, TotalChickQty: e.TotalChickQty,
DailyDepletionRate: e.DailyDepletionRate, Approval: latestApproval,
CumDepletion: e.CumDepletion, EggGradingStatus: gradingStatus,
EggGradingPendingQty: gradingPending,
} }
} }
@@ -133,6 +152,7 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights), BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
Depletions: ToRecordingDepletionDTOs(e.Depletions), Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks), Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: ToRecordingEggDTOs(e.Eggs),
} }
} }
@@ -140,9 +160,9 @@ func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBody
result := make([]RecordingBodyWeightDTO, len(bodyWeights)) result := make([]RecordingBodyWeightDTO, len(bodyWeights))
for i, bw := range bodyWeights { for i, bw := range bodyWeights {
result[i] = RecordingBodyWeightDTO{ result[i] = RecordingBodyWeightDTO{
Weight: bw.Weight, AvgWeight: bw.AvgWeight,
Qty: bw.Qty, Qty: bw.Qty,
Notes: bw.Notes, TotalWeight: bw.TotalWeight,
} }
} }
return result return result
@@ -153,8 +173,7 @@ func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []Recordin
for i, d := range depletions { for i, d := range depletions {
result[i] = RecordingDepletionDTO{ result[i] = RecordingDepletionDTO{
ProductWarehouseId: d.ProductWarehouseId, ProductWarehouseId: d.ProductWarehouseId,
Total: d.Total, Qty: d.Qty,
Notes: d.Notes,
ProductWarehouse: toRecordingProductWarehouseDTO(&d.ProductWarehouse), ProductWarehouse: toRecordingProductWarehouseDTO(&d.ProductWarehouse),
} }
} }
@@ -166,16 +185,43 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
for i, s := range stocks { for i, s := range stocks {
result[i] = RecordingStockDTO{ result[i] = RecordingStockDTO{
ProductWarehouseId: s.ProductWarehouseId, ProductWarehouseId: s.ProductWarehouseId,
Increase: s.Increase, UsageAmount: s.UsageQty,
Decrease: s.Decrease, PendingQty: s.PendingQty,
UsageAmount: s.UsageAmount,
Notes: s.Notes,
ProductWarehouse: toRecordingProductWarehouseDTO(&s.ProductWarehouse), ProductWarehouse: toRecordingProductWarehouseDTO(&s.ProductWarehouse),
} }
} }
return result return result
} }
func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
result := make([]RecordingEggDTO, len(eggs))
for i, egg := range eggs {
result[i] = RecordingEggDTO{
ProductWarehouseId: egg.ProductWarehouseId,
Qty: egg.Qty,
ProductWarehouse: toRecordingProductWarehouseDTO(&egg.ProductWarehouse),
Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs),
}
}
return result
}
func ToRecordingEggGradingDTOs(gradings []entity.GradingEgg) []RecordingEggGradingDTO {
if len(gradings) == 0 {
return nil
}
result := make([]RecordingEggGradingDTO, len(gradings))
for i, grading := range gradings {
result[i] = RecordingEggGradingDTO{
Grade: grading.Grade,
Qty: grading.Qty,
}
}
return result
}
func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProductWarehouseDTO { func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProductWarehouseDTO {
if pw == nil || pw.Id == 0 { if pw == nil || pw.Id == 0 {
return nil return nil
@@ -196,3 +242,57 @@ func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProdu
return &dto return &dto
} }
func computeEggGradingStatus(e entity.Recording) (*string, *int) {
if len(e.Eggs) == 0 {
return nil, nil
}
totalEggs := 0
totalGraded := 0.0
for _, egg := range e.Eggs {
totalEggs += egg.Qty
for _, grading := range egg.GradingEggs {
totalGraded += grading.Qty
}
}
if totalEggs == 0 {
return nil, nil
}
pending := float64(totalEggs) - totalGraded
if pending > 0.5 {
status := "GRADING_TELUR"
pendingInt := int(math.Round(pending))
return &status, &pendingInt
}
status := "GRADING_SELESAI"
zero := 0
return &status, &zero
}
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalBaseDTO {
result := approvalDTO.ApprovalBaseDTO{}
step := utils.RecordingStepPengajuan
result.StepNumber = uint16(step)
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowRecording, step); ok {
result.StepName = label
} else if label, ok := utils.RecordingApprovalSteps[step]; ok {
result.StepName = label
}
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
result.ActionBy = userDTO.ToUserBaseDTO(*e.CreatedUser)
} else if e.CreatedBy != 0 {
result.ActionBy = userDTO.UserBaseDTO{
Id: e.CreatedBy,
IdUser: int64(e.CreatedBy),
}
}
return result
}
@@ -1,14 +1,19 @@
package recordings package recordings
import ( import (
"fmt"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "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" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -18,11 +23,26 @@ type RecordingModule struct{}
func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
recordingRepo := rRecording.NewRecordingRepository(db) recordingRepo := rRecording.NewRecordingRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowRecording, utils.RecordingApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register recording approval workflow: %v", err))
}
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
recordingService := sRecording.NewRecordingService(recordingRepo, projectFlockKandangRepo, productWarehouseRepo, validate) recordingService := sRecording.NewRecordingService(
recordingRepo,
projectFlockKandangRepo,
productWarehouseRepo,
projectFlockPopulationRepo,
approvalRepo,
approvalService,
validate,
)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
RecordingRoutes(router, userService, recordingService) RecordingRoutes(router, userService, recordingService)
@@ -1,10 +1,12 @@
package repository package repository
import ( import (
"context"
"errors" "errors"
"math" "math"
"sort" "sort"
"strings" "strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -22,11 +24,22 @@ type RecordingRepository interface {
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
DeleteStocks(tx *gorm.DB, recordingID uint) error DeleteStocks(tx *gorm.DB, recordingID uint) error
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
DeleteDepletions(tx *gorm.DB, recordingID uint) error DeleteDepletions(tx *gorm.DB, recordingID uint) error
ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error)
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error) CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error
DeleteEggs(tx *gorm.DB, recordingID uint) error
ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error)
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error
DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error)
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error)
@@ -58,13 +71,18 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
Preload("Stocks"). Preload("Stocks").
Preload("Stocks.ProductWarehouse"). Preload("Stocks.ProductWarehouse").
Preload("Stocks.ProductWarehouse.Product"). Preload("Stocks.ProductWarehouse.Product").
Preload("Stocks.ProductWarehouse.Warehouse") Preload("Stocks.ProductWarehouse.Warehouse").
Preload("Eggs").
Preload("Eggs.ProductWarehouse").
Preload("Eggs.ProductWarehouse.Product").
Preload("Eggs.ProductWarehouse.Warehouse").
Preload("Eggs.GradingEggs")
} }
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) { func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
var days []int var days []int
if err := tx.Model(&entity.Recording{}). if err := tx.Model(&entity.Recording{}).
Where("project_flock_id = ?", projectFlockKandangId). Where("project_flock_kandangs_id = ?", projectFlockKandangId).
Where("day IS NOT NULL"). Where("day IS NOT NULL").
Pluck("day", &days).Error; err != nil { Pluck("day", &days).Error; err != nil {
return 0, err return 0, err
@@ -94,6 +112,14 @@ func (r *RecordingRepositoryImpl) DeleteStocks(tx *gorm.DB, recordingID uint) er
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error
} }
func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) {
var items []entity.RecordingStock
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error { func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 { if len(depletions) == 0 {
return nil return nil
@@ -105,11 +131,100 @@ func (r *RecordingRepositoryImpl) DeleteDepletions(tx *gorm.DB, recordingID uint
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingDepletion{}).Error return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingDepletion{}).Error
} }
func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error) { func (r *RecordingRepositoryImpl) ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error) {
var result int64 var items []entity.RecordingDepletion
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *RecordingRepositoryImpl) CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error {
if len(eggs) == 0 {
return nil
}
return tx.Create(&eggs).Error
}
func (r *RecordingRepositoryImpl) DeleteEggs(tx *gorm.DB, recordingID uint) error {
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingEgg{}).Error
}
func (r *RecordingRepositoryImpl) ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error) {
var items []entity.RecordingEgg
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *RecordingRepositoryImpl) GetRecordingEggByID(
ctx context.Context,
id uint,
modifier func(*gorm.DB) *gorm.DB,
) (*entity.RecordingEgg, error) {
if id == 0 {
return nil, gorm.ErrRecordNotFound
}
db := r.DB()
if modifier != nil {
db = modifier(db)
}
var egg entity.RecordingEgg
query := db.WithContext(ctx).
Preload("Recording").
Preload("Recording.ProjectFlockKandang").
Preload("Recording.ProjectFlockKandang.ProjectFlock").
Preload("ProductWarehouse").
Preload("GradingEggs").
Where("id = ?", id)
if err := query.First(&egg).Error; err != nil {
return nil, err
}
return &egg, nil
}
func (r *RecordingRepositoryImpl) CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error {
if len(gradings) == 0 {
return nil
}
return tx.Create(&gradings).Error
}
func (r *RecordingRepositoryImpl) DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error {
return tx.Where("recording_egg_id = ?", recordingEggID).Delete(&entity.GradingEgg{}).Error
}
func (r *RecordingRepositoryImpl) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) {
if projectFlockKandangId == 0 {
return false, nil
}
ref := recordTime.In(time.UTC)
startOfDay := time.Date(ref.Year(), ref.Month(), ref.Day(), 0, 0, 0, 0, time.UTC)
endOfDay := startOfDay.Add(24 * time.Hour)
var count int64
err := r.DB().
WithContext(ctx).
Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
Where("record_datetime >= ? AND record_datetime < ?", startOfDay, endOfDay).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error) {
var result float64
if err := tx.Model(&entity.RecordingDepletion{}). if err := tx.Model(&entity.RecordingDepletion{}).
Where("recording_id = ?", recordingID). Where("recording_id = ?", recordingID).
Select("COALESCE(SUM(total), 0)"). Select("COALESCE(SUM(qty), 0)").
Scan(&result).Error; err != nil { Scan(&result).Error; err != nil {
return 0, err return 0, err
} }
@@ -123,7 +238,7 @@ func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFloc
var prev entity.Recording var prev entity.Recording
err := tx. err := tx.
Where("project_flock_id = ? AND day < ?", projectFlockKandangId, currentDay). Where("project_flock_kandangs_id = ? AND day < ?", projectFlockKandangId, currentDay).
Where("day IS NOT NULL"). Where("day IS NOT NULL").
Order("day DESC"). Order("day DESC").
Limit(1). Limit(1).
@@ -159,7 +274,7 @@ func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID
TotalQty float64 TotalQty float64
} }
if err := tx.Model(&entity.RecordingBW{}). if err := tx.Model(&entity.RecordingBW{}).
Select("COALESCE(SUM(weight * qty), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty"). Select("COALESCE(SUM(total_weight), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
Where("recording_id = ?", recordingID). Where("recording_id = ?", recordingID).
Scan(&result).Error; err != nil { Scan(&result).Error; err != nil {
return 0, err return 0, err
@@ -172,13 +287,13 @@ func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
var rows []struct { var rows []struct {
UsageAmount float64 UsageQty float64
UomName string UomName string
} }
if err := tx. if err := tx.
Table("recording_stocks"). Table("recording_stocks").
Select("COALESCE(recording_stocks.usage_amount, 0) AS usage_amount, LOWER(uoms.name) AS uom_name"). Select("COALESCE(recording_stocks.usage_qty, 0) AS usage_qty, LOWER(uoms.name) AS uom_name").
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id"). Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN uoms ON uoms.id = products.uom_id"). Joins("JOIN uoms ON uoms.id = products.uom_id").
@@ -189,16 +304,16 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u
var total float64 var total float64
for _, row := range rows { for _, row := range rows {
if row.UsageAmount <= 0 { if row.UsageQty <= 0 {
continue continue
} }
switch strings.TrimSpace(row.UomName) { switch strings.TrimSpace(row.UomName) {
case "kilogram", "kg", "kilograms", "kilo": case "kilogram", "kg", "kilograms", "kilo":
total += row.UsageAmount * 1000 total += row.UsageQty * 1000
case "gram", "g", "grams": case "gram", "g", "grams":
total += row.UsageAmount total += row.UsageQty
default: default:
total += row.UsageAmount total += row.UsageQty
} }
} }
return total, nil return total, nil
@@ -23,7 +23,9 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
route.Get("/next-day", ctrl.GetNextDay) route.Get("/next-day", ctrl.GetNextDay)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
route.Post("/gradings", ctrl.SubmitGrading)
route.Get("/:id", ctrl.GetOne) route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne) route.Patch("/:id", ctrl.UpdateOne)
route.Post("/approvals", ctrl.Approve)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
} }
File diff suppressed because it is too large Load Diff
@@ -2,23 +2,25 @@ package validation
type ( type (
BodyWeight struct { BodyWeight struct {
Weight float64 `json:"weight" validate:"required"` AvgWeight float64 `json:"avg_weight" validate:"required"`
Qty int `json:"qty" validate:"required,number,min=1"` Qty float64 `json:"qty" validate:"required,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty"` TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gt=0"`
} }
Stock struct { Stock struct {
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
Increase *float64 `json:"increase,omitempty" validate:"omitempty"` UsageAmount *float64 `json:"usage_amount,omitempty" validate:"omitempty,gte=0"`
Decrease *float64 `json:"decrease,omitempty" validate:"omitempty"` PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
UsageAmount *int64 `json:"usage_amount,omitempty" validate:"omitempty,min=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty"`
} }
Depletion struct { Depletion struct {
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
Total int64 `json:"total" validate:"required,number,min=0"` Qty float64 `json:"qty" validate:"required,gte=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty"` }
Egg struct {
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
Qty int `json:"qty" validate:"required,number,min=0"`
} }
) )
@@ -27,12 +29,14 @@ type Create struct {
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"` BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
} }
type Update struct { type Update struct {
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"` BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
} }
type Query struct { type Query struct {
@@ -40,3 +44,19 @@ type Query struct {
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
} }
type EggGrading struct {
Grade string `json:"grade" validate:"required"`
Qty float64 `json:"qty" validate:"required,gte=0"`
}
type SubmitGrading struct {
RecordingEggId uint `json:"recording_egg_id" validate:"required,number,min=1"`
EggsGrading []EggGrading `json:"eggs_grading" validate:"required,dive"`
}
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"`
}
@@ -13,6 +13,7 @@ type StockLogRepository interface {
GetByFlaggable(ctx context.Context, logType string, logId uint) ([]*entity.StockLog, error) GetByFlaggable(ctx context.Context, logType string, logId uint) ([]*entity.StockLog, error)
GetByProductWarehouse(ctx context.Context, productWarehouseId uint, limit int) ([]*entity.StockLog, error) GetByProductWarehouse(ctx context.Context, productWarehouseId uint, limit int) ([]*entity.StockLog, error)
GetByTransactionType(ctx context.Context, transactionType string, limit int) ([]*entity.StockLog, error) GetByTransactionType(ctx context.Context, transactionType string, limit int) ([]*entity.StockLog, error)
ApplyProductWarehouseFilters(db *gorm.DB, productID, warehouseID uint) *gorm.DB
} }
type StockLogRepositoryImpl struct { type StockLogRepositoryImpl struct {
@@ -86,3 +87,20 @@ func (r *StockLogRepositoryImpl) GetByTransactionType(ctx context.Context, trans
return stockLogs, nil return stockLogs, nil
} }
func (r *StockLogRepositoryImpl) ApplyProductWarehouseFilters(db *gorm.DB, productID, warehouseID uint) *gorm.DB {
if productID == 0 && warehouseID == 0 {
return db
}
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id")
if productID > 0 {
db = db.Where("product_warehouses.product_id = ?", productID)
}
if warehouseID > 0 {
db = db.Where("product_warehouses.warehouse_id = ?", warehouseID)
}
return db
}
+19
View File
@@ -140,6 +140,23 @@ var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{
ProjectFlockStepAktif: "Aktif", ProjectFlockStepAktif: "Aktif",
} }
// -------------------------------------------------------------------
// Recording Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowRecording approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("RECORDINGS")
RecordingStepGradingTelur approvalutils.ApprovalStep = 1
RecordingStepPengajuan approvalutils.ApprovalStep = 2
RecordingStepDisetujui approvalutils.ApprovalStep = 3
)
var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
RecordingStepGradingTelur: "Grading-Telur",
RecordingStepPengajuan: "Pengajuan",
RecordingStepDisetujui: "Disetujui",
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Validators // Validators
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -268,6 +285,8 @@ func IsValidSupplierCategory(v string) bool {
// example use // example use
// Recording helper
/** /**
if !utils.IsValidFlagType(req.FlagName) { if !utils.IsValidFlagType(req.FlagName) {
return fiber.NewError(fiber.StatusBadRequest, "Invalid flag type") return fiber.NewError(fiber.StatusBadRequest, "Invalid flag type")
@@ -0,0 +1,96 @@
package recording
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
)
func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.RecordingBW {
if len(items) == 0 {
return nil
}
result := make([]entity.RecordingBW, 0, len(items))
for _, item := range items {
totalWeight := item.TotalWeight
if totalWeight == nil {
calculated := item.AvgWeight * item.Qty
totalWeight = &calculated
}
result = append(result, entity.RecordingBW{
RecordingId: recordingID,
AvgWeight: item.AvgWeight,
Qty: item.Qty,
TotalWeight: *totalWeight,
})
}
return result
}
func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingStock {
if len(items) == 0 {
return nil
}
result := make([]entity.RecordingStock, 0, len(items))
for _, item := range items {
result = append(result, entity.RecordingStock{
RecordingId: recordingID,
ProductWarehouseId: item.ProductWarehouseId,
UsageQty: item.UsageAmount,
PendingQty: item.PendingQty,
})
}
return result
}
func MapDepletions(recordingID uint, items []validation.Depletion) []entity.RecordingDepletion {
if len(items) == 0 {
return nil
}
result := make([]entity.RecordingDepletion, 0, len(items))
for _, item := range items {
result = append(result, entity.RecordingDepletion{
RecordingId: recordingID,
ProductWarehouseId: item.ProductWarehouseId,
Qty: item.Qty,
})
}
return result
}
func MapEggs(recordingID uint, createdBy uint, items []validation.Egg) []entity.RecordingEgg {
if len(items) == 0 {
return nil
}
result := make([]entity.RecordingEgg, 0, len(items))
for _, item := range items {
result = append(result, entity.RecordingEgg{
RecordingId: recordingID,
ProductWarehouseId: item.ProductWarehouseId,
Qty: item.Qty,
CreatedBy: createdBy,
})
}
return result
}
func ToGrams(weight float64) float64 {
if weight <= 0 {
return 0
}
if weight < 10 {
return weight * 1000
}
return weight
}
func GramsToKg(grams float64) float64 {
if grams <= 0 {
return 0
}
return grams / 1000
}
+2 -1
View File
@@ -2,6 +2,7 @@ package test
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"testing" "testing"
@@ -58,7 +59,7 @@ func TestKandangIntegration(t *testing.T) {
flocID := createFlock(t, app, "Floc Test") flocID := createFlock(t, app, "Floc Test")
projectFloc := entities.ProjectFlock{ projectFloc := entities.ProjectFlock{
FlockId: flocID, FlockName: fmt.Sprintf("Project Flock %d", flocID),
AreaId: areaID, AreaId: areaID,
Category: string(utils.ProjectFlockCategoryGrowing), Category: string(utils.ProjectFlockCategoryGrowing),
FcrId: fcrID, FcrId: fcrID,
+371 -371
View File
@@ -1,417 +1,417 @@
package test package test
import ( // import (
"encoding/json" // "encoding/json"
"fmt" // "fmt"
"net/http" // "net/http"
"net/url" // "net/url"
"testing" // "testing"
"github.com/gofiber/fiber/v2" // "github.com/gofiber/fiber/v2"
"gitlab.com/mbugroup/lti-api.git/internal/entities" // "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils" // "gitlab.com/mbugroup/lti-api.git/internal/utils"
) // )
func TestProjectFlockSummary(t *testing.T) { // func TestProjectFlockSummary(t *testing.T) {
app, db := setupIntegrationApp(t) // app, db := setupIntegrationApp(t)
areaID := createArea(t, app, "Area Project") // areaID := createArea(t, app, "Area Project")
locationID := createLocation(t, app, "Location Project", "Address", areaID) // locationID := createLocation(t, app, "Location Project", "Address", areaID)
flockID := createFlock(t, app, "Flock Summary") // flockID := createFlock(t, app, "Flock Summary")
fcrID := createFcr(t, app, "FCR Summary", []map[string]any{ // fcrID := createFcr(t, app, "FCR Summary", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, // {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) // })
kandangID := createKandang(t, app, "Kandang Summary", locationID, 1) // kandangID := createKandang(t, app, "Kandang Summary", locationID, 1)
createPayload := map[string]any{ // createPayload := map[string]any{
"flock_id": flockID, // "flock_id": flockID,
"area_id": areaID, // "area_id": areaID,
"category": "growing", // "category": "growing",
"fcr_id": fcrID, // "fcr_id": fcrID,
"location_id": locationID, // "location_id": locationID,
"kandang_ids": []uint{kandangID}, // "kandang_ids": []uint{kandangID},
} // }
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload) // resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
if resp.StatusCode != fiber.StatusCreated { // if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body))
} // }
var createResp struct { // var createResp struct {
Data struct { // Data struct {
Id uint `json:"id"` // Id uint `json:"id"`
Period int `json:"period"` // Period int `json:"period"`
Category string `json:"category"` // Category string `json:"category"`
Flock struct { // Flock struct {
Id uint `json:"id"` // Id uint `json:"id"`
Name string `json:"name"` // Name string `json:"name"`
} `json:"flock"` // } `json:"flock"`
Area struct { // Area struct {
Id uint `json:"id"` // Id uint `json:"id"`
Name string `json:"name"` // Name string `json:"name"`
} `json:"area"` // } `json:"area"`
Fcr struct { // Fcr struct {
Id uint `json:"id"` // Id uint `json:"id"`
Name string `json:"name"` // Name string `json:"name"`
} `json:"fcr"` // } `json:"fcr"`
Location struct { // Location struct {
Id uint `json:"id"` // Id uint `json:"id"`
Name string `json:"name"` // Name string `json:"name"`
Address string `json:"address"` // Address string `json:"address"`
} `json:"location"` // } `json:"location"`
Kandangs []struct { // Kandangs []struct {
Id uint `json:"id"` // Id uint `json:"id"`
Name string `json:"name"` // Name string `json:"name"`
Status string `json:"status"` // Status string `json:"status"`
} `json:"kandangs"` // } `json:"kandangs"`
CreatedUser struct { // CreatedUser struct {
Id uint `json:"id"` // Id uint `json:"id"`
IdUser uint `json:"id_user"` // IdUser uint `json:"id_user"`
Email string `json:"email"` // Email string `json:"email"`
Name string `json:"name"` // Name string `json:"name"`
} `json:"created_user"` // } `json:"created_user"`
} `json:"data"` // } `json:"data"`
} // }
if err := json.Unmarshal(body, &createResp); err != nil { // if err := json.Unmarshal(body, &createResp); err != nil {
t.Fatalf("failed to parse create response: %v", err) // t.Fatalf("failed to parse create response: %v", err)
} // }
if createResp.Data.Flock.Id != flockID || createResp.Data.Flock.Name == "" { // if createResp.Data.Flock.Id != flockID || createResp.Data.Flock.Name == "" {
t.Fatalf("expected flock detail to be present, got %+v", createResp.Data.Flock) // t.Fatalf("expected flock detail to be present, got %+v", createResp.Data.Flock)
} // }
if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" { // if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" {
t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area) // t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area)
} // }
if createResp.Data.Category != string(utils.ProjectFlockCategoryGrowing) { // if createResp.Data.Category != string(utils.ProjectFlockCategoryGrowing) {
t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryGrowing, createResp.Data.Category) // t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryGrowing, createResp.Data.Category)
} // }
if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" { // if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" {
t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location) // t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location)
} // }
if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID { // if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID {
t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs) // t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs)
} // }
if createResp.Data.Kandangs[0].Status != string(utils.KandangStatusPengajuan) { // if createResp.Data.Kandangs[0].Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status) // t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status)
} // }
if createResp.Data.Period != 1 { // if createResp.Data.Period != 1 {
t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period) // t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period)
} // }
createdKandang := fetchKandang(t, db, kandangID) // createdKandang := fetchKandang(t, db, kandangID)
if createdKandang.Status != string(utils.KandangStatusPengajuan) { // if createdKandang.Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected kandang status in DB to be PENGAJUAN, got %s", createdKandang.Status) // t.Fatalf("expected kandang status in DB to be PENGAJUAN, got %s", createdKandang.Status)
} // }
var pivotRecords []entities.ProjectFlockKandang // var pivotRecords []entities.ProjectFlockKandang
if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil { // if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil {
t.Fatalf("failed to fetch pivot records: %v", err) // t.Fatalf("failed to fetch pivot records: %v", err)
} // }
if len(pivotRecords) != 1 { // if len(pivotRecords) != 1 {
t.Fatalf("expected 1 pivot record, got %d", len(pivotRecords)) // t.Fatalf("expected 1 pivot record, got %d", len(pivotRecords))
} // }
firstPivotRecord := pivotRecords[0] // firstPivotRecord := pivotRecords[0]
if firstPivotRecord.KandangId != kandangID { // if firstPivotRecord.KandangId != kandangID {
t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId) // t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId)
} // }
secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1) // secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1)
secondPayload := map[string]any{ // secondPayload := map[string]any{
"flock_id": flockID, // "flock_id": flockID,
"area_id": areaID, // "area_id": areaID,
"category": "laying", // "category": "laying",
"fcr_id": fcrID, // "fcr_id": fcrID,
"location_id": locationID, // "location_id": locationID,
"kandang_ids": []uint{secondKandangID}, // "kandang_ids": []uint{secondKandangID},
} // }
resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", secondPayload) // resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", secondPayload)
if resp.StatusCode != fiber.StatusCreated { // if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating second project flock, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 201 when creating second project flock, got %d: %s", resp.StatusCode, string(body))
} // }
var createRespSecond struct { // var createRespSecond struct {
Data struct { // Data struct {
Id uint `json:"id"` // Id uint `json:"id"`
Period int `json:"period"` // Period int `json:"period"`
Category string `json:"category"` // Category string `json:"category"`
} `json:"data"` // } `json:"data"`
} // }
if err := json.Unmarshal(body, &createRespSecond); err != nil { // if err := json.Unmarshal(body, &createRespSecond); err != nil {
t.Fatalf("failed to parse second create response: %v", err) // t.Fatalf("failed to parse second create response: %v", err)
} // }
if createRespSecond.Data.Period != 2 { // if createRespSecond.Data.Period != 2 {
t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period) // t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period)
} // }
if createRespSecond.Data.Category != string(utils.ProjectFlockCategoryLaying) { // if createRespSecond.Data.Category != string(utils.ProjectFlockCategoryLaying) {
t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryLaying, createRespSecond.Data.Category) // t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryLaying, createRespSecond.Data.Category)
} // }
pivotRecords = nil // pivotRecords = nil
if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil { // if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil {
t.Fatalf("failed to fetch second pivot records: %v", err) // t.Fatalf("failed to fetch second pivot records: %v", err)
} // }
if len(pivotRecords) != 1 { // if len(pivotRecords) != 1 {
t.Fatalf("expected 1 pivot record for second project, got %d", len(pivotRecords)) // t.Fatalf("expected 1 pivot record for second project, got %d", len(pivotRecords))
} // }
secondPivotRecord := pivotRecords[0] // secondPivotRecord := pivotRecords[0]
if secondPivotRecord.KandangId != secondKandangID { // if secondPivotRecord.KandangId != secondKandangID {
t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId) // t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId)
} // }
secondKandang := fetchKandang(t, db, secondKandangID) // secondKandang := fetchKandang(t, db, secondKandangID)
if secondKandang.Status != string(utils.KandangStatusPengajuan) { // if secondKandang.Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected second kandang status in DB to be PENGAJUAN, got %s", secondKandang.Status) // t.Fatalf("expected second kandang status in DB to be PENGAJUAN, got %s", secondKandang.Status)
} // }
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) // resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
if resp.StatusCode != fiber.StatusOK { // if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching summary, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 200 when fetching summary, got %d: %s", resp.StatusCode, string(body))
} // }
var summary struct { // var summary struct {
Data struct { // Data struct {
NextPeriod int `json:"next_period"` // NextPeriod int `json:"next_period"`
} `json:"data"` // } `json:"data"`
} // }
if err := json.Unmarshal(body, &summary); err != nil { // if err := json.Unmarshal(body, &summary); err != nil {
t.Fatalf("failed to parse summary response: %v", err) // t.Fatalf("failed to parse summary response: %v", err)
} // }
if summary.Data.NextPeriod != 3 { // if summary.Data.NextPeriod != 3 {
t.Fatalf("expected next_period 3, got %d", summary.Data.NextPeriod) // t.Fatalf("expected next_period 3, got %d", summary.Data.NextPeriod)
} // }
resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createResp.Data.Id), nil) // resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createResp.Data.Id), nil)
if resp.StatusCode != fiber.StatusOK { // if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body))
} // }
firstKandang := fetchKandang(t, db, kandangID) // firstKandang := fetchKandang(t, db, kandangID)
if firstKandang.ProjectFlockId != nil { // if firstKandang.ProjectFlockId != nil {
t.Fatalf("expected project_flock_id to be nil after delete, got %v", *firstKandang.ProjectFlockId) // t.Fatalf("expected project_flock_id to be nil after delete, got %v", *firstKandang.ProjectFlockId)
} // }
if firstKandang.Status != string(utils.KandangStatusNonActive) { // if firstKandang.Status != string(utils.KandangStatusNonActive) {
t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status) // t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status)
} // }
var remainingFirst int64 // var remainingFirst int64
if err := db.Model(&entities.ProjectFlockKandang{}). // if err := db.Model(&entities.ProjectFlockKandang{}).
Where("project_flock_id = ? AND kandang_id = ?", createResp.Data.Id, kandangID). // Where("project_flock_id = ? AND kandang_id = ?", createResp.Data.Id, kandangID).
Count(&remainingFirst).Error; err != nil { // Count(&remainingFirst).Error; err != nil {
t.Fatalf("failed to count first pivot records after delete: %v", err) // t.Fatalf("failed to count first pivot records after delete: %v", err)
} // }
if remainingFirst != 0 { // if remainingFirst != 0 {
t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst) // t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst)
} // }
resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil) // resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil)
if resp.StatusCode != fiber.StatusOK { // if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body))
} // }
secondKandang = fetchKandang(t, db, secondKandangID) // secondKandang = fetchKandang(t, db, secondKandangID)
if secondKandang.ProjectFlockId != nil { // if secondKandang.ProjectFlockId != nil {
t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId) // t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId)
} // }
if secondKandang.Status != string(utils.KandangStatusNonActive) { // if secondKandang.Status != string(utils.KandangStatusNonActive) {
t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status) // t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status)
} // }
var remainingSecond int64 // var remainingSecond int64
if err := db.Model(&entities.ProjectFlockKandang{}). // if err := db.Model(&entities.ProjectFlockKandang{}).
Where("project_flock_id = ? AND kandang_id = ?", createRespSecond.Data.Id, secondKandangID). // Where("project_flock_id = ? AND kandang_id = ?", createRespSecond.Data.Id, secondKandangID).
Count(&remainingSecond).Error; err != nil { // Count(&remainingSecond).Error; err != nil {
t.Fatalf("failed to count second pivot records after delete: %v", err) // t.Fatalf("failed to count second pivot records after delete: %v", err)
} // }
if remainingSecond != 0 { // if remainingSecond != 0 {
t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond) // t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond)
} // }
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) // resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
if resp.StatusCode != fiber.StatusOK { // if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body))
} // }
if err := json.Unmarshal(body, &summary); err != nil { // if err := json.Unmarshal(body, &summary); err != nil {
t.Fatalf("failed to parse summary response after delete: %v", err) // t.Fatalf("failed to parse summary response after delete: %v", err)
} // }
if summary.Data.NextPeriod != 1 { // if summary.Data.NextPeriod != 1 {
t.Fatalf("expected next_period 1 after soft deletes, got %d", summary.Data.NextPeriod) // t.Fatalf("expected next_period 1 after soft deletes, got %d", summary.Data.NextPeriod)
} // }
} // }
func uintToString(v uint) string { // func uintToString(v uint) string {
return fmt.Sprintf("%d", v) // return fmt.Sprintf("%d", v)
} // }
func TestProjectFlockSearchByRelatedFields(t *testing.T) { // func TestProjectFlockSearchByRelatedFields(t *testing.T) {
app, _ := setupIntegrationApp(t) // app, _ := setupIntegrationApp(t)
areaID := createArea(t, app, "Area Search Target") // areaID := createArea(t, app, "Area Search Target")
locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID) // locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID)
flockID := createFlock(t, app, "Flock Search Target") // flockID := createFlock(t, app, "Flock Search Target")
fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{ // fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, // {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) // })
kandangID := createKandang(t, app, "Kandang Search Target", locationID, 1) // kandangID := createKandang(t, app, "Kandang Search Target", locationID, 1)
createPayload := map[string]any{ // createPayload := map[string]any{
"flock_id": flockID, // "flock_id": flockID,
"area_id": areaID, // "area_id": areaID,
"category": "growing", // "category": "growing",
"fcr_id": fcrID, // "fcr_id": fcrID,
"location_id": locationID, // "location_id": locationID,
"kandang_ids": []uint{kandangID}, // "kandang_ids": []uint{kandangID},
} // }
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload) // resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
if resp.StatusCode != fiber.StatusCreated { // if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body))
} // }
var createResp struct { // var createResp struct {
Data struct { // Data struct {
Id uint `json:"id"` // Id uint `json:"id"`
} `json:"data"` // } `json:"data"`
} // }
if err := json.Unmarshal(body, &createResp); err != nil { // if err := json.Unmarshal(body, &createResp); err != nil {
t.Fatalf("failed to parse create response: %v", err) // t.Fatalf("failed to parse create response: %v", err)
} // }
searchTerms := []string{ // searchTerms := []string{
"Flock Search Target", // "Flock Search Target",
"Area Search Target", // "Area Search Target",
string(utils.ProjectFlockCategoryGrowing), // string(utils.ProjectFlockCategoryGrowing),
"growing", // "growing",
"FCR Search Target", // "FCR Search Target",
"Kandang Search Target", // "Kandang Search Target",
"Location Search Target", // "Location Search Target",
"Location Address Target", // "Location Address Target",
"Tester", // "Tester",
"1", // "1",
} // }
for _, term := range searchTerms { // for _, term := range searchTerms {
path := "/api/production/project_flocks?search=" + url.QueryEscape(term) // path := "/api/production/project_flocks?search=" + url.QueryEscape(term)
resp, body := doJSONRequest(t, app, http.MethodGet, path, nil) // resp, body := doJSONRequest(t, app, http.MethodGet, path, nil)
if resp.StatusCode != fiber.StatusOK { // if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when searching for %q, got %d: %s", term, resp.StatusCode, string(body)) // t.Fatalf("expected 200 when searching for %q, got %d: %s", term, resp.StatusCode, string(body))
} // }
var listResp struct { // var listResp struct {
Data []struct { // Data []struct {
Id uint `json:"id"` // Id uint `json:"id"`
} `json:"data"` // } `json:"data"`
Meta struct { // Meta struct {
TotalResults int64 `json:"total_results"` // TotalResults int64 `json:"total_results"`
} `json:"meta"` // } `json:"meta"`
} // }
if err := json.Unmarshal(body, &listResp); err != nil { // if err := json.Unmarshal(body, &listResp); err != nil {
t.Fatalf("failed to parse list response for %q: %v", term, err) // t.Fatalf("failed to parse list response for %q: %v", term, err)
} // }
if listResp.Meta.TotalResults == 0 { // if listResp.Meta.TotalResults == 0 {
t.Fatalf("expected at least one result when searching for %q", term) // t.Fatalf("expected at least one result when searching for %q", term)
} // }
if len(listResp.Data) == 0 { // if len(listResp.Data) == 0 {
t.Fatalf("expected data when searching for %q", term) // t.Fatalf("expected data when searching for %q", term)
} // }
if listResp.Data[0].Id != createResp.Data.Id { // if listResp.Data[0].Id != createResp.Data.Id {
t.Fatalf("expected project flock id %d for search term %q, got %d", createResp.Data.Id, term, listResp.Data[0].Id) // t.Fatalf("expected project flock id %d for search term %q, got %d", createResp.Data.Id, term, listResp.Data[0].Id)
} // }
} // }
} // }
func TestProjectFlockSorting(t *testing.T) { // func TestProjectFlockSorting(t *testing.T) {
app, _ := setupIntegrationApp(t) // app, _ := setupIntegrationApp(t)
areaA := createArea(t, app, "Area Alpha") // areaA := createArea(t, app, "Area Alpha")
areaB := createArea(t, app, "Area Beta") // areaB := createArea(t, app, "Area Beta")
locationA := createLocation(t, app, "Location Alpha", "Address Alpha", areaA) // locationA := createLocation(t, app, "Location Alpha", "Address Alpha", areaA)
locationB := createLocation(t, app, "Location Beta", "Address Beta", areaB) // locationB := createLocation(t, app, "Location Beta", "Address Beta", areaB)
flockOne := createFlock(t, app, "Flock Sort One") // flockOne := createFlock(t, app, "Flock Sort One")
flockTwo := createFlock(t, app, "Flock Sort Two") // flockTwo := createFlock(t, app, "Flock Sort Two")
fcrID := createFcr(t, app, "FCR Sort", []map[string]any{ // fcrID := createFcr(t, app, "FCR Sort", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, // {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) // })
kandangOne := createKandang(t, app, "Kandang Sort One", locationA, 1) // kandangOne := createKandang(t, app, "Kandang Sort One", locationA, 1)
kandangTwo := createKandang(t, app, "Kandang Sort Two", locationB, 1) // kandangTwo := createKandang(t, app, "Kandang Sort Two", locationB, 1)
kandangThree := createKandang(t, app, "Kandang Sort Three", locationB, 1) // kandangThree := createKandang(t, app, "Kandang Sort Three", locationB, 1)
projectOnePayload := map[string]any{ // projectOnePayload := map[string]any{
"flock_id": flockOne, // "flock_id": flockOne,
"area_id": areaA, // "area_id": areaA,
"category": "growing", // "category": "growing",
"fcr_id": fcrID, // "fcr_id": fcrID,
"location_id": locationA, // "location_id": locationA,
"kandang_ids": []uint{kandangOne}, // "kandang_ids": []uint{kandangOne},
} // }
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload) // resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload)
if resp.StatusCode != fiber.StatusCreated { // if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 for project one, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 201 for project one, got %d: %s", resp.StatusCode, string(body))
} // }
projectOneID := parseProjectFlockID(t, body) // projectOneID := parseProjectFlockID(t, body)
projectTwoPayload := map[string]any{ // projectTwoPayload := map[string]any{
"flock_id": flockTwo, // "flock_id": flockTwo,
"area_id": areaB, // "area_id": areaB,
"category": "laying", // "category": "laying",
"fcr_id": fcrID, // "fcr_id": fcrID,
"location_id": locationB, // "location_id": locationB,
"kandang_ids": []uint{kandangTwo, kandangThree}, // "kandang_ids": []uint{kandangTwo, kandangThree},
} // }
resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload) // resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload)
if resp.StatusCode != fiber.StatusCreated { // if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 for project two, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 201 for project two, got %d: %s", resp.StatusCode, string(body))
} // }
projectTwoID := parseProjectFlockID(t, body) // projectTwoID := parseProjectFlockID(t, body)
updatePeriodPayload := map[string]any{"period": 5} // updatePeriodPayload := map[string]any{"period": 5}
resp, body = doJSONRequest(t, app, http.MethodPatch, "/api/production/project_flocks/"+uintToString(projectTwoID), updatePeriodPayload) // resp, body = doJSONRequest(t, app, http.MethodPatch, "/api/production/project_flocks/"+uintToString(projectTwoID), updatePeriodPayload)
if resp.StatusCode != fiber.StatusOK { // if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating period, got %d: %s", resp.StatusCode, string(body)) // t.Fatalf("expected 200 when updating period, got %d: %s", resp.StatusCode, string(body))
} // }
assertOrder := func(t *testing.T, app *fiber.App, query string, expectedFirst uint) { // assertOrder := func(t *testing.T, app *fiber.App, query string, expectedFirst uint) {
t.Helper() // t.Helper()
resp, body := doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks?"+query, nil) // resp, body := doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks?"+query, nil)
if resp.StatusCode != fiber.StatusOK { // if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 for query %q, got %d: %s", query, resp.StatusCode, string(body)) // t.Fatalf("expected 200 for query %q, got %d: %s", query, resp.StatusCode, string(body))
} // }
var listResp struct { // var listResp struct {
Data []struct { // Data []struct {
Id uint `json:"id"` // Id uint `json:"id"`
} `json:"data"` // } `json:"data"`
} // }
if err := json.Unmarshal(body, &listResp); err != nil { // if err := json.Unmarshal(body, &listResp); err != nil {
t.Fatalf("failed to parse list response for %q: %v", query, err) // t.Fatalf("failed to parse list response for %q: %v", query, err)
} // }
if len(listResp.Data) == 0 { // if len(listResp.Data) == 0 {
t.Fatalf("expected data for query %q", query) // t.Fatalf("expected data for query %q", query)
} // }
if listResp.Data[0].Id != expectedFirst { // if listResp.Data[0].Id != expectedFirst {
t.Fatalf("expected first id %d for query %q, got %d", expectedFirst, query, listResp.Data[0].Id) // t.Fatalf("expected first id %d for query %q, got %d", expectedFirst, query, listResp.Data[0].Id)
} // }
} // }
assertOrder(t, app, "sort_by=area&sort_order=asc", projectOneID) // assertOrder(t, app, "sort_by=area&sort_order=asc", projectOneID)
assertOrder(t, app, "sort_by=location&sort_order=desc", projectTwoID) // assertOrder(t, app, "sort_by=location&sort_order=desc", projectTwoID)
assertOrder(t, app, "sort_by=period&sort_order=desc", projectTwoID) // assertOrder(t, app, "sort_by=period&sort_order=desc", projectTwoID)
assertOrder(t, app, "sort_by=kandangs&sort_order=desc", projectTwoID) // assertOrder(t, app, "sort_by=kandangs&sort_order=desc", projectTwoID)
assertOrder(t, app, "sort_by=kandangs&sort_order=asc", projectOneID) // assertOrder(t, app, "sort_by=kandangs&sort_order=asc", projectOneID)
} // }
func parseProjectFlockID(t *testing.T, body []byte) uint { // func parseProjectFlockID(t *testing.T, body []byte) uint {
t.Helper() // t.Helper()
var resp struct { // var resp struct {
Data struct { // Data struct {
Id uint `json:"id"` // Id uint `json:"id"`
} `json:"data"` // } `json:"data"`
} // }
if err := json.Unmarshal(body, &resp); err != nil { // if err := json.Unmarshal(body, &resp); err != nil {
t.Fatalf("failed to parse project flock response: %v", err) // t.Fatalf("failed to parse project flock response: %v", err)
} // }
return resp.Data.Id // return resp.Data.Id
} // }