Merge branch 'dev/ragil-before-sso' into 'feat/BE/US-161,162/purchase'

[FEAT/BE][US#161,162/TASK#229,234,235,230,231,232,233] : purchase request and purchase order and fix master data dto

See merge request mbugroup/lti-api!60
This commit is contained in:
Hafizh A. Y.
2025-11-19 04:26:51 +00:00
102 changed files with 6994 additions and 912 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -13,6 +13,7 @@ type ApprovalRepository interface {
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
DeleteByTarget(ctx context.Context, workflow string, approvableID uint) error
}
type approvalRepositoryImpl struct {
@@ -104,3 +105,13 @@ func (r *approvalRepositoryImpl) LatestByTargets(
return result, nil
}
func (r *approvalRepositoryImpl) DeleteByTarget(
ctx context.Context,
workflow string,
approvableID uint,
) error {
return r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
Delete(&entity.Approval{}).Error
}
+40 -8
View File
@@ -3,6 +3,8 @@ package validation
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
)
@@ -21,34 +23,41 @@ var customMessages = map[string]string{
"alphanum": "Field %s must contain only alphanumeric characters",
"oneof": "Invalid value for field %s",
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
"gt": "Invalid %s, must be greater than %s",
}
func CustomErrorMessages(err error) map[string]string {
func CustomErrorMessages(err error) (string, map[string]string) {
var validationErrors validator.ValidationErrors
if errors.As(err, &validationErrors) {
return generateErrorMessages(validationErrors)
}
return nil
return "", nil
}
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
func generateErrorMessages(validationErrors validator.ValidationErrors) (string, map[string]string) {
errorsMap := make(map[string]string)
for _, err := range validationErrors {
var firstMessage string
for i, err := range validationErrors {
fieldName := err.StructNamespace()
tag := err.Tag()
customMessage := customMessages[tag]
var msg string
if customMessage != "" {
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
msg = formatErrorMessage(customMessage, err, tag)
} else {
errorsMap[fieldName] = defaultErrorMessage(err)
msg = defaultErrorMessage(err)
}
errorsMap[fieldName] = msg
if i == 0 {
firstMessage = msg
}
}
return errorsMap
return firstMessage, errorsMap
}
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
if tag == "min" || tag == "max" || tag == "len" {
if tag == "min" || tag == "max" || tag == "len" || tag == "gt" {
return fmt.Sprintf(customMessage, err.Field(), err.Param())
}
return fmt.Sprintf(customMessage, err.Field())
@@ -61,6 +70,16 @@ func defaultErrorMessage(err validator.FieldError) string {
func Validator() *validator.Validate {
validate := validator.New()
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
if jsonTag := getTagName(fld, "json"); jsonTag != "" {
return jsonTag
}
if queryTag := getTagName(fld, "query"); queryTag != "" {
return queryTag
}
return fld.Name
})
if err := validate.RegisterValidation("password", Password); err != nil {
return nil
}
@@ -72,3 +91,16 @@ func Validator() *validator.Validate {
}
return validate
}
func getTagName(fld reflect.StructField, tag string) string {
value, ok := fld.Tag.Lookup(tag)
if !ok || value == "-" {
return ""
}
name := strings.Split(value, ",")[0]
if name == "" || name == "-" {
return ""
}
return name
}
BIN
View File
Binary file not shown.
@@ -1,36 +0,0 @@
CREATE TABLE IF NOT EXISTS project_chickins (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
chick_in_date DATE NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
note TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by);
@@ -1,36 +0,0 @@
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
initial_quantity NUMERIC(15, 3) NOT NULL,
current_quantity NUMERIC(15, 3) NOT NULL,
reserved_quantity NUMERIC(15, 3),
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_chickin_details (
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
@@ -1,22 +1,30 @@
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
ALTER TABLE kandangs
DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE project_chickins
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
-- Only alter if tables exist
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
ALTER TABLE project_chickins
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
ALTER TABLE project_flock_populations
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
ALTER TABLE project_flock_populations
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
END $$;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS laying_transfers CASCADE;
@@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS laying_transfers (
id BIGSERIAL PRIMARY KEY,
transfer_number VARCHAR(50) UNIQUE NOT NULL,
from_project_flock_id BIGINT NOT NULL,
to_project_flock_id BIGINT NOT NULL,
transfer_date DATE NOT NULL,
pending_usage_qty NUMERIC(15, 3),
usage_qty NUMERIC(15, 3),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_from_project_flock
FOREIGN KEY (from_project_flock_id)
REFERENCES project_flocks(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_to_project_flock
FOREIGN KEY (to_project_flock_id)
REFERENCES project_flocks(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE UNIQUE INDEX IF NOT EXISTS idx_laying_transfers_transfer_number ON laying_transfers (transfer_number)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers (created_by);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers (deleted_at);
@@ -0,0 +1,58 @@
-- ============================================
-- MIGRATION: project_chickins
-- ============================================
-- STEP 1: Hapus tabel jika sudah ada
DROP TABLE IF EXISTS project_chickins;
-- STEP 2: Buat tabel project_chickins
CREATE TABLE IF NOT EXISTS project_chickins (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
chick_in_date DATE NOT NULL,
usage_qty NUMERIC(15, 3) NOT NULL,
pending_usage_qty NUMERIC(15, 3) DEFAULT 0,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- STEP 3: FOREIGN KEYS
BEGIN;
-- Relasi ke project_flock_kandangs
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke product_warehouses
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke users
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
COMMIT;
-- STEP 4: INDEXES
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by);
-- Composite index for common queries
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_deleted ON project_chickins (
project_flock_kandang_id,
deleted_at
);
-- Index for soft delete queries
CREATE INDEX IF NOT EXISTS idx_chickins_deleted_at ON project_chickins (deleted_at);
@@ -0,0 +1,62 @@
-- ============================================
-- MIGRATION: project_flock_populations
-- ============================================
-- STEP 1: Hapus tabel jika sudah ada
DROP TABLE IF EXISTS project_flock_populations;
-- STEP 2: Buat tabel project_flock_populations
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_chickin_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
total_qty NUMERIC(15, 3) NOT NULL,
total_used_qty NUMERIC(15, 3) DEFAULT 0,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- STEP 3: FOREIGN KEYS
BEGIN;
-- Relasi ke project_chickins
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_chickin FOREIGN KEY (project_chickin_id) REFERENCES project_chickins (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke product_warehouses
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke users
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
COMMIT;
-- STEP 4: INDEXES
CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by);
-- Composite index for common queries
CREATE INDEX IF NOT EXISTS idx_populations_chickin_deleted ON project_flock_populations (
project_chickin_id,
deleted_at
);
-- Index for soft delete queries
CREATE INDEX IF NOT EXISTS idx_populations_deleted_at ON project_flock_populations (deleted_at);
-- Unique constraint: one population per chickin
CREATE UNIQUE INDEX IF NOT EXISTS idx_populations_chickin_unique ON project_flock_populations (project_chickin_id)
WHERE
deleted_at IS NULL;
@@ -0,0 +1,5 @@
-- Rollback laying_transfer_sources dan laying_transfer_targets tables
DROP TABLE IF EXISTS laying_transfer_targets CASCADE;
DROP TABLE IF EXISTS laying_transfer_sources CASCADE;
@@ -0,0 +1,93 @@
-- Create laying_transfer_sources dan laying_transfer_targets tables
-- 1. Create laying_transfer_sources table (detail sumber - kandang asal growing)
CREATE TABLE laying_transfer_sources (
id BIGSERIAL PRIMARY KEY,
laying_transfer_id BIGINT NOT NULL,
source_project_flock_kandang_id BIGINT NOT NULL,
product_warehouse_id BIGINT,
qty NUMERIC(15, 3) NOT NULL,
note TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Add foreign keys untuk laying_transfer_sources
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
ALTER TABLE laying_transfer_sources
ADD CONSTRAINT fk_laying_transfer_sources_laying_transfer_id
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE laying_transfer_sources
ADD CONSTRAINT fk_laying_transfer_sources_project_flock_kandang_id
FOREIGN KEY (source_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfer_sources
ADD CONSTRAINT fk_laying_transfer_sources_product_warehouse_id
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
END IF;
END $$;
-- 2. Create laying_transfer_targets table (detail tujuan - kandang laying)
CREATE TABLE laying_transfer_targets (
id BIGSERIAL PRIMARY KEY,
laying_transfer_id BIGINT NOT NULL,
target_project_flock_kandang_id BIGINT NOT NULL,
qty NUMERIC(15, 3) NOT NULL,
product_warehouse_id BIGINT,
note TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Add foreign keys untuk laying_transfer_targets
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
ALTER TABLE laying_transfer_targets
ADD CONSTRAINT fk_laying_transfer_targets_laying_transfer_id
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE laying_transfer_targets
ADD CONSTRAINT fk_laying_transfer_targets_project_flock_kandang_id
FOREIGN KEY (target_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfer_targets
ADD CONSTRAINT fk_laying_transfer_targets_product_warehouse_id
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
END IF;
END $$;
-- 3. Create indexes untuk laying_transfer_sources
CREATE INDEX idx_laying_transfer_sources_laying_transfer_id ON laying_transfer_sources (laying_transfer_id);
CREATE INDEX idx_laying_transfer_sources_source_kandang_id ON laying_transfer_sources (
source_project_flock_kandang_id
);
CREATE INDEX idx_laying_transfer_sources_product_warehouse_id ON laying_transfer_sources (product_warehouse_id);
CREATE INDEX idx_laying_transfer_sources_deleted_at ON laying_transfer_sources (deleted_at);
-- 4. Create indexes untuk laying_transfer_targets
CREATE INDEX idx_laying_transfer_targets_laying_transfer_id ON laying_transfer_targets (laying_transfer_id);
CREATE INDEX idx_laying_transfer_targets_target_kandang_id ON laying_transfer_targets (
target_project_flock_kandang_id
);
CREATE INDEX idx_laying_transfer_targets_product_warehouse_id ON laying_transfer_targets (product_warehouse_id);
CREATE INDEX idx_laying_transfer_targets_deleted_at ON laying_transfer_targets (deleted_at);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS purchase_items;
@@ -0,0 +1,54 @@
CREATE TABLE IF NOT EXISTS purchase_items (
id BIGSERIAL PRIMARY KEY,
purchase_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
warehouse_id BIGINT NOT NULL,
product_warehouse_id BIGINT,
received_date TIMESTAMPTZ,
travel_number VARCHAR,
travel_number_docs VARCHAR,
vehicle_number VARCHAR,
sub_qty NUMERIC(15, 3) NOT NULL,
total_qty NUMERIC(15, 3) NOT NULL DEFAULT 0,
total_used NUMERIC(15, 3) NOT NULL DEFAULT 0,
price NUMERIC(15, 3) NOT NULL DEFAULT 0,
total_price NUMERIC(15, 3) NOT NULL DEFAULT 0,
CONSTRAINT uq_purchase_items_purchase_product_warehouse
UNIQUE (purchase_id, product_id, warehouse_id)
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_product
FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_warehouse
FOREIGN KEY (warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_product_warehouse
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL ON UPDATE CASCADE';
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id);
@@ -0,0 +1,14 @@
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_purchase_items_purchase'
AND conrelid = 'purchase_items'::regclass
) THEN
ALTER TABLE purchase_items
DROP CONSTRAINT fk_purchase_items_purchase;
END IF;
END $$;
DROP TABLE IF EXISTS purchases;
@@ -0,0 +1,58 @@
CREATE TABLE IF NOT EXISTS purchases (
id BIGSERIAL PRIMARY KEY,
pr_number VARCHAR NOT NULL,
po_number VARCHAR NULL,
po_date TIMESTAMPTZ NULL,
supplier_id BIGINT NOT NULL,
credit_term INT NOT NULL,
due_date TIMESTAMPTZ,
grand_total NUMERIC(15, 3) NOT NULL,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL,
CONSTRAINT uq_purchases_pr_number UNIQUE (pr_number),
CONSTRAINT uq_purchases_po_number UNIQUE (po_number)
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
EXECUTE
'ALTER TABLE purchases
ADD CONSTRAINT fk_purchases_supplier
FOREIGN KEY (supplier_id)
REFERENCES suppliers(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
EXECUTE
'ALTER TABLE purchases
ADD CONSTRAINT fk_purchases_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (
SELECT 1 FROM pg_tables WHERE tablename = 'purchase_items'
) AND NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_purchase_items_purchase'
) THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_purchase
FOREIGN KEY (purchase_id)
REFERENCES purchases(id)
ON DELETE CASCADE ON UPDATE CASCADE';
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id);
CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by);
CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date);
CREATE INDEX IF NOT EXISTS idx_purchases_deleted_at ON purchases (deleted_at);
@@ -0,0 +1,2 @@
ALTER TABLE kandangs
DROP COLUMN IF EXISTS capacity;
@@ -0,0 +1,2 @@
ALTER TABLE kandangs
ADD COLUMN capacity NUMERIC(15,3) NOT NULL;
@@ -0,0 +1,16 @@
BEGIN;
ALTER TABLE project_flock_kandangs
DROP COLUMN IF EXISTS period;
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS period INT NOT NULL DEFAULT 0;
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;
COMMIT;
@@ -0,0 +1,29 @@
BEGIN;
ALTER TABLE project_flock_kandangs
ADD COLUMN IF NOT EXISTS period INT;
UPDATE project_flock_kandangs pfk
SET period = pf.period
FROM project_flocks pf
WHERE pfk.project_flock_id = pf.id
AND (pfk.period IS NULL OR pfk.period = 0)
AND pf.period IS NOT NULL;
ALTER TABLE project_flock_kandangs
ALTER COLUMN period SET DEFAULT 0;
UPDATE project_flock_kandangs
SET period = 0
WHERE period IS NULL;
ALTER TABLE project_flock_kandangs
ALTER COLUMN period SET NOT NULL;
-- Drop period from project_flocks as the source of truth
DROP INDEX IF EXISTS project_flocks_base_period_unique;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS period;
COMMIT;
+36 -41
View File
@@ -235,13 +235,14 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
seeds := []struct {
Name string
Status utils.KandangStatus
Capacity float64
Location string
PicKey string
}{
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
}
result := make(map[string]uint, len(seeds))
@@ -571,52 +572,46 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Flags: []utils.FlagType{utils.FlagDOC},
},
{
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Name: "Ayam Pullet",
Brand: "MBU Pullet",
Sku: "PLT0001",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Category: "Pullet",
Price: 15000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPullet},
},
{
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Name: "Ayam 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 Konsumsi Baik",
Brand: "-",
Sku: "4",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Unit",
Category: "Telur",
Price: 1,
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "281 SPECIAL STARTER",
+13 -12
View File
@@ -7,17 +7,18 @@ import (
)
type Kandang struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
Capacity float64 `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
}
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingKandangTransfer struct {
Id uint `gorm:"primaryKey"`
KandangId uint
ProductWarehouseId uint
Qty float64 `gorm:"type:numeric(15,3)"`
LayingTransferId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
}
+29
View File
@@ -0,0 +1,29 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransfer struct {
Id uint `gorm:"primaryKey"`
TransferNumber string `gorm:"uniqueIndex;not null"`
FromProjectFlockId uint `gorm:"not null"`
ToProjectFlockId uint `gorm:"not null"`
TransferDate time.Time `gorm:"type:date;not null"`
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
UsageQty *float64 `gorm:"type:numeric(15,3)"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
@@ -0,0 +1,23 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransferSource struct {
Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"`
Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
@@ -0,0 +1,24 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransferTarget struct {
Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"`
TargetProjectFlockKandangId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
ProductWarehouseId *uint `gorm:""`
Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
TargetProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:TargetProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+3 -2
View File
@@ -16,6 +16,7 @@ type Location struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:LocationId;references:Id"`
}
+7 -4
View File
@@ -12,13 +12,16 @@ type ProjectChickin struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
ChickInDate time.Time `gorm:"not null"`
Quantity float64 `gorm:"not null"`
Note string `gorm:"type:text"`
ProductWarehouseId uint `gorm:"not null"`
UsageQty float64 `gorm:"type:numeric(15,3);not null"`
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+13 -12
View File
@@ -7,17 +7,18 @@ import (
)
type ProjectFlockPopulation struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Id uint `gorm:"primaryKey"`
ProjectChickinId uint `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"`
TotalQty float64 `gorm:"type:numeric(15,3);not null"`
TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
-1
View File
@@ -13,7 +13,6 @@ type ProjectFlock struct {
Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"`
Period int `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
+9 -8
View File
@@ -3,13 +3,14 @@ package entities
import "time"
type ProjectFlockKandang struct {
Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
Period int `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+27
View File
@@ -0,0 +1,27 @@
package entities
import (
"time"
)
type Purchase struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
PrNumber string `gorm:"not null"`
PoNumber *string
PoDate *time.Time
SupplierId uint64 `gorm:"not null"`
CreditTerm *int
DueDate *time.Time
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
Notes *string
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt *time.Time `gorm:"index"`
CreatedBy uint64 `gorm:"not null"`
// Relations
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
Items []PurchaseItem `gorm:"foreignKey:PurchaseId"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+28
View File
@@ -0,0 +1,28 @@
package entities
import (
"time"
)
type PurchaseItem struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
PurchaseId uint64 `gorm:"not null"`
ProductId uint64 `gorm:"not null"`
WarehouseId uint64 `gorm:"not null"`
ProductWarehouseId *uint64
ReceivedDate *time.Time
TravelNumber *string
TravelNumberDocs *string
VehicleNumber *string
SubQty float64 `gorm:"type:numeric(15,3);not null"`
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
Price float64 `gorm:"type:numeric(15,3);default:0"`
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
// Relations
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
@@ -11,6 +11,7 @@ import (
)
type ApprovalBaseDTO struct {
Id uint `json:"id"`
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Action *string `json:"action"`
@@ -27,6 +28,7 @@ type ApprovalGroupDTO struct {
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
dto := ApprovalBaseDTO{
Id: e.Id,
Notes: e.Notes,
}
@@ -82,6 +82,10 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"LOKASI",
"KANDANG",
},
"stock_log": map[string][]string{
"log_types": []string{"TRANSFER", "ADJUSTMENT"},
"transaction_types": []string{"INCREASE", "DECREASE"},
},
"supplier_categories": []string{
"BOP",
"SAPRONAK",
@@ -2,6 +2,7 @@ package repository
import (
"context"
"errors"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
@@ -19,8 +20,13 @@ type ProductWarehouseRepository interface {
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint64) (uint, error)
}
type ProductWarehouseRepositoryImpl struct {
@@ -78,14 +84,14 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
err := r.DB().WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
Order("product_warehouses.created_at DESC")
// preload relations so nested Product and Warehouse are populated
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
if err != nil {
return nil, err
}
@@ -100,12 +106,12 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c
}
fmt.Println(warehouseId)
err := query.WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC").
Preload("Product").Preload("Warehouse").
First(&productWarehouse).Error
if err != nil {
return nil, err
@@ -146,3 +152,123 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
}
return nil
}
func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error {
if len(affected) == 0 {
return nil
}
ids := make([]uint, 0, len(affected))
for id := range affected {
ids = append(ids, id)
}
var emptyIDs []uint
if err := r.DB().WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("id IN ? AND COALESCE(quantity,0) <= 0", ids).
Pluck("id", &emptyIDs).Error; err != nil {
return err
}
if len(emptyIDs) == 0 {
return nil
}
if err := r.DB().WithContext(ctx).
Model(&entity.PurchaseItem{}).
Where("product_warehouse_id IN ?", emptyIDs).
Update("product_warehouse_id", nil).Error; err != nil {
return err
}
if err := r.DB().WithContext(ctx).
Where("id IN ?", emptyIDs).
Delete(&entity.ProductWarehouse{}).Error; err != nil {
return err
}
return nil
}
func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
ctx context.Context,
productID uint,
warehouseID uint,
createdBy uint64,
) (uint, error) {
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil {
return record.Id, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, err
}
entity := &entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouseID,
Quantity: 0,
CreatedBy: uint(createdBy),
}
if entity.CreatedBy == 0 {
entity.CreatedBy = 1
}
if err := r.CreateOne(ctx, entity, nil); err != nil {
return 0, err
}
return entity.Id, nil
}
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
err := r.DB().WithContext(ctx).
Preload("Product").
Preload("Warehouse").
Preload("Warehouse.Area").
Preload("Warehouse.Location").
First(&productWarehouse, id).Error
if err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
var product entity.Product
err := r.DB().WithContext(ctx).
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ?", categoryCode).
First(&product).Error
if err != nil {
return nil, err
}
return &product, nil
}
func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
Order("product_warehouses.created_at DESC").
Preload("Product").Preload("Warehouse").
Find(&productWarehouses).Error
if err != nil {
return nil, err
}
return productWarehouses, nil
}
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) {
var product entity.Product
err := r.DB().WithContext(ctx).
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
Where("flags.name = ?", flagName).
First(&product).Error
if err != nil {
return nil, err
}
return &product, nil
}
@@ -20,7 +20,7 @@ type CustomerBaseDTO struct {
AccountNumber string `json:"account_number"`
Balance float64 `json:"balance"`
Pic *userDTO.UserBaseDTO `json:"pic"`
Pic *userDTO.UserBaseDTO `json:"pic,omitempty"`
}
type CustomerListDTO struct {
@@ -14,15 +14,21 @@ type KandangBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Pic *userDTO.UserBaseDTO `json:"pic"`
Capacity float64 `json:"capacity"`
Location locationDTO.LocationBaseDTO `json:"location,omitempty"`
Pic userDTO.UserBaseDTO `json:"pic,omitempty"`
}
type KandangListDTO struct {
KandangBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Capacity float64 `json:"capacity"`
Location locationDTO.LocationBaseDTO `json:"location"`
Pic userDTO.UserBaseDTO `json:"pic"`
CreatedUser userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type KandangDetailDTO struct {
@@ -32,39 +38,56 @@ type KandangDetailDTO struct {
// === Mapper Functions ===
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
var location *locationDTO.LocationBaseDTO
var location locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
location = &mapped
location = mapped
}
var pic *userDTO.UserBaseDTO
var pic userDTO.UserBaseDTO
if e.Pic.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.Pic)
pic = &mapped
pic = mapped
}
return KandangBaseDTO{
Id: e.Id,
Name: e.Name,
Status: e.Status,
Capacity: e.Capacity,
Location: location,
Pic: pic,
}
}
func ToKandangListDTO(e entity.Kandang) KandangListDTO {
var createdUser *userDTO.UserBaseDTO
var location locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
location = mapped
}
var pic userDTO.UserBaseDTO
if e.Pic.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.Pic)
pic = mapped
}
var createdUser userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
createdUser = mapped
}
return KandangListDTO{
KandangBaseDTO: ToKandangBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Id: e.Id,
Name: e.Name,
Status: e.Status,
Location: location,
Pic: pic,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
@@ -7,6 +7,7 @@ import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
@@ -115,9 +116,41 @@ func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, p
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&link).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
var project entity.ProjectFlock
if err := r.db.WithContext(ctx).
Select("id, location_id").
First(&project, projectFlockID).Error; err != nil {
return err
}
var kandang entity.Kandang
if err := r.db.WithContext(ctx).
Select("id, location_id").
First(&kandang, kandangID).Error; err != nil {
return err
}
if kandang.LocationId != project.LocationId {
return fiber.NewError(fiber.StatusBadRequest, "Kandang tidak berada pada lokasi yang sama dengan project flock")
}
// Determine project's period from existing pivot rows so the new kandang
// shares the same period.
var period int
if err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("project_flock_id = ?", projectFlockID).
Select("COALESCE(MAX(period), 0)").
Scan(&period).Error; err != nil {
return err
}
if period <= 0 {
period = 1
}
link = entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: kandangID,
Period: period,
}
return r.db.WithContext(ctx).Create(&link).Error
}
+2 -2
View File
@@ -1,7 +1,7 @@
package kandangs
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService
ctrl := controller.NewKandangController(s)
route := v1.Group("/kandangs")
route.Use(m.Auth(u))
// route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -134,6 +134,7 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
createBody := &entity.Kandang{
Name: req.Name,
LocationId: req.LocationId,
Capacity: req.Capacity,
Status: status,
PicId: req.PicId,
CreatedBy: 1,
@@ -194,6 +195,10 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["pic_id"] = *req.PicId
}
if req.Capacity != nil {
updateBody["capacity"] = *req.Capacity
}
finalStatus := strings.ToUpper(existing.Status)
if req.Status != nil {
status := strings.ToUpper(*req.Status)
@@ -1,19 +1,21 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Status string `json:"status,omitempty" validate:"omitempty,min=3"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"`
Name string `json:"name" validate:"required_strict,min=3"`
Status string `json:"status,omitempty" validate:"omitempty,min=3"`
Capacity float64 `json:"capacity" validate:"required_strict,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"`
Name *string `json:"name,omitempty" validate:"omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
Capacity *float64 `json:"capacity" validate:"omitempty,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Query struct {
@@ -14,11 +14,14 @@ type LocationBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
Area *areaDTO.AreaBaseDTO `json:"area"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
}
type LocationListDTO struct {
LocationBaseDTO
Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
Area *areaDTO.AreaBaseDTO `json:"area"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -52,11 +55,20 @@ func ToLocationListDTO(e entity.Location) LocationListDTO {
createdUser = &mapped
}
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
return LocationListDTO{
LocationBaseDTO: ToLocationBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Id: e.Id,
Name: e.Name,
Address: e.Address,
Area: area,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
@@ -12,16 +12,17 @@ import (
// === DTO Structs ===
type NonstockBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
UomID uint `json:"uom_id"`
Id uint `json:"id"`
Name string `json:"name"`
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
}
type NonstockListDTO struct {
NonstockBaseDTO
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
Id uint `json:"id"`
Name string `json:"name"`
Uom *uomDTO.UomBaseDTO `json:"uom"`
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
Flags []string `json:"flags"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -35,10 +36,22 @@ type NonstockDetailDTO struct {
// === Mapper Functions ===
func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO {
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return NonstockBaseDTO{
Id: e.Id,
Name: e.Name,
UomID: e.UomId,
Uom: uomRef,
Flags: flags,
}
}
@@ -66,13 +79,13 @@ func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO {
}
return NonstockListDTO{
NonstockBaseDTO: ToNonstockBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Uom: uomRef,
Suppliers: suppliers,
Flags: flags,
Id: e.Id,
Name: e.Name,
Uom: uomRef,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Suppliers: suppliers,
}
}
@@ -13,8 +13,10 @@ import (
// === DTO Structs ===
type ProductBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Id uint `json:"id"`
Name string `json:"name"`
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
}
type ProductListDTO struct {
@@ -25,10 +27,8 @@ type ProductListDTO struct {
SellingPrice *float64 `json:"selling_price,omitempty"`
Tax *float64 `json:"tax,omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty"`
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
Flags []string `json:"flags"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -42,9 +42,22 @@ type ProductDetailDTO struct {
// === Mapper Functions ===
func ToProductBaseDTO(e entity.Product) ProductBaseDTO {
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
return ProductBaseDTO{
Id: e.Id,
Name: e.Name,
Id: e.Id,
Name: e.Name,
Flags: flags,
Uom: uomRef,
}
}
@@ -55,12 +68,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
createdUser = &mapped
}
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
var categoryRef *productCategoryDTO.ProductCategoryBaseDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
@@ -72,11 +79,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
suppliers[i] = supplierDTO.ToSupplierBaseDTO(s)
}
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return ProductListDTO{
Brand: e.Brand,
Sku: e.Sku,
@@ -88,10 +90,8 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Uom: uomRef,
ProductCategory: categoryRef,
Suppliers: suppliers,
Flags: flags,
}
}
@@ -16,6 +16,7 @@ type ProductRepository interface {
IdExists(ctx context.Context, id uint) (bool, error)
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
@@ -90,6 +91,17 @@ func (r *ProductRepositoryImpl) GetSuppliersByIDs(ctx context.Context, supplierI
return suppliers, nil
}
func (r *ProductRepositoryImpl) IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error) {
var count int64
if err := r.DB().WithContext(ctx).
Model(&entity.ProductSupplier{}).
Where("product_id = ? AND supplier_id = ?", productID, supplierID).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error {
db := tx
if db == nil {
+4 -2
View File
@@ -15,7 +15,8 @@ type UomBaseDTO struct {
}
type UomListDTO struct {
UomBaseDTO
Id uint `json:"id"`
Name string `json:"name"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -42,7 +43,8 @@ func ToUomListDTO(e entity.Uom) UomListDTO {
}
return UomListDTO{
UomBaseDTO: ToUomBaseDTO(e),
Id: e.Id,
Name: e.Name,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
@@ -16,16 +16,21 @@ type WarehouseBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Area *areaDTO.AreaBaseDTO `json:"area"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
}
type WarehouseListDTO struct {
WarehouseBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Area *areaDTO.AreaBaseDTO `json:"area"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WarehouseDetailDTO struct {
@@ -70,11 +75,34 @@ func ToWarehouseListDTO(e entity.Warehouse) WarehouseListDTO {
createdUser = &mapped
}
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
var location *locationDTO.LocationBaseDTO
if e.Location != nil && e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(*e.Location)
location = &mapped
}
var kandang *kandangDTO.KandangBaseDTO
if e.Kandang != nil && e.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangBaseDTO(*e.Kandang)
kandang = &mapped
}
return WarehouseListDTO{
WarehouseBaseDTO: ToWarehouseBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Id: e.Id,
Name: e.Name,
Type: e.Type,
Area: area,
Location: location,
Kandang: kandang,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
@@ -16,6 +16,8 @@ type WarehouseRepository interface {
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error)
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error)
}
type WarehouseRepositoryImpl struct {
@@ -60,3 +62,28 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
}
return &warehouse, nil
}
func (r *WarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error) {
var warehouse entity.Warehouse
err := r.db.WithContext(ctx).
Preload("Area").
Preload("Location").
First(&warehouse, id).Error
if err != nil {
return nil, err
}
return &warehouse, nil
}
func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
var warehouse entity.Warehouse
err := r.db.WithContext(ctx).
Where("kandang_id = ?", kandangId).
Where("deleted_at IS NULL").
Order("id DESC").
First(&warehouse).Error
if err != nil {
return nil, err
}
return &warehouse, nil
}
@@ -1,7 +1,6 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
@@ -22,30 +21,84 @@ func NewChickinController(chickinService service.ChickinService) *ChickinControl
}
}
func (u *ChickinController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
// func (u *ChickinController) GetAll(c *fiber.Ctx) error {
// query := &validation.Query{
// Page: c.QueryInt("page", 1),
// Limit: c.QueryInt("limit", 10),
// ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
// }
// result, totalResults, err := u.ChickinService.GetAll(c, query)
// if err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Get all chickins successfully",
// Meta: response.Meta{
// Page: query.Page,
// Limit: query.Limit,
// TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
// TotalResults: totalResults,
// },
// Data: dto.ToChickinListDTOs(result),
// })
// }
// func (u *ChickinController) GetOne(c *fiber.Ctx) error {
// param := c.Params("id")
// id, err := strconv.Atoi(param)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
// }
// result, err := u.ChickinService.GetOne(c, uint(id))
// if err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.Success{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Get chickin successfully",
// Data: dto.ToChickinListDTO(*result),
// })
// }
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, totalResults, err := u.ChickinService.GetAll(c, query)
results, err := u.ChickinService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
Code: fiber.StatusOK,
var (
data interface{}
message = "Create chickin successfully"
)
if len(results) == 1 {
data = dto.ToChickinListDTO(results[0])
} else {
message = "Create chickins successfully"
data = dto.ToChickinListDTOs(results)
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Get all chickins successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToChickinListDTOs(result),
Message: message,
Data: data,
})
}
@@ -67,95 +120,85 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error {
Code: fiber.StatusOK,
Status: "success",
Message: "Get chickin successfully",
Data: dto.ToChickinListDTO(*result),
Data: dto.ToChickinDetailDTO(*result),
})
}
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
// func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
// req := new(validation.Update)
// param := c.Params("id")
// id, err := strconv.Atoi(param)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
// }
// if err := c.BodyParser(req); err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
// }
// result, err := u.ChickinService.UpdateOne(c, req, uint(id))
// if err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.Success{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Update chickin successfully",
// Data: dto.ToChickinListDTO(*result),
// })
// }
// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
// param := c.Params("id")
// id, err := strconv.Atoi(param)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
// }
// if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.Common{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Delete chickin successfully",
// })
// }
func (u *ChickinController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ChickinService.CreateOne(c, req)
results, err := u.ChickinService.Approval(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create chickin successfully",
Data: dto.ToChickinListDTO(*result),
})
}
func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ChickinService.UpdateOne(c, req, uint(id))
if err != nil {
return err
var (
data interface{}
message = "Submit chickin approval successfully"
)
if len(results) == 1 {
data = dto.ToChickinListDTO(results[0])
} else {
message = "Submit chickin approvals successfully"
data = dto.ToChickinListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update chickin successfully",
Data: dto.ToChickinListDTO(*result),
})
}
func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete chickin successfully",
})
}
func (u *ChickinController) Approve(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ChickinService.Approve(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Approve chickin successfully",
Data: nil,
Message: message,
Data: data,
})
}
@@ -16,11 +16,13 @@ import (
// === DTO Structs (ordered) ===
type ChickinBaseDTO struct {
Id uint `json:"id"`
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
ChickInDate time.Time `json:"chick_in_date"`
Quantity float64 `json:"quantity"`
Note string `json:"note"`
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
}
type ProjectFlockDTO struct {
@@ -45,21 +47,32 @@ type ChickinSimpleDTO struct {
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"`
Quantity float64 `json:"quantity"`
Note string `json:"note"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
CreatedBy uint `json:"created_by"`
}
type ChickinListDTO struct {
ChickinBaseDTO
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ChickinDetailDTO struct {
ChickinListDTO
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
CreatedBy uint `json:"created_by"`
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// === Mapper Functions (ordered) ===
@@ -87,7 +100,8 @@ func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO {
return userBaseDTO.ToUserBaseDTO(e)
}
func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO {
e := pfk.ProjectFlock
var flock *flockBaseDTO.FlockBaseDTO
if base := pfutils.DeriveBaseName(e.FlockName); base != "" {
summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base}
@@ -110,7 +124,7 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
}
return ProjectFlockDTO{
Id: e.Id,
Period: e.Period,
Period: pfk.Period,
Category: e.Category,
Flock: flock,
Area: area,
@@ -122,7 +136,7 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
var pf *ProjectFlockDTO
if e.ProjectFlock.Id != 0 {
mapped := ToProjectFlockDTO(e.ProjectFlock)
mapped := ToProjectFlockDTO(e)
pf = &mapped
}
var kandang *kandangBaseDTO.KandangBaseDTO
@@ -138,17 +152,22 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
}
func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO {
var pfk *ProjectFlockKandangDTO
if e.ProjectFlockKandang.Id != 0 {
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
pfk = &mapped
var projectFlockKandangId uint
// Check if ProjectFlockKandang relation is loaded
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 {
projectFlockKandangId = e.ProjectFlockKandang.Id
} else if e.ProjectFlockKandangId != 0 {
// If relation is not loaded but ID is available, use the ID
projectFlockKandangId = e.ProjectFlockKandangId
}
return ChickinBaseDTO{
Id: e.Id,
ProjectFlockKandang: pfk,
ChickInDate: e.ChickInDate,
Quantity: e.Quantity,
Note: e.Note,
Id: e.Id,
ProjectFlockKandangId: projectFlockKandangId,
ChickInDate: e.ChickInDate,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
}
}
@@ -157,29 +176,25 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO {
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
ChickInDate: e.ChickInDate,
Quantity: e.Quantity,
Note: e.Note,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
CreatedBy: e.CreatedBy,
}
}
func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO {
var createdUser *userBaseDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser)
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
var pfk *ProjectFlockKandangDTO
if e.ProjectFlockKandang.Id != 0 {
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
pfk = &mapped
}
return ChickinListDTO{
ChickinBaseDTO: ToChickinBaseDTO(e),
ProjectFlockKandang: pfk,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
ChickinBaseDTO: ToChickinBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
@@ -200,7 +215,31 @@ func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO {
}
func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO {
var createdUser *userBaseDTO.UserBaseDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
return ChickinDetailDTO{
ChickinListDTO: ToChickinListDTO(e),
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
ChickInDate: e.ChickInDate,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
CreatedBy: e.CreatedBy,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO {
result := make([]ChickinDetailDTO, len(e))
for i, r := range e {
result[i] = ToChickinDetailDTO(r)
}
return result
}
@@ -1,10 +1,15 @@
package chickins
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
@@ -15,6 +20,8 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type ChickinModule struct{}
@@ -32,6 +39,12 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
}
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
@@ -11,21 +11,26 @@ import (
type ProjectChickinRepository interface {
repository.BaseRepository[entity.ProjectChickin]
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
}
type ChickinRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectChickin]
db *gorm.DB
}
func NewChickinRepository(db *gorm.DB) ProjectChickinRepository {
return &ChickinRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db),
db: db,
}
}
func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) {
var chickin entity.ProjectChickin
err := r.DB().WithContext(ctx).
err := r.db.WithContext(ctx).
Where("project_floc_id = ?", projectFlockID).
Where("deleted_at IS NULL").
First(&chickin).Error
@@ -34,3 +39,43 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
}
return &chickin, nil
}
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Order("created_at DESC").
Find(&chickins).Error
if err != nil {
return nil, err
}
return chickins, nil
}
func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where("usage_qty = 0").
Where("pending_usage_qty > 0").
Order("created_at DESC").
Find(&chickins).Error
if err != nil {
return nil, err
}
return chickins, nil
}
func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where("pending_usage_qty > 0").
Select("COALESCE(SUM(pending_usage_qty), 0)").
Row().Scan(&total)
if err != nil {
return 0, err
}
return total, nil
}
@@ -1,6 +1,8 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
@@ -8,6 +10,10 @@ import (
type ProjectChickinDetailRepository interface {
repository.BaseRepository[entity.ProjectChickinDetail]
CreateOne(ctx context.Context, entity *entity.ProjectChickinDetail, modifier func(*gorm.DB) *gorm.DB) error
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error)
WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository
}
type ChickinDetailRepositoryImpl struct {
@@ -19,3 +25,22 @@ func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository {
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
}
}
func (r *ChickinDetailRepositoryImpl) WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository {
return &ChickinDetailRepositoryImpl{BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](tx)}
}
func (r *ChickinDetailRepositoryImpl) DB() *gorm.DB {
return r.BaseRepositoryImpl.DB()
}
func (r *ChickinDetailRepositoryImpl) GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) {
var records []entity.ProjectChickinDetail
if err := r.DB().WithContext(ctx).Where("project_chickin_id = ?", projectChickinID).Find(&records).Error; err != nil {
return nil, err
}
if len(records) == 0 {
return nil, gorm.ErrRecordNotFound
}
return records, nil
}
@@ -15,10 +15,10 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
route := v1.Group("/chickins")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
// route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Post("/:id/approve", ctrl.Approve)
// route.Patch("/:id", ctrl.UpdateOne)
// route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval)
}
@@ -2,7 +2,11 @@ package service
import (
"errors"
"fmt"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -21,10 +25,10 @@ import (
type ChickinService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
Approve(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
}
type chickinService struct {
@@ -76,6 +80,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
}
offset := (params.Page - 1) * params.Limit
chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.ProjectFlockKandangId != 0 {
@@ -103,112 +108,164 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e
return chickin, nil
}
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) {
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return nil, err
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
}
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId)
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId)
if err != nil {
s.Log.Errorf("Failed to get warehouse: %+v", err)
return nil, err
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found")
}
// move complex DB query into repository for cleaner service
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
if err != nil {
s.Log.Errorf("Failed to get product warehouses: %+v", err)
return nil, err
}
if len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
}
totalQuantity := 0.0
for _, pw := range productWarehouses {
totalQuantity += pw.Quantity
var productWarehouses []entity.ProductWarehouse
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
var productCategoryCode string
switch category {
case string(utils.ProjectFlockCategoryGrowing):
productCategoryCode = "DOC"
case string(utils.ProjectFlockCategoryLaying):
productCategoryCode = "PULLET"
default:
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category))
}
if totalQuantity < 1 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id)
if err != nil || len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category)))
}
chickinDate, err := utils.ParseDateString(req.ChickInDate)
if err != nil {
s.Log.Errorf("Failed to parse chickin date: %+v", err)
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format")
}
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: projectflockkandang.Id,
ChickInDate: chickinDate,
Quantity: totalQuantity,
Note: req.Note,
CreatedBy: 1, //todo: ganti dengan user login
}
err = s.Repository.CreateOne(c.Context(), newChickin, nil)
if err != nil {
s.Log.Errorf("Failed to create chickin: %+v", err)
return nil, err
}
// Update semua product warehouse: set quantity jadi 0
for _, pw := range productWarehouses {
err = s.ProductWarehouseRepo.PatchOne(c.Context(), pw.Id, map[string]any{
"quantity": 0,
}, nil)
actorID := uint(1) // todo nanti ambil dari auth context
newChikins := make([]*entity.ProjectChickin, 0)
for _, productWarehouse := range productWarehouses {
availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category)
if err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return nil, err
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", productWarehouse.Id))
}
newChickinDetail := &entity.ProjectChickinDetail{
ProjectChickinId: newChickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: 1, // todo: ganti dengan user login
if availableQty <= 0 {
continue
}
err = s.ProjectChickinDetailRepo.CreateOne(c.Context(), newChickinDetail, nil)
if err != nil {
s.Log.Errorf("Failed to create chickin detail: %+v", err)
return nil, err
}
}
existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to get project flock population: %+v", err)
return nil, err
}
if existingPopulation != nil {
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), existingPopulation.Id, map[string]any{
"reserved_quantity": newChickin.Quantity + existingPopulation.ReservedQuantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return nil, err
}
} else {
newPopulation := &entity.ProjectFlockPopulation{
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: req.ProjectFlockKandangId,
InitialQuantity: 0,
CurrentQuantity: 0,
ReservedQuantity: newChickin.Quantity,
CreatedBy: 1, // todo: ganti dengan user login
}
err = s.ProjectflockPopulationRepo.CreateOne(c.Context(), newPopulation, nil)
if err != nil {
s.Log.Errorf("Failed to create project flock population: %+v", err)
return nil, err
ChickInDate: chickinDate,
UsageQty: 0,
PendingUsageQty: availableQty,
ProductWarehouseId: productWarehouse.Id,
Notes: req.Note,
CreatedBy: actorID,
}
newChikins = append(newChikins, newChickin)
}
return s.GetOne(c, newChickin.Id)
if len(newChikins) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create")
}
existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins")
}
isFirstTime := len(existingChikins) == 0
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
}
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
}
if category == string(utils.ProjectFlockCategoryLaying) {
for _, chickin := range newChikins {
updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity")
}
}
}
var approvalAction entity.ApprovalAction
if isFirstTime {
approvalAction = entity.ApprovalActionCreated
} else {
approvalAction = entity.ApprovalActionUpdated
}
if latest == nil {
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
projectFlockKandang.Id,
utils.ProjectFlockKandangStepPengajuan,
&approvalAction,
actorID,
nil); err != nil {
if !errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
}
}
} else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
projectFlockKandang.Id,
utils.ProjectFlockKandangStepPengajuan,
&approvalAction,
actorID,
nil); err != nil {
if !errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
}
}
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
}
result := make([]entity.ProjectChickin, 0, len(newChikins))
for _, chickin := range newChikins {
loaded, err := s.GetOne(c, chickin.Id)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reload chickin %d with relations: %v", chickin.Id, err))
}
result = append(result, *loaded)
}
if len(result) == 0 {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins")
}
return result, nil
}
func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) {
@@ -222,7 +279,7 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["chick_in_date"] = req.ChickInDate
}
if req.Note != "" {
updateBody["note"] = req.Note
updateBody["notes"] = req.Note
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
@@ -240,174 +297,335 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
}
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
db := s.Repository.DB()
tx := db.WithContext(c.Context()).Begin()
if tx.Error != nil {
s.Log.Errorf("Failed to begin transaction: %+v", tx.Error)
return tx.Error
}
rollback := func(err error) error {
if rerr := tx.Rollback().Error; rerr != nil {
s.Log.Errorf("Rollback failed: %+v", rerr)
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
}
return err
}
chickinRepoTx := s.Repository.WithTx(tx)
pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx)
return nil
}
chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
}
if err != nil {
s.Log.Errorf("Failed get chickin by id: %+v", err)
return rollback(err)
}
func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) {
availableQty := productWarehouse.Quantity
var population entity.ProjectFlockPopulation
if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found"))
}
s.Log.Errorf("Failed to get project flock population: %+v", err)
return rollback(err)
}
if category == string(utils.ProjectFlockCategoryGrowing) {
var totalPendingQty float64
newReserved := population.ReservedQuantity - chickin.Quantity
if newReserved < 0 {
newReserved = 0
}
if err := tx.WithContext(c.Context()).Model(&entity.ProjectFlockPopulation{}).Where("id = ?", population.Id).Updates(map[string]any{"reserved_quantity": newReserved}).Error; err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return rollback(err)
}
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err == nil {
for _, chickin := range chickins {
restoreFromDetails := func() (bool, error) {
var details []entity.ProjectChickinDetail
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
return false, err
}
if len(details) == 0 {
return false, nil
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
}
for _, d := range details {
var pw entity.ProductWarehouse
if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
availableQty = productWarehouse.Quantity - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
} else if category == string(utils.ProjectFlockCategoryLaying) {
var totalPopulation float64
var totalPendingQty float64
populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id)
if err == nil {
for _, pop := range populations {
totalPopulation += pop.TotalQty
}
}
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err == nil {
for _, chickin := range chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
}
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
}
return availableQty, nil
}
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected):
action = entity.ApprovalActionRejected
case string(entity.ApprovalActionApproved):
action = entity.ApprovalActionApproved
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
if len(approvableIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
for _, id := range approvableIDs {
idCopy := id
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
return nil, err
}
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
}
if latestApproval == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
}
if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
}
}
step := utils.ProjectFlockKandangStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.ProjectFlockKandangStepDisetujui
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs {
actorID := uint(1) // todo nanti ambil dari auth context
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
}
if action == entity.ApprovalActionApproved {
chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID))
}
kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
}
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
if category == string(utils.ProjectFlockCategoryGrowing) {
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
}
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
}
} else if category == string(utils.ProjectFlockCategoryLaying) {
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
}
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
}
}
}
if action == entity.ApprovalActionRejected {
chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID))
}
if len(chickins) == 0 {
continue
}
return false, err
}
updatedQuantity := pw.Quantity + d.Quantity
if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
return false, err
kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
}
categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category))
for _, chickin := range chickins {
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId))
}
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id))
}
}
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id))
}
}
}
}
}
return nil
})
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil {
return false, err
}
return true, nil
}
restored, err := restoreFromDetails()
if err != nil {
s.Log.Errorf("Failed to restore from chickin details: %+v", err)
return rollback(err)
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
if !restored {
updated := make([]entity.ProjectChickin, 0)
for _, kandangID := range approvableIDs {
var chickins []entity.ProjectChickin
if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load approved chickins")
}
updated = append(updated, chickins...)
}
projectflockkandang, err := pfkRepoTx.GetByID(c.Context(), population.ProjectFlockKandangId)
return updated, nil
}
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) {
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
if err == nil && len(products) > 0 {
return &products[0], nil
}
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
if err != nil {
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
}
if product == nil {
return nil, fmt.Errorf("no %s product found in system", categoryCode)
}
newPW := &entity.ProductWarehouse{
ProductId: product.Id,
WarehouseId: warehouseId,
Quantity: 0,
CreatedBy: actorID,
}
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err)
}
return newPW, nil
}
func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
if targetPW == nil || targetPW.Id == 0 {
return fmt.Errorf("invalid target product warehouse")
}
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
for _, chickin := range chickins {
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return rollback(err)
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
}
var warehouse entity.Warehouse
if err := tx.WithContext(c.Context()).Where("kandang_id = ?", projectflockkandang.KandangId).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang"))
if populationExists {
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
continue
}
quantityToConvert := chickin.PendingUsageQty
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
"usage_qty": quantityToConvert,
"pending_usage_qty": 0,
}, nil); err != nil {
return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err)
}
if chickin.ProductWarehouseId != targetPW.Id {
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
"quantity": gorm.Expr("quantity - ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
}
return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err)
}
s.Log.Errorf("Failed to get warehouse: %+v", err)
return rollback(err)
}
productWarehouse, err := s.ProductWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(
c.Context(),
"DOC",
warehouse.Id,
tx,
)
if err != nil {
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse"))
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
}
s.Log.Errorf("Failed to get product warehouse: %+v", err)
return rollback(err)
return fmt.Errorf("failed to update target warehouse quantity: %w", err)
}
updatedQuantity := productWarehouse.Quantity + chickin.Quantity
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return rollback(err)
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: targetPW.Id,
TotalQty: quantityToConvert,
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
}
}
// delete chickin (single place)
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
return err
}
s.Log.Errorf("Failed to delete chickin: %+v", err)
return rollback(err)
}
if err := tx.Commit().Error; err != nil {
s.Log.Errorf("Failed to commit transaction: %+v", err)
return rollback(err)
}
return nil
}
func (s *chickinService) Approve(c *fiber.Ctx, id uint) error {
// todo: ini contoh akhir jika sudah approved
chickin, err := s.Repository.GetByID(
c.Context(),
id,
nil,
)
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
}
if err != nil {
s.Log.Errorf("Failed get chickin by id: %+v", err)
return err
}
population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId)
if err != nil {
s.Log.Errorf("Failed to get project flock population: %+v", err)
return err
}
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{
"reserved_quantity": population.ReservedQuantity - chickin.Quantity,
"initial_quantity": population.InitialQuantity + chickin.Quantity,
"current_quantity": population.CurrentQuantity + chickin.Quantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return err
}
return nil
@@ -3,7 +3,7 @@ package validation
type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
Note string `json:"note" validate:"omitempty`
Note string `json:"note" validate:"omitempty"`
}
type Update struct {
@@ -16,3 +16,9 @@ type Query struct {
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
}
type Approve struct {
Action string `json:"action" validate:"required_strict"`
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
@@ -0,0 +1,76 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ProjectFlockKandangController struct {
ProjectFlockKandangService service.ProjectFlockKandangService
}
func NewProjectFlockKandangController(projectFlockKandangService service.ProjectFlockKandangService) *ProjectFlockKandangController {
return &ProjectFlockKandangController{
ProjectFlockKandangService: projectFlockKandangService,
}
}
func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all projectFlockKandangs successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProjectFlockKandangListDTOs(result),
})
}
func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, availableQtys, err := u.ProjectFlockKandangService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get projectFlockKandang successfully",
Data: dto.ToProjectFlockKandangListDTOWithAvailableQty(*result, availableQtys),
})
}
@@ -0,0 +1,330 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs (ordered) ===
type ProjectFlockKandangBaseDTO struct {
Id uint `json:"id"`
}
type ProjectFlockDTO struct {
Id uint `json:"id"`
Period int `json:"period"`
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type KandangDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
type ProductWarehouseDTO struct {
Id uint `json:"id"`
Product *productDTO.ProductBaseDTO `json:"product,omitempty"`
Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"`
}
type AvailableQtyDTO struct {
AvailableQty float64 `json:"available_qty"`
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
}
type ProjectFlockKandangListDTO struct {
ProjectFlockKandangBaseDTO
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
Kandang *KandangDTO `json:"kandang,omitempty"`
Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"`
AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
type ProjectFlockKandangDetailDTO struct {
ProjectFlockKandangListDTO
}
// === Mapper Functions (ordered) ===
func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO {
return ProjectFlockKandangBaseDTO{
Id: e.Id,
}
}
func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO {
if pf == nil {
return nil
}
return &ProjectFlockDTO{
Id: pf.Id,
Period: pf.Period,
Area: pf.Area,
Category: pf.Category,
Fcr: pf.Fcr,
Location: pf.Location,
CreatedUser: pf.CreatedUser,
CreatedAt: pf.CreatedAt,
UpdatedAt: pf.UpdatedAt,
}
}
func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO {
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
if e.ProjectFlock.Id != 0 {
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
projectFlockSummary = &mapped
}
return ProjectFlockKandangListDTO{
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangDTO(e.Kandang),
Chickins: toChickinDTOs(e.Chickins),
AvailableQtys: toAvailableQtyDTOsFromRaw(availableQtysRaw),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTO(e),
}
}
func toKandangDTO(kandang entity.Kandang) *KandangDTO {
if kandang.Id == 0 {
return nil
}
return &KandangDTO{
Id: kandang.Id,
Name: kandang.Name,
Status: kandang.Status,
}
}
func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO {
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
return &mapped
}
return nil
}
func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKandangListDTO {
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
if e.ProjectFlock.Id != 0 {
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
projectFlockSummary = &mapped
}
return ProjectFlockKandangListDTO{
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangDTO(e.Kandang),
Chickins: toChickinDTOs(e.Chickins),
AvailableQtys: toAvailableQtyDTOs(e.Chickins),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTO(e),
}
}
func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFlockKandangListDTO {
result := make([]ProjectFlockKandangListDTO, len(e))
for i, r := range e {
result[i] = ToProjectFlockKandangListDTO(r)
}
return result
}
func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDetailDTO {
return ProjectFlockKandangDetailDTO{
ProjectFlockKandangListDTO: ToProjectFlockKandangListDTO(e),
}
}
// === Helper Functions (ordered) ===
func toProductWarehouseDTO(pwData map[string]interface{}) *ProductWarehouseDTO {
if pwData == nil {
return nil
}
dto := &ProductWarehouseDTO{}
if id, ok := pwData["id"].(float64); ok {
dto.Id = uint(id)
} else if id, ok := pwData["id"].(uint); ok {
dto.Id = id
}
if pData, ok := pwData["product"].(map[string]interface{}); ok {
dto.Product = toProductDTO(pData)
}
if wData, ok := pwData["warehouse"].(map[string]interface{}); ok {
dto.Warehouse = toWarehouseDTO(wData)
}
return dto
}
func toProductDTO(pData map[string]interface{}) *productDTO.ProductBaseDTO {
if pData == nil {
return nil
}
product := &productDTO.ProductBaseDTO{}
if id, ok := pData["id"].(float64); ok {
product.Id = uint(id)
} else if id, ok := pData["id"].(uint); ok {
product.Id = id
}
if name, ok := pData["name"].(string); ok {
product.Name = name
}
return product
}
func toWarehouseDTO(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO {
if wData == nil {
return nil
}
warehouse := &warehouseDTO.WarehouseBaseDTO{}
if id, ok := wData["id"].(float64); ok {
warehouse.Id = uint(id)
} else if id, ok := wData["id"].(uint); ok {
warehouse.Id = id
}
if name, ok := wData["name"].(string); ok {
warehouse.Name = name
}
if wType, ok := wData["type"].(string); ok {
warehouse.Type = wType
}
return warehouse
}
func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO {
if pf.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(pf.CreatedUser)
return &mapped
} else if pf.CreatedBy != 0 {
return &userDTO.UserBaseDTO{
Id: pf.CreatedBy,
IdUser: int64(pf.CreatedBy),
}
}
return nil
}
func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO {
if len(chickins) == 0 {
return nil
}
result := make([]chickinDTO.ChickinBaseDTO, len(chickins))
for i, ch := range chickins {
result[i] = chickinDTO.ToChickinBaseDTO(ch)
}
return result
}
func toAvailableQtyDTOs(chickins []entity.ProjectChickin) []AvailableQtyDTO {
if len(chickins) == 0 {
return nil
}
availableQtyMap := make(map[uint]AvailableQtyDTO)
for _, ch := range chickins {
if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 {
continue
}
if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists {
continue
}
pwDTO := &ProductWarehouseDTO{
Id: ch.ProductWarehouse.Id,
}
if ch.ProductWarehouse.Product.Id != 0 {
pwDTO.Product = &productDTO.ProductBaseDTO{
Id: ch.ProductWarehouse.Product.Id,
Name: ch.ProductWarehouse.Product.Name,
}
}
if ch.ProductWarehouse.Warehouse.Id != 0 {
pwDTO.Warehouse = &warehouseDTO.WarehouseBaseDTO{
Id: ch.ProductWarehouse.Warehouse.Id,
Name: ch.ProductWarehouse.Warehouse.Name,
Type: ch.ProductWarehouse.Warehouse.Type,
}
}
availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{
ProductWarehouse: pwDTO,
}
}
if len(availableQtyMap) == 0 {
return nil
}
result := make([]AvailableQtyDTO, 0, len(availableQtyMap))
for _, v := range availableQtyMap {
result = append(result, v)
}
return result
}
func toAvailableQtyDTOsFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO {
if len(availableQtysRaw) == 0 {
return nil
}
result := make([]AvailableQtyDTO, len(availableQtysRaw))
for i, v := range availableQtysRaw {
pwData, ok := v["product_warehouse"].(map[string]interface{})
if !ok {
continue
}
pwDTO := toProductWarehouseDTO(pwData)
availableQty := 0.0
if qty, ok := v["available_qty"].(float64); ok {
availableQty = qty
}
result[i] = AvailableQtyDTO{
AvailableQty: availableQty,
ProductWarehouse: pwDTO,
}
}
return result
}
@@ -0,0 +1,43 @@
package project_flock_kandangs
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProjectFlockKandangModule struct{}
func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlockKandang.NewProjectFlockPopulationRepository(db)
userRepo := rUser.NewUserRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
// register workflow steps for project flock kandang approvals
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
}
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
}
@@ -0,0 +1,26 @@
package project_flock_kandangs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/controllers"
projectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlockKandang.ProjectFlockKandangService) {
ctrl := controller.NewProjectFlockKandangController(s)
route := v1.Group("/project-flock-kandangs")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,195 @@
package service
import (
"errors"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ProjectFlockKandangService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error)
}
type projectFlockKandangService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
PopulationRepo repository.ProjectFlockPopulationRepository
}
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
return &projectFlockKandangService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
PopulationRepo: populationRepo,
}
}
func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
projectFlockKandangs, err := s.Repository.GetAll(c.Context())
if err != nil {
s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err)
return nil, 0, err
}
total := int64(len(projectFlockKandangs))
return projectFlockKandangs, total, nil
}
func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) {
projectFlockKandang, err := s.Repository.GetByID(c.Context(), id)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
if err != nil {
s.Log.Errorf("Failed get projectFlockKandang by id: %+v", err)
return nil, nil, err
}
if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil {
latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
if err != nil {
s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
}
if latest != nil {
projectFlockKandang.LatestApproval = latest
}
}
availableQtys, err := s.getAvailableQuantities(c, projectFlockKandang)
if err != nil {
s.Log.Errorf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandang.Kandang.Id, err)
availableQtys = nil
}
return projectFlockKandang, availableQtys, nil
}
func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) ([]map[string]interface{}, error) {
if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil {
return nil, nil
}
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.Kandang.Id)
if err != nil || warehouse == nil {
return nil, nil
}
var productCategoryCode string
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
productCategoryCode = "DOC"
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
productCategoryCode = "PULLET"
} else {
return nil, nil
}
products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id)
if err != nil || len(products) == 0 {
return nil, nil
}
var result []map[string]interface{}
for _, pw := range products {
availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
if err != nil {
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
}
// Only include product warehouse if available_qty > 0
if availableQty <= 0 {
continue
}
productData := map[string]interface{}{
"id": pw.Product.Id,
"name": pw.Product.Name,
}
warehouseData := map[string]interface{}{
"id": pw.Warehouse.Id,
"name": pw.Warehouse.Name,
"type": pw.Warehouse.Type,
}
productWarehouseData := map[string]interface{}{
"id": pw.Id,
"product": productData,
"warehouse": warehouseData,
}
result = append(result, map[string]interface{}{
"available_qty": availableQty,
"product_warehouse": productWarehouseData,
})
}
return result, nil
}
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
availableQty := productWarehouse.Quantity
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
var totalPendingQty float64
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
availableQty = productWarehouse.Quantity - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
var totalPopulation float64
var totalPendingQty float64
populations, err := s.PopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(c.Context(), projectFlockKandang.Id, productWarehouse.Id)
if err == nil {
for _, pop := range populations {
totalPopulation += pop.TotalQty
}
}
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
}
return availableQty, nil
}
@@ -0,0 +1,17 @@
package validation
type Create struct {
ProjectFlockId uint `json:"project_flock_id" validate:"required"`
KandangId uint `json:"kandang_id" validate:"required"`
}
type Update struct {
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -85,6 +85,17 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
return err
}
var periodMap map[uint]int
if len(result) > 0 {
ids := make([]uint, len(result))
for i, item := range result {
ids[i] = item.Id
}
if periods, err := u.ProjectflockService.GetProjectPeriods(c, ids); err == nil {
periodMap = periods
}
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProjectFlockListDTO]{
Code: fiber.StatusOK,
@@ -96,7 +107,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProjectFlockListDTOs(result),
Data: dto.ToProjectFlockListDTOsWithPeriods(result, periodMap),
})
}
@@ -113,12 +124,19 @@ func (u *ProjectflockController) GetOne(c *fiber.Ctx) error {
return err
}
var period int
if periods, err := u.ProjectflockService.GetProjectPeriods(c, []uint{uint(id)}); err == nil {
if p, ok := periods[uint(id)]; ok {
period = p
}
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get projectflock successfully",
Data: dto.ToProjectFlockListDTO(*result),
Data: dto.ToProjectFlockListDTOWithPeriod(*result, period),
})
}
@@ -205,11 +223,29 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
data interface{}
message = "Submit projectflock approval successfully"
)
var periodMap map[uint]int
if len(results) > 0 {
ids := make([]uint, len(results))
for i, item := range results {
ids[i] = item.Id
}
if periods, err := u.ProjectflockService.GetProjectPeriods(c, ids); err == nil {
periodMap = periods
}
}
if len(results) == 1 {
data = dto.ToProjectFlockListDTO(results[0])
period := 0
if periodMap != nil {
if p, ok := periodMap[results[0].Id]; ok {
period = p
}
}
data = dto.ToProjectFlockListDTOWithPeriod(results[0], period)
} else {
message = "Submit projectflock approvals successfully"
data = dto.ToProjectFlockListDTOs(results)
data = dto.ToProjectFlockListDTOsWithPeriods(results, periodMap)
}
return c.Status(fiber.StatusOK).
@@ -222,25 +258,32 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
}
func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
param := c.Params("project_flock_kandang_id")
param := c.Params("location_id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id")
}
summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id))
summaries, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id))
if err != nil {
return err
}
responseBody := dto.ToFlockPeriodSummaryDTO(summary.Flock, summary.NextPeriod)
responseBody := make([]dto.KandangPeriodSummaryDTO, 0, len(summaries))
for _, item := range summaries {
responseBody = append(responseBody, dto.KandangPeriodSummaryDTO{
Id: item.Id,
Name: item.Name,
Period: item.Period,
})
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get flock period summary successfully",
Message: "Get kandang period summary successfully",
Data: responseBody,
})
}
@@ -10,6 +10,7 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -22,6 +23,13 @@ type ProjectFlockBaseDTO struct {
FlockName string `json:"flock_name"`
}
type KandangWithProjectFlockIdDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
}
type ProjectFlockListDTO struct {
ProjectFlockBaseDTO
// Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
@@ -45,7 +53,13 @@ type FlockPeriodDTO struct {
NextPeriod int `json:"next_period"`
}
func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
type KandangPeriodSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Period int `json:"period"`
}
func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectFlockListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
@@ -91,31 +105,49 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
}
return ProjectFlockListDTO{
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e, period),
// Flock: flockSummary,
Area: areaSummary,
Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Approval: latestApproval,
Area: areaSummary,
Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Approval: latestApproval,
}
}
func ToProjectFlockListDTOs(items []entity.ProjectFlock) []ProjectFlockListDTO {
result := make([]ProjectFlockListDTO, len(items))
for i, item := range items {
result[i] = ToProjectFlockListDTO(item)
result[i] = ToProjectFlockListDTOWithPeriod(item, 0)
}
return result
}
func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
return ToProjectFlockListDTOWithPeriod(e, 0)
}
func ToProjectFlockListDTOsWithPeriods(items []entity.ProjectFlock, periods map[uint]int) []ProjectFlockListDTO {
result := make([]ProjectFlockListDTO, len(items))
for i, item := range items {
p := 0
if periods != nil {
if v, ok := periods[item.Id]; ok {
p = v
}
}
result[i] = ToProjectFlockListDTOWithPeriod(item, p)
}
return result
}
func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO {
return ProjectFlockDetailDTO{
ProjectFlockListDTO: ToProjectFlockListDTO(e),
ProjectFlockListDTO: ToProjectFlockListDTOWithPeriod(e, 0),
}
}
@@ -144,10 +176,10 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv
return result
}
func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
func createProjectFlockBaseDTO(e entity.ProjectFlock, period int) ProjectFlockBaseDTO {
return ProjectFlockBaseDTO{
Id: e.Id,
Period: e.Period,
Period: period,
FlockName: e.FlockName,
}
}
@@ -50,7 +50,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
pfLocal := ProjectFlockWithPivotDTO{
ProjectFlockBaseDTO: ProjectFlockBaseDTO{
Id: e.ProjectFlock.Id,
Period: e.ProjectFlock.Period,
Period: e.Period,
FlockName: e.ProjectFlock.FlockName,
},
Category: e.ProjectFlock.Category,
@@ -9,8 +9,19 @@ import (
)
type ProjectFlockPopulationRepository interface {
repository.BaseRepository[entity.ProjectFlockPopulation]
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error)
// domain-specific
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error)
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
// subset of base repository methods used by services
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
// transaction helpers
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
DB() *gorm.DB
}
type projectFlockPopulationRepositoryImpl struct {
@@ -23,13 +34,60 @@ func NewProjectFlockPopulationRepository(db *gorm.DB) ProjectFlockPopulationRepo
}
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) {
var record entity.ProjectFlockPopulation
func (r *projectFlockPopulationRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockPopulationRepository {
return &projectFlockPopulationRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockPopulation](tx),
}
}
func (r *projectFlockPopulationRepositoryImpl) DB() *gorm.DB {
return r.BaseRepositoryImpl.DB()
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) {
var records []entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
First(&record).Error
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Preload("ProjectChickin").
Find(&records).Error
if err != nil {
return nil, err
}
return &record, nil
return records, nil
}
func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) {
var count int64
err := r.DB().WithContext(ctx).
Where("project_chickin_id = ?", projectChickinID).
Model(&entity.ProjectFlockPopulation{}).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) {
var records []entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx).
Where("project_chickin_id = ? AND product_warehouse_id = ?", projectChickinID, productWarehouseID).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) {
var records []entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx).
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ? AND project_flock_populations.product_warehouse_id = ?", projectFlockKandangID, productWarehouseID).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
@@ -2,7 +2,6 @@ package repository
import (
"context"
"errors"
"fmt"
"strings"
@@ -10,17 +9,12 @@ import (
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/clause"
)
const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))"
type ProjectflockRepository interface {
repository.BaseRepository[entity.ProjectFlock]
GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error)
GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error)
GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
GetNextSequenceForBase(ctx context.Context, baseName string) (int, error)
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error)
WithDefaultRelations() func(*gorm.DB) *gorm.DB
ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error)
@@ -39,65 +33,6 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
}
}
func (r *ProjectflockRepositoryImpl) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) {
var records []entity.ProjectFlock
if err := r.DB().WithContext(ctx).
Unscoped().
Where(baseNameExpression+" = LOWER(?)", baseName).
Order("period ASC").
Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}
func (r *ProjectflockRepositoryImpl) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) {
var record entity.ProjectFlock
err := r.DB().WithContext(ctx).
Where(baseNameExpression+" = LOWER(?)", baseName).
Order("period DESC").
First(&record).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if err != nil {
return nil, err
}
return &record, nil
}
func (r *ProjectflockRepositoryImpl) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
var max int
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlock{}).
Where(baseNameExpression+" = LOWER(?)", baseName).
Select("COALESCE(MAX(period), 0)").
Scan(&max).Error; err != nil {
return 0, err
}
return max, nil
}
func (r *ProjectflockRepositoryImpl) GetNextSequenceForBase(ctx context.Context, baseName string) (int, error) {
var payload struct {
Period int
}
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlock{}).
Where(baseNameExpression+" = LOWER(?)", baseName).
Clauses(clause.Locking{Strength: "UPDATE"}).
Order("period DESC").
Limit(1).
Select("period").
Scan(&payload).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 1, nil
}
return 0, err
}
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)
@@ -132,7 +67,13 @@ func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *vali
db = db.Where("project_flocks.location_id = ?", params.LocationId)
}
if params.Period > 0 {
db = db.Where("project_flocks.period = ?", params.Period)
db = db.Where(`
EXISTS (
SELECT 1
FROM project_flock_kandangs pfk
WHERE pfk.project_flock_id = project_flocks.id
AND pfk.period = ?
)`, params.Period)
}
if len(params.KandangIds) > 0 {
db = db.Where(`
@@ -179,10 +120,15 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
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 project_flock_kandangs
WHERE project_flock_kandangs.project_flock_id = project_flocks.id
AND LOWER(CAST(project_flock_kandangs.period AS TEXT)) LIKE ?
)
OR EXISTS (
SELECT 1 FROM kandangs
WHERE kandangs.project_flock_id = project_flocks.id
JOIN project_flock_kandangs pfk ON pfk.kandang_id = kandangs.id
WHERE pfk.project_flock_id = project_flocks.id
AND LOWER(kandangs.name) LIKE ?
)
`,
@@ -236,7 +182,7 @@ func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder str
}
case "period":
return []string{
fmt.Sprintf("project_flocks.period %s", direction),
fmt.Sprintf("(SELECT COALESCE(MAX(period), 0) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
default:
@@ -4,6 +4,7 @@ import (
"context"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
@@ -18,7 +19,9 @@ type ProjectFlockKandangRepository interface {
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)
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
IdExists(ctx context.Context, id uint) (bool, error)
DB() *gorm.DB
}
@@ -59,6 +62,9 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse").
Order("project_flock_id ASC, created_at ASC").
Find(&records).Error; err != nil {
return nil, err
@@ -73,6 +79,9 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand
func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB {
return r.db
}
func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id)
}
func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
record := new(entity.ProjectFlockKandang)
@@ -85,6 +94,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse").
First(record, id).Error; err != nil {
return nil, err
}
@@ -103,6 +115,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse").
First(record).Error; err != nil {
return nil, err
}
@@ -163,7 +178,33 @@ func (r *projectFlockKandangRepositoryImpl) MaxPeriodByBaseName(ctx context.Cont
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)").
Select("COALESCE(MAX(pfk.period), 0)").
Scan(&max).Error
return max, err
}
func (r *projectFlockKandangRepositoryImpl) ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) {
result := make(map[uint]int)
if len(projectIDs) == 0 {
return result, nil
}
type row struct {
ProjectFlockID uint
Period int
}
var rows []row
if err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("project_flock_id IN ?", projectIDs).
Select("project_flock_id, COALESCE(MAX(period), 0) AS period").
Group("project_flock_id").
Scan(&rows).Error; err != nil {
return nil, err
}
for _, item := range rows {
result[item.ProjectFlockID] = item.Period
}
return result, nil
}
@@ -1,7 +1,7 @@
package project_flocks
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers"
projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
ctrl := controller.NewProjectflockController(s)
route := v1.Group("/project-flocks")
route.Use(m.Auth(u))
// route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -22,6 +22,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Delete("/:id", ctrl.DeleteOne)
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval)
route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
route.Get("/kandangs/:location_id/periods", ctrl.GetFlockPeriodSummary)
}
@@ -35,7 +35,8 @@ type ProjectflockService interface {
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, locationID uint) ([]KandangPeriodSummary, error)
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
}
@@ -52,9 +53,10 @@ type projectflockService struct {
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
type FlockPeriodSummary struct {
Flock entity.Flock
NextPeriod int
type KandangPeriodSummary struct {
Id uint
Name string
Period int
}
func NewProjectflockService(
@@ -81,6 +83,18 @@ func NewProjectflockService(
}
}
func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Flock").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs").
Preload("KandangHistory").
Preload("KandangHistory.Kandang")
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
@@ -206,6 +220,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
if len(kandangs) != len(kandangIDs) {
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
}
for _, kandang := range kandangs {
if kandang.LocationId != req.LocationId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
}
}
// larang kalau ada yg sudah terikat ke project lain
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
@@ -224,22 +243,24 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction)
nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), canonicalBase)
if err != nil {
return err
}
generatedName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, nextSeq, nil)
// Generate unique flock name (sequential per base name, starting from 1)
generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil)
if err != nil {
return err
}
createBody.FlockName = generatedName
createBody.Period = seq
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs); err != nil {
// Compute period based on location history (max period in that location + 1),
// and store it on project_flock_kandangs only.
nextPeriod, err := s.nextLocationPeriod(c.Context(), dbTransaction, req.LocationId)
if err != nil {
return err
}
if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, nextPeriod); err != nil {
return err
}
@@ -375,6 +396,15 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
if len(kandangs) != len(newKandangIDs) {
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
}
targetLocationID := existing.LocationId
if req.LocationId != nil && *req.LocationId > 0 {
targetLocationID = *req.LocationId
}
for _, kandang := range kandangs {
if kandang.LocationId != targetLocationID {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
}
}
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
} else if linked {
@@ -400,18 +430,11 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
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)
newName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, 1, &id)
if err != nil {
return err
}
updateBody["flock_name"] = newName
if seq != existing.Period {
updateBody["period"] = seq
}
}
if len(updateBody) > 0 {
@@ -455,7 +478,19 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
if len(toAttach) > 0 {
if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach); err != nil {
var currentPeriod int
if err := dbTransaction.WithContext(c.Context()).
Table("project_flock_kandangs").
Where("project_flock_id = ?", id).
Select("COALESCE(MAX(period), 0)").
Scan(&currentPeriod).Error; err != nil {
return err
}
if currentPeriod <= 0 {
currentPeriod = 1
}
if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, currentPeriod); err != nil {
return err
}
}
@@ -731,57 +766,90 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
return total, nil
}
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, projectFlockKandangID uint) (*FlockPeriodSummary, error) {
if projectFlockKandangID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
// nextLocationPeriod computes the next period number for a given location
// based on the maximum period that has ever been used by any kandang in that location.
func (s projectflockService) nextLocationPeriod(ctx context.Context, tx *gorm.DB, locationID uint) (int, error) {
if locationID == 0 {
return 0, fiber.NewError(fiber.StatusBadRequest, "location_id is required to compute period")
}
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")
db := s.Repository.DB()
if tx != nil {
db = tx
}
var maxPeriod int
if err := db.WithContext(ctx).
Table("project_flock_kandangs pfk").
Joins("JOIN kandangs k ON k.id = pfk.kandang_id").
Where("k.location_id = ?", locationID).
Select("COALESCE(MAX(pfk.period), 0)").
Scan(&maxPeriod).Error; err != nil {
s.Log.Errorf("Failed to compute max period for location %d: %+v", locationID, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute period for location")
}
return maxPeriod + 1, nil
}
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
if len(projectIDs) == 0 {
return map[uint]int{}, nil
}
return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs)
}
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) {
if locationID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
}
exists, err := s.Repository.LocationExists(c.Context(), locationID)
if err != nil {
s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", projectFlockKandangID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
s.Log.Errorf("Failed to validate location %d: %+v", locationID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate location")
}
if !exists {
return nil, fiber.NewError(fiber.StatusNotFound, "Location not found")
}
var baseName string
var referenceFlock *entity.Flock
if pivot.ProjectFlock.Id != 0 {
baseName = pfutils.DeriveBaseName(pivot.ProjectFlock.FlockName)
type kandangPeriodRow struct {
Id uint
Name string
LatestPeriod int
}
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")
}
var rows []kandangPeriodRow
db := s.Repository.DB().WithContext(c.Context())
if err := db.
Table("kandangs AS k").
Select("k.id, k.name, COALESCE(MAX(pfk.period), 0) AS latest_period").
Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.kandang_id = k.id").
Where("k.location_id = ?", locationID).
Where("k.deleted_at IS NULL").
Group("k.id, k.name").
Order("k.id ASC").
Scan(&rows).Error; err != nil {
s.Log.Errorf("Failed to fetch kandang period summary for location %d: %+v", locationID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang period summary")
}
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
summaries := make([]KandangPeriodSummary, 0, len(rows))
for _, row := range rows {
nextPeriod := 0
if row.LatestPeriod > 0 {
nextPeriod = row.LatestPeriod + 1
}
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
}
summaries = append(summaries, KandangPeriodSummary{
Id: row.Id,
Name: row.Name,
Period: nextPeriod,
})
}
return &FlockPeriodSummary{
Flock: *referenceFlock,
NextPeriod: maxPeriod + 1,
}, nil
return summaries, nil
}
func uniqueUintSlice(values []uint) []uint {
@@ -857,7 +925,7 @@ func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint
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, period int) error {
if len(kandangIDs) == 0 {
return nil
}
@@ -895,6 +963,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
records = append(records, &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: id,
Period: period,
})
}
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
@@ -2,10 +2,12 @@ package dto
import (
"math"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/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"
@@ -17,19 +19,19 @@ type RecordingBaseDTO struct {
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
RecordDatetime time.Time `json:"record_datetime"`
Day *int `json:"day,omitempty"`
ProjectFlockCategory *string `json:"project_flock_category,omitempty"`
TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"`
CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"`
DailyGain *float64 `json:"daily_gain,omitempty"`
AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"`
CumIntake *int `json:"cum_intake,omitempty"`
FcrValue *float64 `json:"fcr_value,omitempty"`
TotalChickQty *float64 `json:"total_chick_qty,omitempty"`
Day int `json:"day"`
ProjectFlockCategory string `json:"project_flock_category"`
TotalDepletionQty float64 `json:"total_depletion_qty"`
CumDepletionRate float64 `json:"cum_depletion_rate"`
DailyGain float64 `json:"daily_gain"`
AvgDailyGain float64 `json:"avg_daily_gain"`
CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"`
TotalChickQty float64 `json:"total_chick_qty"`
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
EggGradingStatus *string `json:"egg_grading_status,omitempty"`
EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"`
EggGradingCompletedQty *int `json:"egg_grading_completed_qty,omitempty"`
EggGradingStatus *string `json:"egg_grading_status"`
EggGradingPendingQty *int `json:"egg_grading_pending_qty"`
EggGradingCompletedQty *int `json:"egg_grading_completed_qty"`
}
type RecordingListDTO struct {
@@ -54,31 +56,24 @@ type RecordingBodyWeightDTO struct {
}
type RecordingDepletionDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"`
Qty float64 `json:"qty"`
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
ProductWarehouseId uint `json:"product_warehouse_id"`
Qty float64 `json:"qty"`
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
}
type RecordingStockDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageAmount *float64 `json:"usage_amount,omitempty"`
PendingQty *float64 `json:"pending_qty,omitempty"`
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageAmount float64 `json:"usage_amount"`
PendingQty *float64 `json:"pending_qty,omitempty"`
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
}
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"`
Id uint `json:"id"`
ProductWarehouseId uint `json:"product_warehouse_id"`
Qty int `json:"qty"`
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"`
}
type RecordingEggGradingDTO struct {
@@ -89,12 +84,46 @@ type RecordingEggGradingDTO struct {
// === Mapper Functions ===
func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
var projectFlockCategory *string
var (
projectFlockCategory string
day int
totalDepletionQty float64
cumDepletionRate float64
dailyGain float64
avgDailyGain float64
cumIntake int
fcrValue float64
totalChickQty float64
)
if e.Day != nil {
day = *e.Day
}
if e.TotalDepletionQty != nil {
totalDepletionQty = *e.TotalDepletionQty
}
if e.CumDepletionRate != nil {
cumDepletionRate = *e.CumDepletionRate
}
if e.DailyGain != nil {
dailyGain = *e.DailyGain
}
if e.AvgDailyGain != nil {
avgDailyGain = *e.AvgDailyGain
}
if e.CumIntake != nil {
cumIntake = *e.CumIntake
}
if e.FcrValue != nil {
fcrValue = *e.FcrValue
}
if e.TotalChickQty != nil {
totalChickQty = *e.TotalChickQty
}
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
category := e.ProjectFlockKandang.ProjectFlock.Category
if category != "" {
projectFlockCategory = &category
}
projectFlockCategory = category
}
latestApproval := defaultRecordingLatestApproval(e)
@@ -109,15 +138,15 @@ func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
RecordDatetime: e.RecordDatetime,
Day: e.Day,
Day: day,
ProjectFlockCategory: projectFlockCategory,
TotalDepletionQty: e.TotalDepletionQty,
CumDepletionRate: e.CumDepletionRate,
DailyGain: e.DailyGain,
AvgDailyGain: e.AvgDailyGain,
CumIntake: e.CumIntake,
FcrValue: e.FcrValue,
TotalChickQty: e.TotalChickQty,
TotalDepletionQty: totalDepletionQty,
CumDepletionRate: cumDepletionRate,
DailyGain: dailyGain,
AvgDailyGain: avgDailyGain,
CumIntake: cumIntake,
FcrValue: fcrValue,
TotalChickQty: totalChickQty,
Approval: latestApproval,
EggGradingStatus: gradingStatus,
EggGradingPendingQty: gradingPending,
@@ -149,12 +178,21 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
}
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
listDTO := ToRecordingListDTO(e)
var eggs []RecordingEggDTO
if strings.EqualFold(listDTO.ProjectFlockCategory, string(utils.ProjectFlockCategoryLaying)) {
eggs = ToRecordingEggDTOs(e.Eggs)
} else if len(e.Eggs) > 0 {
eggs = ToRecordingEggDTOs(e.Eggs)
}
return RecordingDetailDTO{
RecordingListDTO: ToRecordingListDTO(e),
RecordingListDTO: listDTO,
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: ToRecordingEggDTOs(e.Eggs),
Eggs: eggs,
}
}
@@ -176,7 +214,7 @@ func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []Recordin
result[i] = RecordingDepletionDTO{
ProductWarehouseId: d.ProductWarehouseId,
Qty: d.Qty,
ProductWarehouse: toRecordingProductWarehouseDTO(&d.ProductWarehouse),
ProductWarehouse: mapProductWarehouseDTO(&d.ProductWarehouse),
}
}
return result
@@ -185,11 +223,16 @@ func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []Recordin
func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
result := make([]RecordingStockDTO, len(stocks))
for i, s := range stocks {
var usageAmount float64
if s.UsageQty != nil {
usageAmount = *s.UsageQty
}
result[i] = RecordingStockDTO{
ProductWarehouseId: s.ProductWarehouseId,
UsageAmount: s.UsageQty,
UsageAmount: usageAmount,
PendingQty: s.PendingQty,
ProductWarehouse: toRecordingProductWarehouseDTO(&s.ProductWarehouse),
ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse),
}
}
return result
@@ -199,9 +242,10 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
result := make([]RecordingEggDTO, len(eggs))
for i, egg := range eggs {
result[i] = RecordingEggDTO{
Id: egg.Id,
ProductWarehouseId: egg.ProductWarehouseId,
Qty: egg.Qty,
ProductWarehouse: toRecordingProductWarehouseDTO(&egg.ProductWarehouse),
ProductWarehouse: mapProductWarehouseDTO(&egg.ProductWarehouse),
Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs),
}
}
@@ -224,25 +268,17 @@ func ToRecordingEggGradingDTOs(gradings []entity.GradingEgg) []RecordingEggGradi
return result
}
func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProductWarehouseDTO {
if pw == nil || pw.Id == 0 {
return nil
func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO {
if pw == nil {
return productWarehouseDTO.ProductWarehouseDTO{}
}
dto := RecordingProductWarehouseDTO{
Id: pw.Id,
ProductId: pw.ProductId,
WarehouseId: pw.WarehouseId,
mapped := productWarehouseDTO.ToProductWarehouseDTO(pw)
if mapped == nil {
return productWarehouseDTO.ProductWarehouseDTO{}
}
if pw.Product.Id != 0 {
dto.ProductName = pw.Product.Name
}
if pw.Warehouse.Id != 0 {
dto.WarehouseName = pw.Warehouse.Name
}
return &dto
return *mapped
}
const goodEggProductWarehouseID uint = 5
@@ -254,18 +254,22 @@ func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFloc
}
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
var population entity.ProjectFlockPopulation
var total float64
err := tx.
Where("project_flock_kandang_id = ?", projectFlockKandangId).
Order("created_at DESC").
First(&population).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil
}
Table("project_flock_populations").
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId).
Scan(&total).Error
if err != nil {
return 0, err
}
return int64(math.Round(population.InitialQuantity)), nil
if total < 0 {
total = 0
}
return int64(math.Round(total)), nil
}
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
@@ -261,6 +261,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return nil, err
}
if req.BodyWeights == nil && req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
return s.GetOne(c, id)
}
ctx := c.Context()
var recordingEntity *entity.Recording
@@ -277,12 +281,21 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
recordingEntity = recording
hasBodyChanges := req.BodyWeights != nil
hasStockChanges := req.Stocks != nil
hasDepletionChanges := req.Depletions != nil
hasEggChanges := req.Eggs != nil
if !hasBodyChanges && !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
return nil
}
var category string
if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 {
category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category)
}
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
if req.Eggs != nil {
if hasEggChanges {
if !isLaying && len(req.Eggs) > 0 {
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
}
@@ -291,7 +304,29 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
}
if req.BodyWeights != nil {
if hasStockChanges {
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
return err
}
}
if hasDepletionChanges || hasEggChanges {
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil {
return err
}
}
hasExistingGradings := false
for _, egg := range recordingEntity.Eggs {
if len(egg.GradingEggs) > 0 {
hasExistingGradings = true
break
}
}
hasEggsAfterUpdate := len(recordingEntity.Eggs) > 0
if hasBodyChanges {
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear body weights: %+v", err)
return err
@@ -302,11 +337,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
}
if req.Stocks != nil {
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
return err
}
if hasStockChanges {
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing stocks: %+v", err)
@@ -330,17 +361,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
}
if req.Eggs != nil && req.Depletions == nil {
if err := s.ensureProductWarehousesExist(c, nil, nil, req.Eggs); err != nil {
return err
}
}
if req.Depletions != nil {
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil {
return err
}
if hasDepletionChanges {
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing depletions: %+v", err)
@@ -364,7 +385,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
}
if req.Eggs != nil {
if hasEggChanges {
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing eggs: %+v", err)
@@ -386,17 +407,71 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
return err
}
hasExistingGradings = false
hasEggsAfterUpdate = len(req.Eggs) > 0
}
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
return err
if hasBodyChanges || hasStockChanges || hasDepletionChanges {
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
return err
}
}
action := entity.ApprovalActionUpdated
if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, utils.RecordingStepPengajuan, action, recordingEntity.CreatedBy, nil); err != nil {
s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err)
return err
actorID := recordingEntity.CreatedBy
if actorID == 0 {
actorID = 1
}
var step approvalutils.ApprovalStep
if isLaying {
if !hasEggsAfterUpdate {
step = utils.RecordingStepGradingTelur
} else if hasEggChanges {
step = utils.RecordingStepGradingTelur
} else if hasExistingGradings {
step = utils.RecordingStepPengajuan
} else {
step = utils.RecordingStepGradingTelur
}
} else {
step = utils.RecordingStepPengajuan
}
latestApproval := recordingEntity.LatestApproval
if latestApproval == nil {
if s.ApprovalSvc != nil {
if fetched, fetchErr := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, recordingEntity.Id, nil); fetchErr != nil {
s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
return fetchErr
} else {
latestApproval = fetched
}
} else if s.ApprovalRepo != nil {
if fetched, fetchErr := s.ApprovalRepo.LatestByTarget(ctx, utils.ApprovalWorkflowRecording.String(), recordingEntity.Id, nil); fetchErr != nil {
s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
return fetchErr
} else {
latestApproval = fetched
}
}
}
shouldCreateApproval := true
if latestApproval != nil &&
latestApproval.StepNumber == uint16(step) &&
latestApproval.Action != nil &&
*latestApproval.Action == action {
shouldCreateApproval = false
}
if shouldCreateApproval {
if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, step, action, actorID, nil); err != nil {
s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err)
return err
}
}
return nil
@@ -1015,13 +1090,21 @@ func (s *recordingService) ensureChickInExists(ctx context.Context, projectFlock
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
}
_, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err == nil {
return nil
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err != nil {
s.Log.Errorf("Failed to check project flock population for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa data chick in")
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum melakukan chick in sehingga belum dapat membuat recording")
if len(populations) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum memiliki chick in yang disetujui sehingga belum dapat membuat recording")
}
s.Log.Errorf("Failed to check project flock population for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa data chick in")
for _, population := range populations {
if population.TotalQty > 0 {
return nil
}
}
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
}
@@ -4,12 +4,12 @@ type (
BodyWeight struct {
AvgWeight float64 `json:"avg_weight" validate:"required"`
Qty float64 `json:"qty" validate:"required,gt=0"`
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gt=0"`
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gte=0"`
}
Stock struct {
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
Qty *float64 `json:"qty,omitempty" validate:"required_without=UsageAmount,gte=0"`
Qty float64 `json:"qty" validate:"required,gte=0"`
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
}
@@ -26,10 +26,10 @@ type (
type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
BodyWeights []BodyWeight `json:"body_weights" validate:"dive"`
Stocks []Stock `json:"stocks" validate:"dive"`
Depletions []Depletion `json:"depletions" validate:"dive"`
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
}
type Update struct {
+5 -1
View File
@@ -10,6 +10,8 @@ import (
chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins"
projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks"
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings"
projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs"
// MODULE IMPORTS
)
@@ -20,8 +22,10 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
projectflocks.ProjectflockModule{},
recordings.RecordingModule{},
chickins.ChickinModule{},
transferLayings.TransferLayingModule{},
projectFlockKandangs.ProjectFlockKandangModule{},
// MODULE REGISTRY
}
}
for _, m := range allModules {
m.RegisterRoutes(group, db, validate)
@@ -0,0 +1,182 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransferLayingController struct {
TransferLayingService service.TransferLayingService
}
func NewTransferLayingController(transferLayingService service.TransferLayingService) *TransferLayingController {
return &TransferLayingController{
TransferLayingService: transferLayingService,
}
}
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)),
TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)),
TransferDateFrom: c.Query("transfer_date_from", ""),
TransferDateTo: c.Query("transfer_date_to", ""),
ApprovalStatus: c.Query("approval_status", ""),
TransferNumber: c.Query("transfer_number", ""),
Sort: c.Query("sort", "created_at"),
Order: c.Query("order", "desc"),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.TransferLayingService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transferLayings successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransferLayingListDTOs(result),
})
}
func (u *TransferLayingController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, approval, err := u.TransferLayingService.GetOneWithApproval(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transferLaying successfully",
Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, approval),
})
}
func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.TransferLayingService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.TransferLayingService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
func (u *TransferLayingController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.TransferLayingService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete transferLaying successfully",
})
}
func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
results, err := u.TransferLayingService.Approval(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit transfer laying approval successfully"
)
if len(results) == 1 {
data = dto.ToTransferLayingListDTO(results[0])
} else {
message = "Submit transfer laying approvals successfully"
data = dto.ToTransferLayingListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
})
}
@@ -0,0 +1,253 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type TransferLayingBaseDTO struct {
Id uint `json:"id"`
TransferNumber string `json:"transfer_number"`
TransferDate time.Time `json:"transfer_date"`
Notes string `json:"notes"`
}
type ProjectFlockSummaryDTO struct {
Id uint `json:"id"`
Category string `json:"category"`
}
type ProductSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type WarehouseSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
type ProductWarehouseSummaryDTO struct {
Product *ProductSummaryDTO `json:"product,omitempty"`
Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"`
}
type ProjectFlockKandangSummaryDTO struct {
Id uint `json:"id"`
KandangId uint `json:"kandang_id"`
}
type LayingTransferSourceDTO struct {
SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"`
Qty float64 `json:"qty"`
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
Note string `json:"note,omitempty"`
}
type LayingTransferTargetDTO struct {
TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"`
Qty float64 `json:"qty"`
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
Note string `json:"note,omitempty"`
}
type TransferLayingListDTO struct {
TransferLayingBaseDTO
FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"`
ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"`
PendingUsageQty *float64 `json:"pending_usage_qty"`
UsageQty *float64 `json:"usage_qty"`
CreatedBy uint `json:"created_by"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
type TransferLayingDetailDTO struct {
TransferLayingListDTO
Sources []LayingTransferSourceDTO `json:"sources,omitempty"`
Targets []LayingTransferTargetDTO `json:"targets,omitempty"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
// === Mapper Functions ===
func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO {
if pf == nil || pf.Id == 0 {
return nil
}
return &ProjectFlockSummaryDTO{
Id: pf.Id,
Category: pf.Category,
}
}
func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO {
if pfk == nil || pfk.Id == 0 {
return nil
}
return &ProjectFlockKandangSummaryDTO{
Id: pfk.Id,
KandangId: pfk.KandangId,
}
}
func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO {
if product == nil || product.Id == 0 {
return nil
}
return &ProductSummaryDTO{
Id: product.Id,
Name: product.Name,
}
}
func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO {
if warehouse == nil || warehouse.Id == 0 {
return nil
}
return &WarehouseSummaryDTO{
Id: warehouse.Id,
Name: warehouse.Name,
Type: warehouse.Type,
}
}
func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO {
if pw == nil || pw.Id == 0 {
return nil
}
return &ProductWarehouseSummaryDTO{
Product: ToProductSummaryDTO(&pw.Product),
Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse),
}
}
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
return LayingTransferSourceDTO{
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
Qty: source.Qty,
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
Note: source.Note,
}
}
func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingTransferSourceDTO {
if len(sources) == 0 {
return []LayingTransferSourceDTO{}
}
result := make([]LayingTransferSourceDTO, len(sources))
for i, s := range sources {
result[i] = ToLayingTransferSourceDTO(s)
}
return result
}
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
return LayingTransferTargetDTO{
TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang),
Qty: target.Qty,
ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse),
Note: target.Note,
}
}
func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingTransferTargetDTO {
if len(targets) == 0 {
return []LayingTransferTargetDTO{}
}
result := make([]LayingTransferTargetDTO, len(targets))
for i, t := range targets {
result[i] = ToLayingTransferTargetDTO(t)
}
return result
}
func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO {
return TransferLayingBaseDTO{
Id: e.Id,
TransferNumber: e.TransferNumber,
TransferDate: e.TransferDate,
Notes: e.Notes,
}
}
func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
return TransferLayingListDTO{
TransferLayingBaseDTO: ToTransferLayingBaseDTO(e),
FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock),
ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock),
PendingUsageQty: e.PendingUsageQty,
UsageQty: e.UsageQty,
CreatedBy: e.CreatedBy,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
}
}
func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
var latestApproval *approvalDTO.ApprovalBaseDTO
// Use LatestApproval from entity if available
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = &mapped
} else if len(approvals) > 0 {
// Fallback to approvals slice
latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1])
latestApproval = &latest
}
return TransferLayingDetailDTO{
TransferLayingListDTO: ToTransferLayingListDTO(e),
Sources: ToLayingTransferSourceDTOs(e.Sources),
Targets: ToLayingTransferTargetDTOs(e.Targets),
Approval: latestApproval,
}
}
func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO {
var mappedApproval *approvalDTO.ApprovalBaseDTO
// Prefer LatestApproval from entity
if e.LatestApproval != nil && e.LatestApproval.Id != 0 {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
mappedApproval = &mapped
} else if approval != nil && approval.Id != 0 {
// Fallback to passed approval parameter
mapped := approvalDTO.ToApprovalDTO(*approval)
mappedApproval = &mapped
}
return TransferLayingDetailDTO{
TransferLayingListDTO: ToTransferLayingListDTO(e),
Sources: ToLayingTransferSourceDTOs(e.Sources),
Targets: ToLayingTransferTargetDTOs(e.Targets),
Approval: mappedApproval,
}
}
func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingListDTO {
result := make([]TransferLayingListDTO, len(items))
for i, item := range items {
result[i] = ToTransferLayingListDTO(item)
}
return result
}
@@ -0,0 +1,53 @@
package transfer_layings
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type TransferLayingModule struct{}
func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rInventory.NewProductWarehouseRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register transfer to laying approval workflow: %v", err))
}
transferLayingService := sTransferLaying.NewTransferLayingService(
transferLayingRepo,
projectFlockRepo,
projectFlockKandangRepo,
projectFlockPopulationRepo,
productWarehouseRepo,
warehouseRepo,
approvalService,
validate,
)
userService := sUser.NewUserService(userRepo, validate)
TransferLayingRoutes(router, userService, transferLayingService)
}
@@ -0,0 +1,42 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type TransferLayingRepository interface {
repository.BaseRepository[entity.LayingTransfer]
GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error)
IdExists(ctx context.Context, id uint) (bool, error)
}
type TransferLayingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.LayingTransfer]
db *gorm.DB
}
func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository {
return &TransferLayingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransfer](db),
db: db,
}
}
func (r *TransferLayingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.LayingTransfer](ctx, r.db, id)
}
func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) {
var transfer entity.LayingTransfer
err := r.db.WithContext(ctx).
Where("transfer_number = ?", transferNumber).
Where("deleted_at IS NULL").
First(&transfer).Error
if err != nil {
return nil, err
}
return &transfer, nil
}
@@ -0,0 +1,38 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type LayingTransferSourceRepository interface {
repository.BaseRepository[entity.LayingTransferSource]
GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error)
}
type LayingTransferSourceRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.LayingTransferSource]
db *gorm.DB
}
func NewLayingTransferSourceRepository(db *gorm.DB) LayingTransferSourceRepository {
return &LayingTransferSourceRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferSource](db),
db: db,
}
}
func (r *LayingTransferSourceRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error) {
var sources []entity.LayingTransferSource
err := r.db.WithContext(ctx).
Where("laying_transfer_id = ?", layingTransferId).
Order("created_at DESC").
Find(&sources).Error
if err != nil {
return nil, err
}
return sources, nil
}
@@ -0,0 +1,38 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type LayingTransferTargetRepository interface {
repository.BaseRepository[entity.LayingTransferTarget]
GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error)
}
type LayingTransferTargetRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.LayingTransferTarget]
db *gorm.DB
}
func NewLayingTransferTargetRepository(db *gorm.DB) LayingTransferTargetRepository {
return &LayingTransferTargetRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferTarget](db),
db: db,
}
}
func (r *LayingTransferTargetRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error) {
var targets []entity.LayingTransferTarget
err := r.db.WithContext(ctx).
Where("laying_transfer_id = ?", layingTransferId).
Order("created_at DESC").
Find(&targets).Error
if err != nil {
return nil, err
}
return targets, nil
}
@@ -0,0 +1,30 @@
package transfer_layings
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/controllers"
transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.TransferLayingService) {
ctrl := controller.NewTransferLayingController(s)
route := v1.Group("/transfer_layings")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
// route.Post("/approval", m.Auth(u), ctrl.Approval)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval)
}
@@ -0,0 +1,727 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type TransferLayingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error)
GetOneWithApproval(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
}
type transferLayingService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.TransferLayingRepository
ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository
ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository
ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository
ProductWarehouseRepo rInventory.ProductWarehouseRepository
WarehouseRepo rWarehouse.WarehouseRepository
ApprovalService commonSvc.ApprovalService
}
func NewTransferLayingService(
repo repository.TransferLayingRepository,
projectFlockRepo ProjectFlockRepository.ProjectflockRepository,
projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository,
projectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository,
productWarehouseRepo rInventory.ProductWarehouseRepository,
warehouseRepo rWarehouse.WarehouseRepository,
approvalService commonSvc.ApprovalService,
validate *validator.Validate,
) TransferLayingService {
return &transferLayingService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ProductWarehouseRepo: productWarehouseRepo,
WarehouseRepo: warehouseRepo,
ApprovalService: approvalService,
}
}
func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Sources").
Preload("Sources.SourceProjectFlockKandang").
Preload("Sources.ProductWarehouse").
Preload("Sources.ProductWarehouse.Product").
Preload("Sources.ProductWarehouse.Warehouse").
Preload("Targets").
Preload("Targets.TargetProjectFlockKandang").
Preload("Targets.ProductWarehouse").
Preload("Targets.ProductWarehouse.Product").
Preload("Targets.ProductWarehouse.Warehouse")
}
func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.SourceProjectFlockId != 0 {
db = db.Where("from_project_flock_id = ?", params.SourceProjectFlockId)
}
if params.TargetProjectFlockId != 0 {
db = db.Where("to_project_flock_id = ?", params.TargetProjectFlockId)
}
if params.TransferDateFrom != "" {
db = db.Where("transfer_date >= ?", params.TransferDateFrom)
}
if params.TransferDateTo != "" {
db = db.Where("transfer_date <= ?", params.TransferDateTo)
}
if params.TransferNumber != "" {
db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%")
}
sortField := "created_at"
if params.Sort != "" {
sortField = params.Sort
}
sortOrder := "DESC"
if params.Order == "asc" {
sortOrder = "ASC"
}
db = db.Order(fmt.Sprintf("%s %s", sortField, sortOrder))
return db
})
if err != nil {
s.Log.Errorf("Failed to get transferLayings: %+v", err)
return nil, 0, err
}
if params.ApprovalStatus != "" {
var filtered []entity.LayingTransfer
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
for _, transfer := range transferLayings {
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil)
if err == nil && latestApproval != nil && latestApproval.Action != nil {
if string(*latestApproval.Action) == params.ApprovalStatus {
filtered = append(filtered, transfer)
}
}
}
transferLayings = filtered
}
return transferLayings, total, nil
}
func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) {
transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
if err != nil {
s.Log.Errorf("Failed get transferLaying by id: %+v", err)
return nil, err
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil)
if err == nil && latestApproval != nil {
transferLaying.LatestApproval = latestApproval
}
return transferLaying, nil
}
func (s transferLayingService) GetOneWithApproval(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) {
transferLaying, err := s.GetOne(c, id)
if err != nil {
return nil, nil, err
}
return transferLaying, transferLaying.LatestApproval, nil
}
func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate source project flock")
}
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Target Project Flock not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate target project flock")
}
for _, detail := range req.SourceKandangs {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil {
return nil, err
}
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get source project flock kandang")
}
if pfk.ProjectFlockId != req.SourceProjectFlockId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d does not belong to source project flock %d", detail.ProjectFlockKandangId, req.SourceProjectFlockId))
}
}
for _, detail := range req.TargetKandangs {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil {
return nil, err
}
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
}
if pfk.ProjectFlockId != req.TargetProjectFlockId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Target kandang %d does not belong to target project flock %d", detail.ProjectFlockKandangId, req.TargetProjectFlockId))
}
}
transferDate, err := utils.ParseDateString(req.TransferDate)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format")
}
var totalSourceQty, totalTargetQty float64
sourceWarehouseMap := make(map[uint]uint)
for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Source kandang quantity must be greater than 0")
}
totalSourceQty += sourceDetail.Quantity
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
if err != nil {
return nil, err
}
var totalPopulation float64
var productWarehouseId uint
for _, pop := range populations {
totalPopulation += pop.TotalQty
if productWarehouseId == 0 {
productWarehouseId = pop.ProductWarehouseId
}
}
if totalPopulation == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available for transfer", sourceDetail.ProjectFlockKandangId))
}
if totalPopulation < sourceDetail.Quantity {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity))
}
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
}
for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Target kandang quantity must be greater than 0")
}
totalTargetQty += targetDetail.Quantity
}
if totalSourceQty != totalTargetQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total source quantity (%f) must equal total target quantity (%f)", totalSourceQty, totalTargetQty))
}
transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano())
createBody := &entity.LayingTransfer{
TransferNumber: transferNumber,
Notes: req.Reason,
FromProjectFlockId: req.SourceProjectFlockId,
ToProjectFlockId: req.TargetProjectFlockId,
TransferDate: transferDate,
PendingUsageQty: &totalSourceQty,
CreatedBy: 1, //todo : harus diambil dari auth
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record")
}
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
for _, sourceDetail := range req.SourceKandangs {
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
source := entity.LayingTransferSource{
LayingTransferId: createBody.Id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
Qty: sourceDetail.Quantity,
ProductWarehouseId: &productWarehouseId,
}
if err := dbTransaction.Create(&source).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source")
}
if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reduce project flock population")
}
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("quantity - ?", sourceDetail.Quantity)}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity")
}
}
for _, targetDetail := range req.TargetKandangs {
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
}
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
}
target := entity.LayingTransferTarget{
LayingTransferId: createBody.Id,
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
Qty: targetDetail.Quantity,
ProductWarehouseId: &targetWarehouse.Id,
}
if err := dbTransaction.Create(&target).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
}
}
if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval")
}
return nil
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying")
}
return s.GetOne(c, createBody.Id)
}
func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
_, err := s.Repository.GetByID(c.Context(), id, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
}
if latestApproval != nil && latestApproval.Action != nil {
action := string(*latestApproval.Action)
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot update transfer laying with status %s", action))
}
}
updateBody := make(map[string]any)
if req.TransferDate != nil {
updateBody["transfer_date"] = *req.TransferDate
}
if req.Reason != nil {
updateBody["notes"] = *req.Reason
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
s.Log.Errorf("Failed to update transferLaying: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying")
}
return s.GetOne(c, id)
}
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
_, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return db.Preload("Sources.ProductWarehouse").Preload("Targets")
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
}
if latestApproval != nil && latestApproval.Action != nil {
action := string(*latestApproval.Action)
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action))
}
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
}
for _, source := range sources {
if source.ProductWarehouseId != nil && source.Qty > 0 {
if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{
"quantity": gorm.Expr("quantity + ?", source.Qty),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity")
}
}
}
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
for _, source := range sources {
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration")
}
remainingToRestore := source.Qty
for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- {
pop := populations[i]
restoreAmount := remainingToRestore
if remainingToRestore < pop.TotalQty {
restoreAmount = remainingToRestore
}
newQty := pop.TotalQty + restoreAmount
if err := projectFlockPopulationRepoTx.PatchOne(c.Context(), pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore population quantity")
}
remainingToRestore -= restoreAmount
}
}
if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return fiberErr
}
s.Log.Errorf("Failed to delete transferLaying: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
}
return nil
}
func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID := uint(1) // TODO: change from auth context
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected):
action = entity.ApprovalActionRejected
case string(entity.ApprovalActionApproved):
action = entity.ApprovalActionApproved
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
if len(approvableIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
step := utils.TransferToLayingStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.TransferToLayingStepDisetujui
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs {
transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID))
}
return err
}
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowTransferToLaying,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
if action == entity.ApprovalActionApproved && transfer.PendingUsageQty != nil && *transfer.PendingUsageQty > 0 {
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
}
targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer targets")
}
if len(sources) > 0 && len(targets) > 0 {
firstSource := sources[0]
if firstSource.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
}
sourceWarehouse, err := productWarehouseRepoTx.GetByID(c.Context(), *firstSource.ProductWarehouseId, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse")
}
for _, target := range targets {
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), target.TargetProjectFlockKandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
}
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
}
if _, err := s.getOrCreateProductWarehouse(
c.Context(),
dbTransaction,
sourceWarehouse.ProductId,
targetWarehouse.Id,
target.Qty,
actorID,
); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse")
}
}
}
usageQty := *transfer.PendingUsageQty
updateData := map[string]any{
"usage_qty": usageQty,
"pending_usage_qty": nil,
}
if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), approvableID, updateData, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status")
}
}
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
updated := make([]entity.LayingTransfer, 0, len(approvableIDs))
for _, approvableID := range approvableIDs {
transfer, err := s.GetOne(c, approvableID)
if err != nil {
return nil, err
}
updated = append(updated, *transfer)
}
return updated, nil
}
func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayingID uint, actorID uint) error {
if transferLayingID == 0 || actorID == 0 {
return nil
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
action := entity.ApprovalActionCreated
_, err := approvalSvc.CreateApproval(
ctx,
utils.ApprovalWorkflowTransferToLaying,
transferLayingID,
utils.TransferToLayingStepPengajuan,
&action,
actorID,
nil,
)
return err
}
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint) (*entity.ProductWarehouse, error) {
productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx)
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil && existing != nil {
if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"quantity": gorm.Expr("quantity + ?", quantity)}, nil); err != nil {
return nil, err
}
return existing, nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
newWarehouse := &entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouseID,
Quantity: quantity,
CreatedBy: actorID,
}
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
return nil, err
}
return newWarehouse, nil
}
func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToReduce float64) error {
populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err != nil {
return err
}
if len(populations) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "No populations found for reduction")
}
remainingToReduce := quantityToReduce
for i := len(populations) - 1; i >= 0; i-- {
if remainingToReduce <= 0 {
break
}
pop := populations[i]
reductionAmount := remainingToReduce
if pop.TotalQty < remainingToReduce {
reductionAmount = pop.TotalQty
}
newQty := pop.TotalQty - reductionAmount
if err := populationRepo.PatchOne(ctx, pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
return err
}
remainingToReduce -= reductionAmount
}
if remainingToReduce > 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient population to reduce. Still need to reduce: %.0f", remainingToReduce))
}
return nil
}
@@ -0,0 +1,44 @@
package validation
type SourceKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
}
type TargetKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
}
type Create struct {
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"`
TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"`
SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,dive,required"`
TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"`
Reason string `json:"reason" validate:"omitempty,max=1000"`
}
type Update struct {
TransferDate *string `json:"transfer_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Reason *string `json:"reason,omitempty" validate:"omitempty,max=1000"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
SourceProjectFlockId uint `query:"source_project_flock_id" validate:"omitempty"`
TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"`
TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"`
TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"`
ApprovalStatus string `query:"approval_status" validate:"omitempty,oneof=PENDING APPROVED REJECTED"` // Filter by latest approval status
TransferNumber string `query:"transfer_number" validate:"omitempty"` // Search by transfer number
Sort string `query:"sort" validate:"omitempty,oneof=created_at transfer_date"` // Sort by field
Order string `query:"order" validate:"omitempty,oneof=asc desc"` // Sort order
}
type Approve struct {
Action string `json:"action" validate:"required_strict"`
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
@@ -0,0 +1,237 @@
package controller
import (
"fmt"
"math"
"strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type PurchaseController struct {
service service.PurchaseService
}
func NewPurchaseController(s service.PurchaseService) *PurchaseController {
return &PurchaseController{service: s}
}
func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
query := &validation.PurchaseQuery{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: strings.TrimSpace(c.Query("search")),
PrNumber: strings.TrimSpace(c.Query("pr_number")),
CreatedFrom: strings.TrimSpace(c.Query("created_from")),
CreatedTo: strings.TrimSpace(c.Query("created_to")),
}
if supplierID := c.QueryInt("supplier_id", 0); supplierID > 0 {
query.SupplierID = uint(supplierID)
}
if status := strings.TrimSpace(c.Query("status")); status != "" {
query.Status = strings.ToUpper(status)
}
results, total, err := ctrl.service.GetAll(c, query)
if err != nil {
return err
}
limit := query.Limit
if limit <= 0 {
limit = 10
}
page := query.Page
if page <= 0 {
page = 1
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.PurchaseListItemDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase fetched successfully",
Meta: response.Meta{
Page: page,
Limit: limit,
TotalPages: int64(math.Ceil(float64(total) / float64(limit))),
TotalResults: total,
},
Data: dto.ToPurchaseListDTOs(results),
})
}
func (ctrl *PurchaseController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
result, err := ctrl.service.GetOne(c, id)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase fetched successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
req := new(validation.CreatePurchaseRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Purchase created successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
req := new(validation.ApproveStaffPurchaseRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
}
result, err := ctrl.service.ApproveStaffPurchase(c, id, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Staff purchase approval recorded successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
req := new(validation.ApproveManagerPurchaseRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.ApproveManagerPurchase(c, id, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Manager purchase approval recorded successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
req := new(validation.ReceivePurchaseRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.ReceiveProducts(c, id, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase receiving recorded successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
req := new(validation.DeletePurchaseItemsRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.DeleteItems(c, id, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase items deleted successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) DeletePurchase(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
if err := ctrl.service.DeletePurchase(c, id); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase deleted successfully",
Data: nil,
})
}
@@ -0,0 +1,173 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
)
type PurchaseListItemDTO struct {
Id uint64 `json:"id"`
PrNumber string `json:"pr_number"`
PoNumber *string `json:"po_number"`
Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"`
CreditTerm *int `json:"credit_term"`
DueDate *time.Time `json:"due_date"`
PoDate *time.Time `json:"po_date"`
GrandTotal float64 `json:"grand_total"`
Notes *string `json:"notes"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval"`
}
type PurchaseDetailDTO struct {
Id uint64 `json:"id"`
PrNumber string `json:"pr_number"`
PoNumber *string `json:"po_number"`
Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"`
CreditTerm *int `json:"credit_term"`
DueDate *time.Time `json:"due_date"`
PoDate *time.Time `json:"po_date"`
GrandTotal float64 `json:"grand_total"`
Notes *string `json:"notes"`
Items []PurchaseItemDTO `json:"items"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval"`
}
type PurchaseItemDTO struct {
Id uint64 `json:"id"`
ProductID uint64 `json:"product_id"`
Product *productDTO.ProductBaseDTO `json:"product"`
WarehouseID uint64 `json:"warehouse_id"`
Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse"`
ProductWarehouseID *uint64 `json:"product_warehouse_id"`
SubQty float64 `json:"sub_qty"`
TotalQty float64 `json:"total_qty"`
TotalUsed float64 `json:"total_used"`
Price float64 `json:"price"`
TotalPrice float64 `json:"total_price"`
ReceivedDate *time.Time `json:"received_date"`
TravelNumber *string `json:"travel_number"`
TravelDocumentPath *string `json:"travel_document_path"`
VehicleNumber *string `json:"vehicle_number"`
}
func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
dto := PurchaseItemDTO{
Id: item.Id,
ProductID: item.ProductId,
ProductWarehouseID: item.ProductWarehouseId,
WarehouseID: item.WarehouseId,
SubQty: item.SubQty,
TotalQty: item.TotalQty,
TotalUsed: item.TotalUsed,
Price: item.Price,
TotalPrice: item.TotalPrice,
ReceivedDate: item.ReceivedDate,
TravelNumber: item.TravelNumber,
TravelDocumentPath: item.TravelNumberDocs,
VehicleNumber: item.VehicleNumber,
}
if item.Product != nil && item.Product.Id != 0 {
summary := productDTO.ToProductBaseDTO(*item.Product)
dto.Product = &summary
}
if item.Warehouse != nil && item.Warehouse.Id != 0 {
summary := warehouseDTO.ToWarehouseBaseDTO(*item.Warehouse)
if item.Warehouse.Area.Id != 0 {
areaSummary := areaDTO.ToAreaBaseDTO(item.Warehouse.Area)
summary.Area = &areaSummary
}
if item.Warehouse.Location != nil && item.Warehouse.Location.Id != 0 {
locationSummary := locationDTO.ToLocationBaseDTO(*item.Warehouse.Location)
summary.Location = &locationSummary
}
dto.Warehouse = &summary
}
return dto
}
func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO {
result := make([]PurchaseItemDTO, len(items))
for i, item := range items {
result[i] = ToPurchaseItemDTO(item)
}
return result
}
func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
dto := PurchaseDetailDTO{
Id: p.Id,
PrNumber: p.PrNumber,
PoNumber: p.PoNumber,
Supplier: mapSupplier(p.Supplier),
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
PoDate: p.PoDate,
GrandTotal: p.GrandTotal,
Notes: p.Notes,
Items: ToPurchaseItemDTOs(p.Items),
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
if approval := toPurchaseApprovalDTO(p); approval != nil {
dto.Approval = approval
}
return dto
}
func ToPurchaseListDTO(p entity.Purchase) PurchaseListItemDTO {
dto := PurchaseListItemDTO{
Id: p.Id,
PrNumber: p.PrNumber,
PoNumber: p.PoNumber,
Supplier: mapSupplier(p.Supplier),
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
PoDate: p.PoDate,
GrandTotal: p.GrandTotal,
Notes: p.Notes,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
if approval := toPurchaseApprovalDTO(p); approval != nil {
dto.Approval = approval
}
return dto
}
func mapSupplier(s entity.Supplier) *supplierDTO.SupplierBaseDTO {
if s.Id == 0 {
return nil
}
summary := supplierDTO.ToSupplierBaseDTO(s)
return &summary
}
func ToPurchaseListDTOs(items []entity.Purchase) []PurchaseListItemDTO {
if len(items) == 0 {
return nil
}
result := make([]PurchaseListItemDTO, len(items))
for i, item := range items {
result[i] = ToPurchaseListDTO(item)
}
return result
}
func toPurchaseApprovalDTO(p entity.Purchase) *approvalDTO.ApprovalBaseDTO {
if p.LatestApproval == nil || p.LatestApproval.Id == 0 {
return nil
}
mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval)
return &mapped
}
+55
View File
@@ -0,0 +1,55 @@
package purchases
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
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"
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
type PurchaseModule struct{}
func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
purchaseRepo := rPurchase.NewPurchaseRepository(db)
productRepo := rProduct.NewProductRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
}
expenseBridge := service.NewNoopPurchaseExpenseBridge()
purchaseService := service.NewPurchaseService(
validate,
purchaseRepo,
productRepo,
warehouseRepo,
supplierRepo,
productWarehouseRepo,
approvalRepo,
approvalService,
expenseBridge,
)
userRepo := rUser.NewUserRepository(db)
userService := sUser.NewUserService(userRepo, validate)
Routes(router, purchaseService, userService)
}
@@ -0,0 +1,423 @@
package repositories
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type PurchaseRepository interface {
repository.BaseRepository[entity.Purchase]
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error
GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error)
UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
UpdateReceivingDetails(ctx context.Context, purchaseID uint64, updates []PurchaseReceivingUpdate) error
DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
WithListRelations() func(*gorm.DB) *gorm.DB
UpdateGrandTotal(ctx context.Context, purchaseID uint64, grandTotal float64) error
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
}
type PurchaseRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Purchase]
}
func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
return &PurchaseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db),
}
}
type PurchaseListFilter struct {
SupplierID uint
Search string
PrNumber string
CreatedFrom *time.Time
CreatedTo *time.Time
Status *entity.ApprovalAction
CompletedOnly bool
}
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
db := r.DB().WithContext(ctx)
if err := db.Create(purchase).Error; err != nil {
return err
}
if len(items) == 0 {
return nil
}
for _, item := range items {
item.PurchaseId = purchase.Id
}
if err := db.Create(&items).Error; err != nil {
return err
}
return nil
}
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error {
if len(items) == 0 {
return nil
}
for _, item := range items {
if item == nil {
continue
}
item.PurchaseId = purchaseID
}
return r.DB().WithContext(ctx).Create(&items).Error
}
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
var purchase entity.Purchase
err := r.DB().WithContext(ctx).
Scopes(r.withDetailRelations).
First(&purchase, id).Error
if err != nil {
return nil, err
}
return &purchase, nil
}
func (r *PurchaseRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
db = r.withListRelations(db)
return r.applyListFilters(db, filter)
})
}
func (r *PurchaseRepositoryImpl) WithListRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withListRelations(db)
}
}
func (r *PurchaseRepositoryImpl) withDetailRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("Supplier").
Preload("Items", func(db *gorm.DB) *gorm.DB {
return db.Order("id ASC")
}).
Preload("Items.Product").
Preload("Items.Warehouse").
Preload("Items.Warehouse.Area").
Preload("Items.Warehouse.Location").
Preload("Items.ProductWarehouse")
}
func (r *PurchaseRepositoryImpl) WithDetailRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withDetailRelations(db)
}
}
type PurchasePricingUpdate struct {
ItemID uint64
ProductID *uint64
Price float64
TotalPrice float64
Quantity *float64
TotalQty *float64
}
type PurchaseReceivingUpdate struct {
ItemID uint64
ReceivedDate *time.Time
TravelNumber *string
TravelDocumentPath *string
VehicleNumber *string
ReceivedQty *float64
WarehouseID *uint
ProductWarehouseID *uint
ClearProductWarehouse bool
}
func (r *PurchaseRepositoryImpl) UpdatePricing(
ctx context.Context,
purchaseID uint64,
updates []PurchasePricingUpdate,
grandTotal float64,
) error {
if len(updates) == 0 {
return errors.New("pricing updates cannot be empty")
}
db := r.DB().WithContext(ctx)
for _, upd := range updates {
data := map[string]interface{}{
"price": upd.Price,
"total_price": upd.TotalPrice,
}
if upd.ProductID != nil {
data["product_id"] = *upd.ProductID
}
if upd.Quantity != nil {
data["sub_qty"] = *upd.Quantity
}
if upd.TotalQty != nil {
data["total_qty"] = *upd.TotalQty
}
result := db.Model(&entity.PurchaseItem{}).
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
Updates(data)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
}
if err := db.Model(&entity.Purchase{}).
Where("id = ?", purchaseID).
Updates(map[string]interface{}{
"grand_total": grandTotal,
"updated_at": gorm.Expr("NOW()"),
}).Error; err != nil {
return err
}
return nil
}
func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
ctx context.Context,
purchaseID uint64,
updates []PurchaseReceivingUpdate,
) error {
if len(updates) == 0 {
return errors.New("receiving updates cannot be empty")
}
db := r.DB().WithContext(ctx)
for _, upd := range updates {
data := map[string]interface{}{}
if upd.ReceivedDate != nil {
data["received_date"] = upd.ReceivedDate
}
if upd.TravelNumber != nil {
data["travel_number"] = upd.TravelNumber
}
if upd.TravelDocumentPath != nil {
data["travel_number_docs"] = upd.TravelDocumentPath
}
if upd.VehicleNumber != nil {
data["vehicle_number"] = upd.VehicleNumber
}
if upd.ReceivedQty != nil {
data["total_qty"] = upd.ReceivedQty
}
if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
data["warehouse_id"] = upd.WarehouseID
}
if upd.ProductWarehouseID != nil {
data["product_warehouse_id"] = *upd.ProductWarehouseID
} else if upd.ClearProductWarehouse {
data["product_warehouse_id"] = gorm.Expr("NULL")
}
if len(data) == 0 {
continue
}
result := db.Model(&entity.PurchaseItem{}).
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
Updates(data)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
}
return nil
}
func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
ctx context.Context,
purchaseID uint64,
grandTotal float64,
) error {
return r.DB().WithContext(ctx).
Model(&entity.Purchase{}).
Where("id = ?", purchaseID).
Updates(map[string]interface{}{
"grand_total": grandTotal,
"updated_at": gorm.Expr("NOW()"),
}).Error
}
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error {
if len(itemIDs) == 0 {
return errors.New("itemIDs cannot be empty")
}
return r.DB().WithContext(ctx).
Where("purchase_id = ? AND id IN ?", purchaseID, itemIDs).
Delete(&entity.PurchaseItem{}).Error
}
func (r *PurchaseRepositoryImpl) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) {
return r.generateSequentialNumber(ctx, tx, "pr_number", utils.PurchasePRNumberPrefix, utils.PurchaseNumberPadding)
}
func (r *PurchaseRepositoryImpl) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
return r.generateSequentialNumber(ctx, tx, "po_number", utils.PurchasePONumberPrefix, utils.PurchaseNumberPadding)
}
func (r *PurchaseRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
db := tx
if db == nil {
db = r.DB()
}
var values []string
err := db.WithContext(ctx).
Model(&entity.Purchase{}).
Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%").
Select(column).
Order(fmt.Sprintf("%s DESC", column)).
Limit(20).
Clauses(clause.Locking{Strength: "UPDATE"}).
Pluck(column, &values).Error
if err != nil {
return "", err
}
next := 1
for _, value := range values {
if number, ok := parseNumericSuffix(value, prefix); ok {
next = number + 1
break
}
}
const maxAttempts = 20
for attempt := 0; attempt < maxAttempts; attempt++ {
candidate := fmt.Sprintf("%s%0*d", prefix, padding, next)
exists, err := r.numberExists(ctx, db, column, candidate)
if err != nil {
return "", err
}
if !exists {
return candidate, nil
}
next++
}
return "", fmt.Errorf("unable to generate unique %s", column)
}
func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(&entity.Purchase{}).
Where(fmt.Sprintf("%s = ?", column), value).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func parseNumericSuffix(value, prefix string) (int, bool) {
if !strings.HasPrefix(value, prefix) {
return 0, false
}
suffix := strings.TrimPrefix(value, prefix)
if suffix == "" {
return 0, false
}
trimmed := strings.TrimLeft(suffix, "0")
if trimmed == "" {
trimmed = "0"
}
number, err := strconv.Atoi(trimmed)
if err != nil {
return 0, false
}
return number, true
}
func (r *PurchaseRepositoryImpl) withListRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Supplier")
}
func (r *PurchaseRepositoryImpl) applyListFilters(db *gorm.DB, filter *PurchaseListFilter) *gorm.DB {
if filter == nil {
return db
}
if filter.SupplierID > 0 {
db = db.Where("purchases.supplier_id = ?", filter.SupplierID)
}
if search := strings.ToLower(strings.TrimSpace(filter.Search)); search != "" {
like := "%" + search + "%"
db = db.Where("(LOWER(purchases.pr_number) LIKE ? OR LOWER(COALESCE(purchases.notes, '')) LIKE ?)", like, like)
}
if pr := strings.TrimSpace(filter.PrNumber); pr != "" {
db = db.Where("purchases.pr_number ILIKE ?", "%"+pr+"%")
}
if filter.CreatedFrom != nil {
db = db.Where("purchases.created_at >= ?", *filter.CreatedFrom)
}
if filter.CreatedTo != nil {
db = db.Where("purchases.created_at < ?", *filter.CreatedTo)
}
if filter.CompletedOnly {
step := uint16(utils.PurchaseStepCompleted)
db = r.applyLatestApprovalFilter(db, entity.ApprovalActionApproved, &step)
} else if filter.Status != nil {
db = r.applyLatestApprovalFilter(db, *filter.Status, nil)
}
return db.Order("purchases.created_at DESC").Order("purchases.id DESC")
}
func (r *PurchaseRepositoryImpl) applyLatestApprovalFilter(db *gorm.DB, action entity.ApprovalAction, minStep *uint16) *gorm.DB {
latestSub := r.DB().
Model(&entity.Approval{}).
Select("approvable_id, MAX(action_at) AS latest_action_at").
Where("approvable_type = ?", utils.ApprovalWorkflowPurchase.String()).
Group("approvable_id")
db = db.
Joins("LEFT JOIN (?) AS latest_purchase_approvals ON latest_purchase_approvals.approvable_id = purchases.id", latestSub).
Joins(
"LEFT JOIN approvals ON approvals.approvable_id = purchases.id AND approvals.approvable_type = ? AND approvals.action_at = latest_purchase_approvals.latest_action_at",
utils.ApprovalWorkflowPurchase.String(),
).
Where("approvals.action = ?", string(action))
if minStep != nil {
db = db.Where("approvals.step_number >= ?", *minStep)
}
return db
}
+26
View File
@@ -0,0 +1,26 @@
package purchases
import (
"github.com/gofiber/fiber/v2"
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
func Routes(router fiber.Router, purchaseService service.PurchaseService, userService user.UserService) {
ctrl := controller.NewPurchaseController(purchaseService)
route := router.Group("/purchases")
route.Use(middleware.Auth(userService))
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
route.Post("/", ctrl.CreateOne)
route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase)
route.Post("/:id/approvals/manager", ctrl.ApproveManagerPurchase)
route.Post("/:id/receipts", ctrl.ReceiveProducts)
route.Delete("/:id", ctrl.DeletePurchase)
route.Delete("/:id/items", ctrl.DeleteItems)
}
@@ -0,0 +1,43 @@
package service
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
// PurchaseExpenseBridge defines hooks that allow purchase flows to stay in sync with expense data once it exists.
type PurchaseExpenseBridge interface {
OnItemsCreated(ctx context.Context, purchaseID uint64, items []entity.PurchaseItem) error
OnItemsDeleted(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
OnItemsReceived(ctx context.Context, purchaseID uint64, updates []ExpenseReceivingPayload) error
}
// ExpenseReceivingPayload captures the minimum data expense integration will need once available.
type ExpenseReceivingPayload struct {
PurchaseItemID uint64
ProductID uint64
WarehouseID uint64
ReceivedQty float64
ReceivedDate *time.Time
}
// noopPurchaseExpenseBridge is the default implementation until the expense module is ready.
type noopPurchaseExpenseBridge struct{}
func NewNoopPurchaseExpenseBridge() PurchaseExpenseBridge {
return &noopPurchaseExpenseBridge{}
}
func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint64, _ []entity.PurchaseItem) error {
return nil
}
func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint64, _ []uint64) error {
return nil
}
func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint64, _ []ExpenseReceivingPayload) error {
return nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
package validation
type PurchaseItemPayload struct {
WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"`
ProductID uint `json:"product_id" validate:"required,gt=0"`
Quantity float64 `json:"qty" validate:"required,gt=0"`
}
type CreatePurchaseRequest struct {
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
CreditTerm int `json:"credit_term" validate:"required,gte=0"`
Notes *string `json:"notes" validate:"omitempty,max=500"`
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
}
type StaffPurchaseApprovalItem struct {
PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
// For new items (no purchase_item_id), product_id is required.
ProductID uint64 `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Price float64 `json:"price" validate:"required,gt=0"`
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
}
type ApproveStaffPurchaseRequest struct {
Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type ApproveManagerPurchaseRequest struct {
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type ReceivePurchaseItemRequest struct {
PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"`
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"`
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"`
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"`
TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"`
VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"`
ReceivedQty *float64 `json:"received_qty" validate:"omitempty,gte=0"`
}
type ReceivePurchaseRequest struct {
Items []ReceivePurchaseItemRequest `json:"items" validate:"required,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type DeletePurchaseItemsRequest struct {
ItemIDs []uint64 `json:"item_ids" validate:"required,min=1,dive,gt=0"`
}
type PurchaseQuery struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
PrNumber string `query:"pr_number" validate:"omitempty,max=50"`
CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"`
CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"`
Status string `query:"status" validate:"omitempty,oneof=CREATED UPDATED APPROVED REJECTED COMPLETED"`
}
+3 -1
View File
@@ -12,8 +12,9 @@ import (
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
// MODULE IMPORTS
)
@@ -29,6 +30,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
master.MasterModule{},
constants.ConstantModule{},
inventory.InventoryModule{},
purchases.PurchaseModule{},
production.ProductionModule{},
approvals.ApprovalModule{},
ssoModule.Module{},
+79 -2
View File
@@ -18,6 +18,8 @@ const (
FlagIsActive FlagType = "IS_ACTIVE"
FlagDOC FlagType = "DOC"
FlagPullet FlagType = "PULLET"
FlagLayer FlagType = "LAYER"
FlagPakan FlagType = "PAKAN"
FlagPreStarter FlagType = "PRE-STARTER"
FlagStarter FlagType = "STARTER"
@@ -37,6 +39,8 @@ const (
var flagGroupOptions = map[FlagGroup][]FlagType{
FlagGroupProduct: {
FlagDOC,
FlagPullet,
FlagLayer,
FlagPakan,
FlagPreStarter,
FlagStarter,
@@ -79,6 +83,24 @@ const (
WarehouseTypeKandang WarehouseType = "KANDANG"
)
// -------------------------------------------------------------------
// Stock log
// -------------------------------------------------------------------
type StockLogTransactionType string
const (
StockLogTransactionTypeIncrease StockLogTransactionType = "INCREASE"
StockLogTransactionTypeDecrease StockLogTransactionType = "DECREASE"
)
type StockLogType string
const (
StockLogTypeAdjustment StockLogType = "ADJUSTMENT"
StockLogTypeTransfer StockLogType = "TRANSFER"
)
// -------------------------------------------------------------------
// WarehouseType
// -------------------------------------------------------------------
@@ -140,6 +162,36 @@ var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{
ProjectFlockStepAktif: "Aktif",
}
// -------------------------------------------------------------------
// Project Flock Kandang Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowProjectFlockKandang approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCK_KANDANGS")
ProjectFlockKandangStepPengajuan approvalutils.ApprovalStep = 1
ProjectFlockKandangStepDisetujui approvalutils.ApprovalStep = 2
)
var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{
ProjectFlockKandangStepPengajuan: "Pengajuan",
ProjectFlockKandangStepDisetujui: "Disetujui",
}
// -------------------------------------------------------------------
// Transfer To laying Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowTransferToLaying approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("TRANSFER_TO_LAYINGS")
TransferToLayingStepPengajuan approvalutils.ApprovalStep = 1
TransferToLayingStepDisetujui approvalutils.ApprovalStep = 2
)
var TransferToLayingApprovalSteps = map[approvalutils.ApprovalStep]string{
TransferToLayingStepPengajuan: "Pengajuan",
TransferToLayingStepDisetujui: "Disetujui",
}
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// Recording Approval
// -------------------------------------------------------------------
@@ -153,8 +205,33 @@ const (
var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
RecordingStepGradingTelur: "Grading-Telur",
RecordingStepPengajuan: "Pengajuan",
RecordingStepDisetujui: "Disetujui",
RecordingStepPengajuan: "Pengajuan",
RecordingStepDisetujui: "Disetujui",
}
// -------------------------------------------------------------------
// Purchase Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowPurchase approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PURCHASES")
PurchaseStepPengajuan approvalutils.ApprovalStep = 1
PurchaseStepStaffPurchase approvalutils.ApprovalStep = 2
PurchaseStepManager approvalutils.ApprovalStep = 3
PurchaseStepReceiving approvalutils.ApprovalStep = 4
PurchaseStepCompleted approvalutils.ApprovalStep = 5
PurchasePRNumberPrefix = "PR-LTI-"
PurchasePONumberPrefix = "PO-LTI-"
PurchaseNumberPadding = 4
)
var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{
PurchaseStepPengajuan: "Pengajuan",
PurchaseStepStaffPurchase: "Staff Purchase",
PurchaseStepManager: "Manager Purchase",
PurchaseStepReceiving: "Penerimaan Produk",
PurchaseStepCompleted: "Selesai",
}
// -------------------------------------------------------------------
+2 -2
View File
@@ -10,8 +10,8 @@ import (
)
func ErrorHandler(c *fiber.Ctx, err error) error {
if errorsMap := validation.CustomErrorMessages(err); len(errorsMap) > 0 {
return response.Error(c, fiber.StatusBadRequest, "Bad Request", errorsMap)
if message, errorsMap := validation.CustomErrorMessages(err); len(errorsMap) > 0 {
return response.Error(c, fiber.StatusBadRequest, message, nil)
}
var fiberErr *fiber.Error

Some files were not shown because too many files have changed in this diff Show More