Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh

This commit is contained in:
aguhh18
2025-11-05 14:03:45 +07:00
47 changed files with 3474 additions and 1810 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -0,0 +1,30 @@
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
-- Only alter if tables exist
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
ALTER TABLE project_chickins
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
ALTER TABLE project_flock_populations
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
END $$;
@@ -0,0 +1,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;
@@ -1,5 +0,0 @@
DROP TABLE IF EXISTS project_chickin_details;
DROP TABLE IF EXISTS project_chickins;
DROP TABLE IF EXISTS project_flock_populations;
+96 -406
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,22 +40,15 @@ 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
} }
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations) kandangs, err := seedKandangs(tx, adminID, locations, users)
if err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users, projectFlocks)
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,159 +231,16 @@ 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) (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 {
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 DOC",
Location: "Singaparna",
Period: 1,
},
{
Key: "Cikaum Period 1",
Flock: "Flock Banten",
Area: "Banten",
Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR DOC",
Location: "Cikaum",
Period: 1,
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
flockID, ok := flocks[seed.Flock]
if !ok {
return nil, fmt.Errorf("floc %s not seeded", seed.Flock)
}
areaID, ok := areas[seed.Area]
if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area)
}
fcrID, ok := fcrs[seed.Fcr]
if !ok {
return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr)
}
locationID, ok := locations[seed.Location]
if !ok {
return nil, 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 nil, err
}
} else if err != nil {
return nil, 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 nil, err
}
}
if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil {
return nil, err
}
result[seed.Key] = projectFlock.Id
}
return result, 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, projectFlocks 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
ProjectFlockKey *string
}{ }{
{Name: "Singaparna 1", Status: utils.KandangStatusActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")}, {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"},
{Name: "Cikaum 1", Status: utils.KandangStatusActive, Location: "Cikaum", PicKey: "admin", ProjectFlockKey: strPtr("Cikaum Period 1")}, {Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"}, {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
} }
@@ -411,15 +256,6 @@ 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 projectFlockID *uint
if seed.ProjectFlockKey != nil {
pfID, ok := projectFlocks[*seed.ProjectFlockKey]
if !ok {
return nil, fmt.Errorf("project flock %s not seeded", *seed.ProjectFlockKey)
}
projectFlockID = uintPtr(pfID)
}
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) {
@@ -428,15 +264,11 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
Status: string(seed.Status), Status: string(seed.Status),
LocationId: locID, LocationId: locID,
PicId: picID, PicId: picID,
ProjectFlockId: projectFlockID,
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
} }
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err
}
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} else { } else {
@@ -445,17 +277,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
"pic_id": picID, "pic_id": picID,
"status": string(seed.Status), "status": string(seed.Status),
} }
if projectFlockID != nil {
updates["project_flock_id"] = *projectFlockID
} else {
updates["project_flock_id"] = nil
}
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil { if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
return nil, err return nil, err
} }
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err
}
} }
result[seed.Name] = kandang.Id result[seed.Name] = kandang.Id
} }
@@ -463,38 +287,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return result, nil return result, nil
} }
func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint) error {
if err := detachActivePivot(tx, kandangID); err != nil {
return err
}
if projectFlockID == nil {
return nil
}
return ensureActivePivot(tx, *projectFlockID, kandangID)
}
func detachActivePivot(tx *gorm.DB, kandangID uint) error {
return tx.Where("kandang_id = ?", kandangID).
Delete(&entity.ProjectFlockKandang{}).Error
}
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error {
var pivot entity.ProjectFlockKandang
err := tx.Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&pivot).Error
if err == nil {
return nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
newRecord := entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: kandangID,
}
return tx.Create(&newRecord).Error
}
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error { func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -571,9 +363,10 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error)
Name string Name string
Code string Code string
}{ }{
{"Pullet", "PLT"},
{"Bahan Baku", "RAW"}, {"Bahan Baku", "RAW"},
{"Day Old Chick", "DOC"}, {"Day Old Chick", "DOC"},
{"Pullet", "PULLET"}, {"Telur", "EGG"},
} }
result := make(map[string]uint, len(seeds)) result := make(map[string]uint, len(seeds))
@@ -697,25 +490,14 @@ func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
} }
}{ }{
{ {
Name: "FCR DOC", Name: "FCR Layer",
Standards: []struct { Standards: []struct {
Weight float64 Weight float64
FcrNumber float64 FcrNumber float64
Mortality float64 Mortality float64
}{ }{
{Weight: 0.1, FcrNumber: 1.20, Mortality: 1.0}, {Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0},
{Weight: 0.3, FcrNumber: 1.35, Mortality: 1.5}, {Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5},
},
},
{
Name: "FCR Pullet",
Standards: []struct {
Weight float64
FcrNumber float64
Mortality float64
}{
{Weight: 0.5, FcrNumber: 1.45, Mortality: 2.0},
{Weight: 0.8, FcrNumber: 1.50, Mortality: 2.5},
}, },
}, },
} }
@@ -788,6 +570,56 @@ 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.FlagDOC}, Flags: []utils.FlagType{utils.FlagDOC},
}, },
{
Name: "Ayam Pullet",
Brand: "MBU Pullet",
Sku: "PLT0001",
Uom: "Ekor",
Category: "Pullet",
Price: 15000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPullet},
},
{
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Telur Konsumsi Baik",
Brand: "-",
Sku: "4",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{ {
Name: "281 SPECIAL STARTER", Name: "281 SPECIAL STARTER",
Brand: "281 STARTER", Brand: "281 STARTER",
@@ -799,26 +631,6 @@ 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: "DOC MAlindo",
Brand: "MAlindo",
Sku: "MAL0001",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 8000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagDOC},
},
{
Name: "Ayam Pullet",
Brand: "MBU Pullet",
Sku: "PUL0001",
Uom: "Ekor",
Category: "Pullet",
Price: 15000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPullet},
},
} }
for _, seed := range seeds { for _, seed := range seeds {
@@ -1058,25 +870,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,
} }
@@ -1085,6 +916,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
}
} }
} }
@@ -1165,153 +1002,6 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error {
return nil return nil
} }
// func seedChickin(tx *gorm.DB, createdBy uint) error {
// seeds := []struct {
// ProjectFlockKandangId uint
// ChickInDate string
// Quantity float64
// Note string
// }{
// {ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"},
// {ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"},
// }
// for _, seed := range seeds {
// chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate)
// if err != nil {
// return err
// }
// // Insert ProjectChickin jika belum ada
// var chickin entity.ProjectChickin
// err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate).
// First(&chickin).Error
// if errors.Is(err, gorm.ErrRecordNotFound) {
// chickin = entity.ProjectChickin{
// ProjectFlockKandangId: seed.ProjectFlockKandangId,
// ChickInDate: chickinDate,
// Quantity: seed.Quantity,
// Note: seed.Note,
// CreatedBy: createdBy,
// }
// if err := tx.Create(&chickin).Error; err != nil {
// return err
// }
// } else if err != nil {
// return err
// }
// var population entity.ProjectFlockPopulation
// err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error
// if errors.Is(err, gorm.ErrRecordNotFound) {
// population = entity.ProjectFlockPopulation{
// ProjectFlockKandangId: seed.ProjectFlockKandangId,
// InitialQuantity: seed.Quantity,
// CurrentQuantity: seed.Quantity,
// ReservedQuantity: 0,
// CreatedBy: createdBy,
// }
// if err := tx.Create(&population).Error; err != nil {
// return err
// }
// } else if err != nil {
// return err
// } else {
// // Update population quantities
// if err := tx.Model(&entity.ProjectFlockPopulation{}).
// Where("id = ?", population.Id).
// Updates(map[string]any{
// "initial_quantity": population.InitialQuantity + seed.Quantity,
// "current_quantity": population.CurrentQuantity + seed.Quantity,
// "reserved_quantity": 0,
// }).Error; err != nil {
// return err
// }
// }
// var pfk entity.ProjectFlockKandang
// if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil {
// if errors.Is(err, gorm.ErrRecordNotFound) {
// // no pivot found; skip creating details
// continue
// }
// return err
// }
// var warehouse entity.Warehouse
// if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil {
// // if warehouse not found, cannot create details
// if errors.Is(err, gorm.ErrRecordNotFound) {
// continue
// }
// return err
// }
// var productWarehouses []entity.ProductWarehouse
// err = tx.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 = ?", "DOC", warehouse.Id).
// Order("product_warehouses.created_at DESC").
// Find(&productWarehouses).Error
// if err != nil {
// return err
// }
// // If no product warehouses found, keep existing chickin.Quantity and skip details
// if len(productWarehouses) == 0 {
// continue
// }
// // sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne)
// totalQty := 0.0
// for _, pw := range productWarehouses {
// totalQty += pw.Quantity
// }
// if chickin.Quantity != totalQty {
// if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil {
// return err
// }
// chickin.Quantity = totalQty
// }
// for _, pw := range productWarehouses {
// // ensure detail exists or create it with full pw.Quantity
// var detail entity.ProjectChickinDetail
// err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error
// if errors.Is(err, gorm.ErrRecordNotFound) {
// detail = entity.ProjectChickinDetail{
// ProjectChickinId: chickin.Id,
// ProductWarehouseId: pw.Id,
// Quantity: pw.Quantity,
// CreatedBy: createdBy,
// }
// if err := tx.Create(&detail).Error; err != nil {
// return err
// }
// } else if err != nil {
// return err
// } else {
// if detail.Quantity != pw.Quantity {
// if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil {
// return err
// }
// }
// }
// // zero out pw quantity
// if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("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
} }
+1 -2
View File
@@ -12,7 +12,6 @@ type Kandang struct {
Status string `gorm:"type:varchar(50);not null"` Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"` PicId uint `gorm:"not null"`
ProjectFlockId *uint `gorm:"column:project_flock_id"`
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"`
@@ -20,5 +19,5 @@ type Kandang struct {
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"` Pic User `gorm:"foreignKey:PicId;references:Id"`
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
} }
+1 -1
View File
@@ -10,7 +10,7 @@ const ()
type ProjectChickin struct { type ProjectChickin struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null"` ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
ChickInDate time.Time `gorm:"not null"` ChickInDate time.Time `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"` ProductWarehouseId uint `gorm:"not null"`
UsageQty float64 `gorm:"type:numeric(15,3);not null"` UsageQty float64 `gorm:"type:numeric(15,3);not null"`
+5 -5
View File
@@ -8,24 +8,24 @@ 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"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"` Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
} }
+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:"-"`
} }
+3 -4
View File
@@ -1,4 +1,3 @@
package entities package entities
import "time" import "time"
@@ -6,9 +5,9 @@ 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"`
+1 -3
View File
@@ -4,10 +4,8 @@ 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"`
}
+2 -4
View File
@@ -4,10 +4,8 @@ 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,47 +18,37 @@ 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) (*entity.ProductWarehouse, error) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
WithTxRepo(tx *gorm.DB) ProductWarehouseRepository GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
DB() *gorm.DB 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) WithTxRepo(tx *gorm.DB) ProductWarehouseRepository {
return &ProductWarehouseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](tx),
db: tx,
}
}
func (r *ProductWarehouseRepositoryImpl) DB() *gorm.DB {
return r.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)
@@ -70,7 +61,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 {
@@ -89,34 +80,34 @@ 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). q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Preload("Product").
Preload("Warehouse").
Table("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").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId). Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC"). Order("product_warehouses.created_at DESC")
Find(&productWarehouses).Error
// preload relations so nested Product and Warehouse are populated
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return productWarehouses, nil return productWarehouses, nil
} }
func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) { func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
err := r.db.WithContext(ctx). query := r.DB()
Preload("Product"). if db != nil {
Preload("Warehouse"). query = db
Table("product_warehouses"). }
Select("product_warehouses.*"). fmt.Println(warehouseId)
err := query.WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId). Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC"). Order("product_warehouses.created_at DESC").
Limit(1). Preload("Product").Preload("Warehouse").
First(&productWarehouse).Error First(&productWarehouse).Error
if err != nil { if err != nil {
return nil, err return nil, err
@@ -124,10 +115,44 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c
return &productWarehouse, nil 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
}
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) { func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
var product entity.Product var product entity.Product
err := r.db.WithContext(ctx). err := r.DB().WithContext(ctx).
Joins("JOIN product_categories ON products.product_category_id = product_categories.id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ?", categoryCode). Where("product_categories.code = ?", categoryCode).
First(&product).Error First(&product).Error
if err != nil { if err != nil {
@@ -135,3 +160,30 @@ func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx conte
} }
return &product, nil return &product, nil
} }
func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
Order("product_warehouses.created_at DESC").
Preload("Product").Preload("Warehouse").
Find(&productWarehouses).Error
if err != nil {
return nil, err
}
return productWarehouses, nil
}
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) {
var product entity.Product
err := r.DB().WithContext(ctx).
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
Where("flags.name = ?", flagName).
First(&product).Error
if err != nil {
return nil, err
}
return &product, nil
}
@@ -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
}
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"errors"
"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"
@@ -18,6 +19,8 @@ type KandangRepository interface {
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error)
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
UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error
} }
type KandangRepositoryImpl struct { type KandangRepositoryImpl struct {
@@ -59,12 +62,13 @@ 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).
Model(&entity.Kandang{}). Table("kandangs k").
Where("project_flock_id = ?", projectFlockID). Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
Where("status = ?", utils.KandangStatusActive). Where("pfk.project_flock_id = ?", projectFlockID).
Where("deleted_at IS NULL") Where("k.status = ?", utils.KandangStatusActive).
Where("k.deleted_at IS NULL")
if excludeID != nil { if excludeID != nil {
q = q.Where("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
@@ -75,17 +79,58 @@ 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).
Where("project_flock_id = ?", projectFlockID). Table("kandangs k").
First(kandang).Error Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
Where("pfk.project_flock_id = ?", projectFlockID).
Where("k.deleted_at IS NULL").
Order("k.id ASC").
Limit(1).
Find(kandang).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
if kandang.Id == 0 {
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).
Table("project_flock_kandangs").
Select("kandang_id").
Where("project_flock_id = ?", projectFlockID)
return r.db.WithContext(ctx). return r.db.WithContext(ctx).
Model(&entity.Kandang{}). Model(&entity.Kandang{}).
Where("project_flock_id = ?", projectFlockID). Where("id IN (?)", sub).
Where("deleted_at IS NULL").
Update("status", string(status)).Error
}
func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error {
var link entity.ProjectFlockKandang
err := r.db.WithContext(ctx).
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&link).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
link = entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: kandangID,
}
return r.db.WithContext(ctx).Create(&link).Error
}
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 Update("status", string(status)).Error
} }
@@ -40,7 +40,8 @@ func NewKandangService(repo repository.KandangRepository, validate *validator.Va
} }
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB { func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").Preload("Location").Preload("Pic") return db.Preload("CreatedUser").Preload("Location").Preload("Pic").Preload("ProjectFlockKandangs.ProjectFlock")
} }
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) { func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
@@ -110,7 +111,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status")
} }
var projectFlockID *uint
if req.ProjectFlockId != nil { if req.ProjectFlockId != nil {
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil { if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
s.Log.Errorf("Failed to check project flock existence: %+v", err) s.Log.Errorf("Failed to check project flock existence: %+v", err)
@@ -128,8 +128,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
} }
} }
idCopy := *req.ProjectFlockId
projectFlockID = &idCopy
} }
//TODO: created by dummy //TODO: created by dummy
@@ -138,7 +136,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
LocationId: req.LocationId, LocationId: req.LocationId,
Status: status, Status: status,
PicId: req.PicId, PicId: req.PicId,
ProjectFlockId: projectFlockID,
CreatedBy: 1, CreatedBy: 1,
} }
@@ -147,6 +144,12 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
if req.ProjectFlockId != nil {
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, createBody.Id); err != nil {
s.Log.Errorf("Failed to link kandang to project_flock via pivot: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
}
}
return s.GetOne(c, createBody.Id) return s.GetOne(c, createBody.Id)
} }
@@ -201,7 +204,6 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
finalStatus = status finalStatus = status
} }
projectFlockIDToUse := existing.ProjectFlockId
if req.ProjectFlockId != nil { if req.ProjectFlockId != nil {
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil { if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
s.Log.Errorf("Failed to check project flock existence: %+v", err) s.Log.Errorf("Failed to check project flock existence: %+v", err)
@@ -209,24 +211,19 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} else if !exists { } else if !exists {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId)) return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId))
} }
idCopy := *req.ProjectFlockId
projectFlockIDToUse = &idCopy
updateBody["project_flock_id"] = idCopy
}
if projectFlockIDToUse != nil && finalStatus == string(utils.KandangStatusActive) { // Kalau status jadi ACTIVE, pastikan tidak ada kandang aktif lain pada project flock tsb (hitung via pivot)
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *projectFlockIDToUse, &id); err != nil { if finalStatus == string(utils.KandangStatusActive) {
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *projectFlockIDToUse, err) if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *req.ProjectFlockId, &id); err != nil {
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *req.ProjectFlockId, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock")
} else if active { } else if active {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang") return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang")
} }
} }
if len(updateBody) == 0 {
return s.GetOne(c, id)
} }
if len(updateBody) > 0 {
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found") return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
@@ -234,6 +231,14 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
s.Log.Errorf("Failed to update kandang: %+v", err) s.Log.Errorf("Failed to update kandang: %+v", err)
return nil, err return nil, err
} }
}
if req.ProjectFlockId != nil {
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, id); err != nil {
s.Log.Errorf("Failed to upsert pivot kandang-project_flock: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
}
}
return s.GetOne(c, id) return s.GetOne(c, id)
} }
@@ -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"
) )
@@ -101,9 +102,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 {
@@ -67,7 +67,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").
@@ -538,12 +537,12 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) {
products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), categoryCode, warehouseId) products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
if err == nil && len(products) > 0 { if err == nil && len(products) > 0 {
return &products[0], nil return &products[0], nil
} }
product, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), categoryCode) product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err) return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
} }
@@ -83,7 +83,6 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO
return &ProjectFlockDTO{ return &ProjectFlockDTO{
Id: pf.Id, Id: pf.Id,
Period: pf.Period, Period: pf.Period,
Flock: pf.Flock,
Area: pf.Area, Area: pf.Area,
Category: pf.Category, Category: pf.Category,
Fcr: pf.Fcr, Fcr: pf.Fcr,
@@ -222,11 +222,11 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
} }
func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
param := c.Params("flock_id") param := c.Params("project_flock_kandang_id")
id, err := strconv.Atoi(param) id, err := strconv.Atoi(param)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Flock Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
} }
summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id)) summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id))
@@ -10,6 +10,8 @@ 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"
@@ -18,6 +20,7 @@ import (
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 KandangWithProjectFlockIdDTO struct { type KandangWithProjectFlockIdDTO struct {
@@ -29,12 +32,12 @@ type KandangWithProjectFlockIdDTO struct {
type ProjectFlockListDTO struct { type ProjectFlockListDTO struct {
ProjectFlockBaseDTO ProjectFlockBaseDTO
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` // Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"` Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -57,32 +60,19 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
createdUser = &mapped createdUser = &mapped
} }
var kandangSummaries []KandangWithProjectFlockIdDTO var kandangSummaries []kandangDTO.KandangBaseDTO
if len(e.Kandangs) > 0 { if len(e.Kandangs) > 0 {
kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs)) kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs))
// Create a map of KandangId -> ProjectFlockKandangId from KandangHistory
kandangIdToProjectFlockKandangId := make(map[uint]uint)
for _, kh := range e.KandangHistory {
kandangIdToProjectFlockKandangId[kh.KandangId] = kh.Id
}
for i, kandang := range e.Kandangs { for i, kandang := range e.Kandangs {
baseDTO := kandangDTO.ToKandangBaseDTO(kandang) kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang)
kandangSummaries[i] = KandangWithProjectFlockIdDTO{
Id: baseDTO.Id,
Name: baseDTO.Name,
Status: baseDTO.Status,
ProjectFlockKandangId: kandangIdToProjectFlockKandangId[kandang.Id],
}
} }
} }
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
if e.Area.Id != 0 { if e.Area.Id != 0 {
@@ -110,7 +100,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
return ProjectFlockListDTO{ return ProjectFlockListDTO{
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
Flock: flockSummary, // Flock: flockSummary,
Area: areaSummary, Area: areaSummary,
Kandangs: kandangSummaries, Kandangs: kandangSummaries,
Category: e.Category, Category: e.Category,
@@ -166,6 +156,7 @@ 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"
) )
@@ -50,13 +51,14 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
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,20 +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)
IdExists(ctx context.Context, id uint) (bool, 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 {
@@ -29,15 +39,11 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
} }
} }
func (r *ProjectflockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { func (r *ProjectflockRepositoryImpl) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) {
return repository.Exists[entity.ProjectFlock](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID uint) ([]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
@@ -45,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) {
@@ -60,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
@@ -72,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).
@@ -91,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
}
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"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"
@@ -14,6 +15,10 @@ 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)
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository WithTx(tx *gorm.DB) ProjectFlockKandangRepository
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
DB() *gorm.DB DB() *gorm.DB
@@ -23,6 +28,8 @@ type projectFlockKandangRepositoryImpl struct {
db *gorm.DB db *gorm.DB
} }
const flockBaseNameExpression = "LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')))"
func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository { func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository {
return &projectFlockKandangRepositoryImpl{db: db} return &projectFlockKandangRepositoryImpl{db: db}
} }
@@ -47,7 +54,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").
@@ -80,7 +86,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").
@@ -102,7 +107,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").
@@ -118,3 +122,62 @@ 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
}
func (r *projectFlockKandangRepositoryImpl) MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
if strings.TrimSpace(baseName) == "" {
return 0, nil
}
var max int
err := r.db.WithContext(ctx).
Table("project_flock_kandangs pfk").
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Where(flockBaseNameExpression+" = LOWER(?)", baseName).
Select("COALESCE(MAX(pf.period), 0)").
Scan(&max).Error
return max, err
}
@@ -27,5 +27,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval) route.Post("/approvals", ctrl.Approval)
route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary) route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
} }
@@ -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,9 +31,9 @@ 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, int, 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)
} }
@@ -44,7 +46,7 @@ type projectflockService struct {
KandangRepo kandangRepository.KandangRepository KandangRepo kandangRepository.KandangRepository
WarehouseRepo warehouseRepository.WarehouseRepository WarehouseRepo warehouseRepository.WarehouseRepository
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
ProjectFlockKandangRepo repository.ProjectFlockKandangRepository PivotRepo repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey approvalWorkflow approvalutils.ApprovalWorkflowKey
} }
@@ -58,7 +60,7 @@ func NewProjectflockService(
repo repository.ProjectflockRepository, repo repository.ProjectflockRepository,
flockRepo flockRepository.FlockRepository, flockRepo flockRepository.FlockRepository,
kandangRepo kandangRepository.KandangRepository, kandangRepo kandangRepository.KandangRepository,
ProjectFlockKandangRepo repository.ProjectFlockKandangRepository, pivotRepo repository.ProjectFlockKandangRepository,
warehouseRepo warehouseRepository.WarehouseRepository, warehouseRepo warehouseRepository.WarehouseRepository,
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository, productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
@@ -72,7 +74,7 @@ func NewProjectflockService(
KandangRepo: kandangRepo, KandangRepo: kandangRepo,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
ProjectFlockKandangRepo: ProjectFlockKandangRepo, PivotRepo: pivotRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock, approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
} }
@@ -104,74 +106,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 kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds)
}
if params.Search != "" {
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
if normalizedSearch == "" {
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
}
likeQuery := "%" + normalizedSearch + "%"
db = db.
Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id").
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN 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 {
@@ -198,13 +137,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 {
@@ -240,15 +179,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 {
@@ -260,14 +212,14 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
if len(kandangs) != len(kandangIDs) { if len(kandangs) != len(kandangIDs) {
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
} }
for _, kandang := range kandangs { // larang kalau ada yg sudah terikat ke project lain
if kandang.ProjectFlockId != nil { if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah memiliki project flock", kandang.Name)) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
} } else if linked {
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
} }
createBody := &entity.ProjectFlock{ createBody := &entity.ProjectFlock{
FlockId: req.FlockId,
AreaId: req.AreaId, AreaId: req.AreaId,
Category: cat, Category: cat,
FcrId: req.FcrId, FcrId: req.FcrId,
@@ -278,11 +230,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
@@ -308,11 +265,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)
@@ -323,7 +283,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")
} }
@@ -334,15 +294,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)
if trimmed == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
canonicalBase := trimmed
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 hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{ }
Name: "Flock",
ID: req.FlockId,
Exists: relationExistsChecker[entity.Flock](s.Repository.DB()),
})
} }
if req.AreaId != nil { if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId updateBody["area_id"] = *req.AreaId
@@ -350,7 +323,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 {
@@ -367,7 +340,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 {
@@ -376,7 +349,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,
}) })
} }
@@ -404,11 +377,12 @@ 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")
} }
for _, k := range kandangs { if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
if k.ProjectFlockId != nil && *k.ProjectFlockId != id { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah terikat dengan project flock lain", k.Name)) } else if linked {
} return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
} }
} }
hasChanges := hasBodyChanges || hasKandangChanges hasChanges := hasBodyChanges || hasKandangChanges
@@ -419,6 +393,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
@@ -507,7 +504,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)
@@ -611,7 +611,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")
} }
@@ -645,28 +645,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, int, error) { func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) {
availableStock, err := s.GetAvailableDocQuantity(ctx, kandangID) pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
if err != nil {
return nil, 0, err
}
projectFlockKandang, err := s.ProjectFlockKandangRepo.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, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
} }
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")
}
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
if err != nil {
return nil, 0, err return nil, 0, err
} }
return projectFlockKandang, int(availableStock), nil 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) {
@@ -676,14 +718,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
} }
@@ -695,26 +730,55 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
return total, nil return total, nil
} }
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) { func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, projectFlockKandangID uint) (*FlockPeriodSummary, error) {
flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB { if projectFlockKandangID == 0 {
return db.Preload("CreatedUser") return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
})
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Flock not found")
}
if err != nil {
s.Log.Errorf("Failed get flock %d for period summary: %+v", flockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
} }
maxPeriod, err := s.Repository.GetMaxPeriodByFlock(c.Context(), flockID) pivot, err := s.pivotRepo().GetByID(c.Context(), projectFlockKandangID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
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 fetch project_flock_kandang %d: %+v", projectFlockKandangID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
var baseName string
var referenceFlock *entity.Flock
if pivot.ProjectFlock.Id != 0 {
baseName = pfutils.DeriveBaseName(pivot.ProjectFlock.FlockName)
}
if strings.TrimSpace(baseName) != "" {
referenceFlock, err = s.FlockRepo.GetByName(c.Context(), baseName)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch flock %q: %+v", baseName, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
}
}
if referenceFlock == nil {
referenceFlock = &entity.Flock{Name: pivot.ProjectFlock.FlockName}
}
maxPeriod := pivot.ProjectFlock.Period
if strings.TrimSpace(baseName) != "" {
if headerMax, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), baseName); err != nil {
s.Log.Warnf("Unable to compute header period for base %q: %+v", baseName, err)
} else if headerMax > maxPeriod {
maxPeriod = headerMax
}
if pivotMax, err := s.pivotRepo().MaxPeriodByBaseName(c.Context(), baseName); err != nil {
s.Log.Warnf("Unable to compute pivot period for base %q: %+v", baseName, err)
} else if pivotMax > maxPeriod {
maxPeriod = pivotMax
}
} }
return &FlockPeriodSummary{ return &FlockPeriodSummary{
Flock: *flock, Flock: *referenceFlock,
NextPeriod: maxPeriod + 1, NextPeriod: maxPeriod + 1,
}, nil }, nil
} }
@@ -732,45 +796,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": if !errors.Is(err, gorm.ErrRecordNotFound) {
return []string{ s.Log.Errorf("Failed to fetch flock by name %q: %+v", trimmed, err)
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction), return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
fmt.Sprintf("project_flocks.id %s", direction),
} }
case "kandangs":
return []string{ newFlock := &entity.Flock{
fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction), Name: trimmed,
fmt.Sprintf("project_flocks.id %s", direction), CreatedBy: 1, // TODO: replace with authenticated user
} }
case "period": if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
return []string{ if errors.Is(err, gorm.ErrDuplicatedKey) {
fmt.Sprintf("project_flocks.period %s", direction), return s.FlockRepo.GetByName(ctx, trimmed)
fmt.Sprintf("project_flocks.id %s", direction),
}
default:
return []string{
"project_flocks.created_at DESC",
"project_flocks.updated_at DESC",
} }
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 {
@@ -778,24 +861,45 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
return nil return nil
} }
if err := dbTransaction.Model(&entity.Kandang{}). if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusPengajuan); err != nil {
Where("id IN ?", kandangIDs). return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
Updates(map[string]any{
"project_flock_id": projectFlockID,
"status": string(utils.KandangStatusPengajuan),
}).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
} }
ProjectFlockKandangRepo := s.ProjectFlockKandangRepoWithTx(dbTransaction) already, err := s.pivotRepoWithTx(dbTransaction).ListExistingKandangIDs(ctx, projectFlockID, kandangIDs)
records := make([]*entity.ProjectFlockKandang, len(kandangIDs)) if err != nil {
for i, id := range kandangIDs { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot")
records[i] = &entity.ProjectFlockKandang{ }
exists := make(map[uint]struct{}, len(already))
for _, id := range already {
exists[id] = struct{}{}
}
var toAttach []uint
seen := make(map[uint]struct{}, len(kandangIDs))
for _, id := range kandangIDs {
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
if _, ok := exists[id]; !ok {
toAttach = append(toAttach, id)
}
}
if len(toAttach) == 0 {
return nil
}
records := make([]*entity.ProjectFlockKandang, 0, len(toAttach))
for _, id := range toAttach {
records = append(records, &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID, ProjectFlockId: projectFlockID,
KandangId: id, KandangId: id,
})
} }
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")
} }
if err := ProjectFlockKandangRepo.CreateMany(ctx, records); err != nil {
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
@@ -806,26 +910,55 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return nil return nil
} }
updates := map[string]any{"project_flock_id": 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 {
updates["status"] = string(utils.KandangStatusNonActive) if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
}
} }
if err := dbTransaction.Model(&entity.Kandang{}). if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
Where("id IN ?", kandangIDs).
Updates(updates).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
}
if err := s.ProjectFlockKandangRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
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
} }
func (s projectflockService) ProjectFlockKandangRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository { func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
if s.ProjectFlockKandangRepo == nil { if dbTransaction == nil {
return repository.NewProjectFlockKandangRepository(dbTransaction) return s.pivotRepo()
} }
return s.ProjectFlockKandangRepo.WithTx(dbTransaction) return s.pivotRepo().WithTx(dbTransaction)
}
func (s projectflockService) pivotRepo() repository.ProjectFlockKandangRepository {
if s.PivotRepo != nil {
return s.PivotRepo
}
return repository.NewProjectFlockKandangRepository(s.Repository.DB())
}
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,10 +1,14 @@
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 ===
@@ -13,18 +17,19 @@ 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"`
Ontime bool `json:"ontime"`
Day *int `json:"day,omitempty"` Day *int `json:"day,omitempty"`
TotalDepletion *int `json:"total_depletion,omitempty"` ProjectFlockCategory *string `json:"project_flock_category,omitempty"`
TotalDepletionQty *float64 `json:"total_depletion_qty,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 *int64 `json:"cum_intake,omitempty"` CumIntake *int `json:"cum_intake,omitempty"`
FcrValue *float64 `json:"fcr_value,omitempty"` FcrValue *float64 `json:"fcr_value,omitempty"`
TotalChick *int64 `json:"total_chick,omitempty"` TotalChickQty *float64 `json:"total_chick_qty,omitempty"`
DailyDepletionRate *float64 `json:"daily_depletion_rate,omitempty"` Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
CumDepletion *int `json:"cum_depletion,omitempty"` EggGradingStatus *string `json:"egg_grading_status,omitempty"`
EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"`
EggGradingCompletedQty *int `json:"egg_grading_completed_qty,omitempty"`
} }
type RecordingListDTO struct { type RecordingListDTO struct {
@@ -39,58 +44,84 @@ 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"`
} }
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"` ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
Notes *string `json:"notes,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 {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
ProductName string `json:"product_name"`
WarehouseId uint `json:"warehouse_id"`
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, gradingCompleted := 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,
EggGradingCompletedQty: gradingCompleted,
} }
} }
@@ -123,6 +154,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),
} }
} }
@@ -130,9 +162,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
@@ -143,8 +175,8 @@ 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),
} }
} }
return result return result
@@ -155,11 +187,138 @@ 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, ProductWarehouse: toRecordingProductWarehouseDTO(&s.ProductWarehouse),
Notes: s.Notes,
} }
} }
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 {
if pw == nil || pw.Id == 0 {
return nil
}
dto := RecordingProductWarehouseDTO{
Id: pw.Id,
ProductId: pw.ProductId,
WarehouseId: pw.WarehouseId,
}
if pw.Product.Id != 0 {
dto.ProductName = pw.Product.Name
}
if pw.Warehouse.Id != 0 {
dto.WarehouseName = pw.Warehouse.Name
}
return &dto
}
const goodEggProductWarehouseID uint = 5
func computeEggGradingStatus(e entity.Recording) (*string, *int, *int) {
goodEggs := filterGoodEggs(e.Eggs)
if len(goodEggs) == 0 {
return nil, nil, nil
}
totalEggs := 0
totalGraded := 0.0
for _, egg := range goodEggs {
totalEggs += egg.Qty
for _, grading := range egg.GradingEggs {
totalGraded += grading.Qty
}
}
if totalEggs == 0 {
return nil, nil, nil
}
pendingFloat := float64(totalEggs) - totalGraded
if pendingFloat < 0 {
pendingFloat = 0
}
pendingInt := int(math.Round(pendingFloat))
completedInt := int(math.Round(totalGraded))
if completedInt < 0 {
completedInt = 0
}
if pendingInt > 0 {
status := "GRADING_TELUR"
return &status, &pendingInt, &completedInt
}
status := "GRADING_SELESAI"
zero := 0
return &status, &zero, &completedInt
}
func filterGoodEggs(eggs []entity.RecordingEgg) []entity.RecordingEgg {
if len(eggs) == 0 {
return nil
}
result := make([]entity.RecordingEgg, 0, len(eggs))
for _, egg := range eggs {
if egg.ProductWarehouseId == goodEggProductWarehouseID {
result = append(result, egg)
}
}
return result
}
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,13 +1,51 @@
package repository package repository
import ( import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "context"
"errors"
"math"
"sort"
"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"
"gorm.io/gorm" "gorm.io/gorm"
) )
type RecordingRepository interface { type RecordingRepository interface {
repository.BaseRepository[entity.Recording] repository.BaseRepository[entity.Recording]
WithRelations(db *gorm.DB) *gorm.DB
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error
DeleteBodyWeights(tx *gorm.DB, recordingID uint) error
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) 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
DeleteDepletions(tx *gorm.DB, recordingID uint) error
ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, 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)
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error)
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error)
GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
} }
type RecordingRepositoryImpl struct { type RecordingRepositoryImpl struct {
@@ -19,3 +57,337 @@ func NewRecordingRepository(db *gorm.DB) RecordingRepository {
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
} }
} }
func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("ProjectFlockKandang").
Preload("ProjectFlockKandang.ProjectFlock").
Preload("BodyWeights").
Preload("Depletions").
Preload("Depletions.ProductWarehouse").
Preload("Depletions.ProductWarehouse.Product").
Preload("Depletions.ProductWarehouse.Warehouse").
Preload("Stocks").
Preload("Stocks.ProductWarehouse").
Preload("Stocks.ProductWarehouse.Product").
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) {
var days []int
if err := tx.Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
Where("day IS NOT NULL").
Pluck("day", &days).Error; err != nil {
return 0, err
}
return nextRecordingDay(days), nil
}
func (r *RecordingRepositoryImpl) CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error {
if len(bodyWeights) == 0 {
return nil
}
return tx.Create(&bodyWeights).Error
}
func (r *RecordingRepositoryImpl) DeleteBodyWeights(tx *gorm.DB, recordingID uint) error {
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingBW{}).Error
}
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
if len(stocks) == 0 {
return nil
}
return tx.Create(&stocks).Error
}
func (r *RecordingRepositoryImpl) DeleteStocks(tx *gorm.DB, recordingID uint) 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 {
if len(depletions) == 0 {
return nil
}
return tx.Create(&depletions).Error
}
func (r *RecordingRepositoryImpl) DeleteDepletions(tx *gorm.DB, recordingID uint) error {
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingDepletion{}).Error
}
func (r *RecordingRepositoryImpl) ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error) {
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{}).
Where("recording_id = ?", recordingID).
Select("COALESCE(SUM(qty), 0)").
Scan(&result).Error; err != nil {
return 0, err
}
return result, nil
}
func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) {
if currentDay <= 1 {
return nil, nil
}
var prev entity.Recording
err := tx.
Where("project_flock_kandangs_id = ? AND day < ?", projectFlockKandangId, currentDay).
Where("day IS NOT NULL").
Order("day DESC").
Limit(1).
Find(&prev).Error
if errors.Is(err, gorm.ErrRecordNotFound) || prev.Id == 0 {
return nil, nil
}
if err != nil {
return nil, err
}
return &prev, nil
}
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
var population entity.ProjectFlockPopulation
err := tx.
Where("project_flock_kandang_id = ?", projectFlockKandangId).
Order("created_at DESC").
First(&population).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil
}
if err != nil {
return 0, err
}
return int64(math.Round(population.TotalQty)), nil
}
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
var result struct {
TotalWeight float64
TotalQty float64
}
if err := tx.Model(&entity.RecordingBW{}).
Select("COALESCE(SUM(total_weight), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
Where("recording_id = ?", recordingID).
Scan(&result).Error; err != nil {
return 0, err
}
if result.TotalQty == 0 {
return 0, nil
}
return result.TotalWeight / result.TotalQty, nil
}
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
var rows []struct {
UsageQty float64
UomName string
}
if err := tx.
Table("recording_stocks").
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 products ON products.id = product_warehouses.product_id").
Joins("JOIN uoms ON uoms.id = products.uom_id").
Where("recording_stocks.recording_id = ?", recordingID).
Scan(&rows).Error; err != nil {
return 0, err
}
var total float64
for _, row := range rows {
if row.UsageQty <= 0 {
continue
}
switch strings.TrimSpace(row.UomName) {
case "kilogram", "kg", "kilograms", "kilo":
total += row.UsageQty * 1000
case "gram", "g", "grams":
total += row.UsageQty
default:
total += row.UsageQty
}
}
return total, nil
}
func (r *RecordingRepositoryImpl) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) {
var result struct {
FcrID uint
}
if err := tx.Table("project_flock_kandangs").
Select("project_flocks.fcr_id AS fcr_id").
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
Where("project_flock_kandangs.id = ?", projectFlockKandangId).
Scan(&result).Error; err != nil {
return 0, err
}
return result.FcrID, nil
}
func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
if fcrId == 0 {
return 0, false, nil
}
var standard entity.FcrStandard
err := tx.
Where("fcr_id = ? AND weight >= ?", fcrId, currentWeightKg).
Order("weight ASC").
First(&standard).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
err = tx.
Where("fcr_id = ?", fcrId).
Order("weight DESC").
First(&standard).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, false, nil
}
}
if err != nil {
return 0, false, err
}
weight := standard.Weight
if weight > 10 {
return weight / 1000, true, nil
}
return weight, true, nil
}
func nextRecordingDay(days []int) int {
if len(days) == 0 {
return 1
}
unique := make(map[int]struct{}, len(days))
for _, day := range days {
if day > 0 {
unique[day] = struct{}{}
}
}
normalized := make([]int, 0, len(unique))
for day := range unique {
normalized = append(normalized, day)
}
sort.Ints(normalized)
for idx, day := range normalized {
expected := idx + 1
if day != expected {
return expected
}
}
return len(normalized) + 1
}
@@ -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"` Qty *float64 `json:"qty,omitempty" validate:"required_without=UsageAmount,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 {
RecordingEggId uint `json:"recording_egg_id" validate:"required,number,min=1"`
Grade string `json:"grade" validate:"required"`
Qty float64 `json:"qty" validate:"required,gte=0"`
}
type SubmitGrading struct {
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"`
}
@@ -108,7 +108,6 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%") db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%")
} }
// Handle sort
sortField := "created_at" sortField := "created_at"
if params.Sort != "" { if params.Sort != "" {
sortField = params.Sort sortField = params.Sort
@@ -127,7 +126,6 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
return nil, 0, err return nil, 0, err
} }
// Filter by approval status if requested
if params.ApprovalStatus != "" { if params.ApprovalStatus != "" {
var filtered []entity.LayingTransfer var filtered []entity.LayingTransfer
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
@@ -156,7 +154,6 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran
return nil, err return nil, err
} }
// Fetch and populate latest approval
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil) latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil)
if err == nil && latestApproval != nil { if err == nil && latestApproval != nil {
@@ -172,7 +169,6 @@ func (s transferLayingService) GetOneWithApproval(c *fiber.Ctx, id uint) (*entit
return nil, nil, err return nil, nil, err
} }
// Return the LatestApproval that was populated in GetOne
return transferLaying, transferLaying.LatestApproval, nil return transferLaying, transferLaying.LatestApproval, nil
} }
@@ -181,11 +177,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
return nil, err return nil, err
} }
if err := commonSvc.EnsureRelations(c.Context(), if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil {
commonSvc.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, if errors.Is(err, gorm.ErrRecordNotFound) {
commonSvc.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found")
); err != nil { }
return nil, err return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate source project flock")
}
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Target Project Flock not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate target project flock")
} }
for _, detail := range req.SourceKandangs { for _, detail := range req.SourceKandangs {
@@ -288,7 +291,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record")
} }
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
for _, sourceDetail := range req.SourceKandangs { for _, sourceDetail := range req.SourceKandangs {
@@ -320,7 +323,6 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
} }
// Get warehouse for this kandang
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -359,7 +361,6 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
return nil, err return nil, err
} }
// Check if transfer laying exists
_, err := s.Repository.GetByID(c.Context(), id, nil) _, err := s.Repository.GetByID(c.Context(), id, nil)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -368,14 +369,12 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
} }
// Check if latest approval is PENDING (not approved)
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil) latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
} }
// If latest approval exists and is APPROVED or REJECTED, cannot update
if latestApproval != nil && latestApproval.Action != nil { if latestApproval != nil && latestApproval.Action != nil {
action := string(*latestApproval.Action) action := string(*latestApproval.Action)
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) { if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
@@ -409,7 +408,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
} }
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
// Verify transfer laying exists
_, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { _, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return db.Preload("Sources.ProductWarehouse").Preload("Targets") return db.Preload("Sources.ProductWarehouse").Preload("Targets")
}) })
@@ -420,14 +419,12 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
} }
// Check if latest approval is PENDING (not approved/rejected)
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil) latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
} }
// If latest approval exists and is APPROVED or REJECTED, cannot delete
if latestApproval != nil && latestApproval.Action != nil { if latestApproval != nil && latestApproval.Action != nil {
action := string(*latestApproval.Action) action := string(*latestApproval.Action)
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) { if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
@@ -435,22 +432,19 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
} }
// Delete in transaction to handle cascades and qty restoration
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 {
// Restore source warehouse quantities
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction)
// Get source repository for detail info productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id) sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
} }
// Restore quantity for each source that was reduced
for _, source := range sources { for _, source := range sources {
if source.ProductWarehouseId != nil && source.Qty > 0 { if source.ProductWarehouseId != nil && source.Qty > 0 {
// Add back the quantity that was transferred
if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{ if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{
"quantity": gorm.Expr("quantity + ?", source.Qty), "quantity": gorm.Expr("quantity + ?", source.Qty),
}, nil); err != nil { }, nil); err != nil {
@@ -459,7 +453,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
} }
// Restore project flock population that was reduced
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
for _, source := range sources { for _, source := range sources {
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId) populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
@@ -467,13 +460,12 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration")
} }
// Restore to latest populations first
remainingToRestore := source.Qty remainingToRestore := source.Qty
for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- { for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- {
pop := populations[i] pop := populations[i]
restoreAmount := remainingToRestore restoreAmount := remainingToRestore
if remainingToRestore < pop.TotalQty { if remainingToRestore < pop.TotalQty {
// Cap restore to what can fit in this population
restoreAmount = remainingToRestore restoreAmount = remainingToRestore
} }
@@ -486,7 +478,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
} }
// Delete the transfer laying (cascade will delete sources and targets)
if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil { if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying") return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
} }
@@ -536,7 +527,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs { for _, approvableID := range approvableIDs {
transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil) transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil)
@@ -606,6 +597,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
sourceWarehouse.ProductId, sourceWarehouse.ProductId,
targetWarehouse.Id, targetWarehouse.Id,
target.Qty, target.Qty,
actorID,
); err != nil { ); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse")
} }
@@ -665,9 +657,9 @@ func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayi
return err return err
} }
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64) (*entity.ProductWarehouse, error) { func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint) (*entity.ProductWarehouse, error) {
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(tx) productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx)
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID) existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil && existing != nil { if err == nil && existing != nil {
@@ -685,6 +677,7 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
ProductId: productID, ProductId: productID,
WarehouseId: warehouseID, WarehouseId: warehouseID,
Quantity: quantity, Quantity: quantity,
CreatedBy: actorID,
} }
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil { if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
@@ -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
@@ -192,6 +192,23 @@ var TransferToLayingApprovalSteps = map[approvalutils.ApprovalStep]string{
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// -------------------------------------------------------------------
// 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
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -320,6 +337,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")
+106
View File
@@ -0,0 +1,106 @@
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 {
var usageAmount float64
if item.Qty != nil {
usageAmount = *item.Qty
}
usagePtr := new(float64)
*usagePtr = usageAmount
pending := item.PendingQty
if pending == nil {
pending = new(float64)
}
result = append(result, entity.RecordingStock{
RecordingId: recordingID,
ProductWarehouseId: item.ProductWarehouseId,
UsageQty: usagePtr,
PendingQty: pending,
})
}
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
} // }