Merge branch 'development-after-sso' into 'development'

[FEAT/BE] merge feat recording, refactor chickin and implement auth middleware

See merge request mbugroup/lti-api!54
This commit is contained in:
Adnan Zahir
2025-11-05 21:51:05 +07:00
116 changed files with 5178 additions and 1542 deletions
Vendored
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -16,7 +16,7 @@ docker-compose.yaml
Dockerfile.local Dockerfile.local
# Go build cache # Go build cache
.gocache/ .gocache/
vendor/ vendor
# Logs & reports # Logs & reports
*.log *.log
-59
View File
@@ -1,59 +0,0 @@
# ===============================
# LTI-API Makefile (Docker Setup)
# ===============================
APP_NAME := lti-api
COMPOSE := docker compose -f docker-compose.yaml
NETWORK := lti-network
ENV_FILE := .env.lti-api
include $(ENV_FILE)
export $(shell sed 's/=.*//' $(ENV_FILE))
MIGRATIONS_DIR := ./migrations
MIGRATE_IMAGE := migrate/migrate:v4.15.2
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@lti-postgres:5432/$(DB_NAME)?sslmode=disable
# --- Docker ---
docker-local:
@echo "🚀 Starting $(APP_NAME) with local PostgreSQL & Redis..."
@$(COMPOSE) up --build -d
docker-down:
@$(COMPOSE) down --remove-orphans
docker-nuke:
@echo "💣 Removing all containers, images, and volumes..."
@$(COMPOSE) down --rmi all --volumes --remove-orphans
# --- Database / Migration ---
wait-db:
@echo "⏳ Waiting for database lti-postgres to be ready (inside Docker network)..."
@$(COMPOSE) run --rm app sh -c 'until nc -z lti-postgres 5432; do echo "Waiting for DB..."; sleep 2; done; echo "✅ Database is ready!"'
migrate-up: wait-db
@echo "⬆️ Running migrations..."
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
migrate-down: wait-db
@echo "⬇️ Rolling back all migrations..."
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all
seed:
@echo "🌱 Running seed script..."
@$(COMPOSE) run --rm app go run cmd/seed/main.go
psql:
@docker exec -it lti-postgres psql -U $(DB_USER) -d $(DB_NAME)
logs:
@$(COMPOSE) logs -f app
restart:
@$(COMPOSE) restart
status:
@$(COMPOSE) ps
+120
View File
@@ -0,0 +1,120 @@
# --- Load .env kalau ada, dan export ke shell child ---
ifneq (,$(wildcard .env))
include .env
export
endif
# --- Konfigurasi umum ---
COMPOSE ?= docker compose -f docker-compose.local.yml
NETWORK ?= lti-api_go-network
MIGRATE_IMAGE ?= migrate/migrate
MIGRATIONS_DIR := $(PWD)/internal/database/migrations
# Fallback agar tetap jalan meski .env kosong
DB_HOST ?= postgresdb
DB_PORT ?= 5432
DB_USER ?= postgres
DB_PASSWORD ?= postgres
DB_NAME ?= db_lti_erp
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
# Tunggu DB ready memakai pg_isready dari image postgres
WAIT_DB := docker run --rm --network $(NETWORK) postgres:alpine \
sh -c 'until pg_isready -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -d $(DB_NAME); do echo "waiting for postgres..."; sleep 1; done'
# Default target
.DEFAULT_GOAL := start
# --- Daftar phony targets ---
.PHONY: start build test lint gen \
db-up wait-db \
migration-% migrate-up migrate-down migrate-fresh \
seed \
docker-local docker-down docker-nuke docker-cache psql
# --- Go workflow ---
start:
@go run cmd/api/main.go
build:
@go build -o tmp/app ./cmd/api
test:
@go test ./test/...
lint:
@golangci-lint run
# --- Compose / DB helpers ---
db-up:
@$(COMPOSE) up -d postgresdb
wait-db:
@$(WAIT_DB)
# --- Migration (pembuatan file) ---
# Contoh: make migration-create_users_table
# ":" akan diubah ke "_" (biar aman untuk nama file)
migration-%:
@migrate create -ext sql -dir $(MIGRATIONS_DIR) $(subst :,_,$*)
# --- Migration (apply via docker image 'migrate') ---
migrate-up: db-up wait-db
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
# Contoh:
# make migrate-down step=2 → rollback 2 step
# make migrate-down → rollback semua
migrate-down: db-up wait-db
@if [ -n "$(step)" ]; then \
echo "⬇️ Migrating down $(step) step(s)..."; \
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down $(step); \
else \
echo "⬇️ Migrating down ALL steps..."; \
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all; \
fi
migrate-fresh: migrate-down migrate-up
@true
# Pakai: make migrate-force v=20250917120000
migrate-force:
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" force $(v)
# --- Seeder ---
seed: db-up wait-db
@$(COMPOSE) run --rm app go run cmd/seed/main.go
# --- Docker orchestration convenience ---
docker-local:
@$(COMPOSE) up --build -d
docker-down:
@$(COMPOSE) down --remove-orphans
# ⚠️ Akan menghapus container, images dan volumes.
docker-nuke:
@$(COMPOSE) down --rmi all --volumes --remove-orphans
docker-cache:
@docker builder prune -f
# --- PSQL shell ke DB di container ---
psql: db-up
@$(COMPOSE) exec -it postgresdb psql -U $(DB_USER) -d $(DB_NAME)
# Single feature
# example: make gen feat=product-category
# Sub feature
# make gen feat=master/area
gen:
@go run tools/gen.go $(feat)
# @goimports -w internal
+26 -1
View File
@@ -62,9 +62,34 @@ func setupRedis() *redis.Client {
} }
func setupSSO(ctx context.Context, rdb *redis.Client) { func setupSSO(ctx context.Context, rdb *redis.Client) {
const (
maxAttempts = 12
retryDelay = 5 * time.Second
)
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil { if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil {
utils.Log.Fatalf("SSO initialization failed: %v", err) lastErr = err
utils.Log.WithError(err).Warnf("SSO initialization attempt %d/%d failed", attempt, maxAttempts)
select {
case <-ctx.Done():
utils.Log.Fatalf("SSO initialization aborted: %v", ctx.Err())
case <-time.After(retryDelay):
} }
continue
}
lastErr = nil
if attempt > 1 {
utils.Log.Infof("SSO initialization succeeded after %d attempts", attempt)
}
break
}
if lastErr != nil {
utils.Log.Fatalf("SSO initialization failed: %v", lastErr)
}
if rdb != nil { if rdb != nil {
session.SetRevocationStore(session.NewRevocationStore(rdb, config.SSOTokenBlacklistPrefix)) session.SetRevocationStore(session.NewRevocationStore(rdb, config.SSOTokenBlacklistPrefix))
} else { } else {
-2
View File
@@ -41,8 +41,6 @@ services:
working_dir: /lti-api working_dir: /lti-api
volumes: volumes:
- .:/lti-api - .:/lti-api
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
command: air -c .air.toml command: air -c .air.toml
env_file: env_file:
- .env - .env
+44
View File
@@ -0,0 +1,44 @@
package capabilities
import (
"strings"
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
)
// FromPermissions returns a filtered map of capabilities that the frontend can use
// to toggle features. Only permissions recognized by the application are exposed.
func FromPermissions(perms []string) map[string]bool {
if len(perms) == 0 {
return nil
}
out := make(map[string]bool)
for _, perm := range perms {
if key, ok := normalizeAndAllow(perm); ok {
out[key] = true
}
}
if len(out) == 0 {
return nil
}
return out
}
func normalizeAndAllow(perm string) (string, bool) {
perm = strings.ToLower(strings.TrimSpace(perm))
if perm == "" {
return "", false
}
if _, ok := allowed[perm]; !ok {
return "", false
}
return perm, true
}
var allowed = map[string]struct{}{
recordings.PermissionRecordingRead: {},
recordings.PermissionRecordingCreate: {},
recordings.PermissionRecordingUpdate: {},
recordings.PermissionRecordingDelete: {},
}
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"fmt"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -32,3 +33,21 @@ func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeI
} }
return count > 0, nil return count > 0, nil
} }
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
if field == "" {
return false, fmt.Errorf("field is required")
}
var count int64
q := db.WithContext(ctx).
Model(new(T)).
Where(fmt.Sprintf("%s = ?", field), value).
Where("deleted_at IS NULL")
if excludeID != nil {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
+8
View File
@@ -32,6 +32,10 @@ var (
DBPassword string DBPassword string
DBName string DBName string
DBPort int DBPort int
DBSSLMode string
DBSSLRootCert string
DBSSLCert string
DBSSLKey string
JWTSecret string JWTSecret string
JWTAccessExp int JWTAccessExp int
JWTRefreshExp int JWTRefreshExp int
@@ -79,6 +83,10 @@ func init() {
DBPassword = viper.GetString("DB_PASSWORD") DBPassword = viper.GetString("DB_PASSWORD")
DBName = viper.GetString("DB_NAME") DBName = viper.GetString("DB_NAME")
DBPort = viper.GetInt("DB_PORT") DBPort = viper.GetInt("DB_PORT")
DBSSLMode = defaultString(viper.GetString("DB_SSLMODE"), "disable")
DBSSLRootCert = strings.TrimSpace(viper.GetString("DB_SSLROOTCERT"))
DBSSLCert = strings.TrimSpace(viper.GetString("DB_SSLCERT"))
DBSSLKey = strings.TrimSpace(viper.GetString("DB_SSLKEY"))
// jwt configuration // jwt configuration
JWTSecret = viper.GetString("JWT_SECRET") JWTSecret = viper.GetString("JWT_SECRET")
+20 -4
View File
@@ -2,6 +2,7 @@ package database
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
@@ -13,10 +14,25 @@ import (
) )
func Connect(dbHost, dbName string) *gorm.DB { func Connect(dbHost, dbName string) *gorm.DB {
dsn := fmt.Sprintf( parts := []string{
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", fmt.Sprintf("host=%s", dbHost),
dbHost, config.DBUser, config.DBPassword, dbName, config.DBPort, fmt.Sprintf("user=%s", config.DBUser),
) fmt.Sprintf("password=%s", config.DBPassword),
fmt.Sprintf("dbname=%s", dbName),
fmt.Sprintf("port=%d", config.DBPort),
fmt.Sprintf("sslmode=%s", config.DBSSLMode),
"TimeZone=Asia/Shanghai",
}
if config.DBSSLRootCert != "" {
parts = append(parts, fmt.Sprintf("sslrootcert=%s", config.DBSSLRootCert))
}
if config.DBSSLCert != "" {
parts = append(parts, fmt.Sprintf("sslcert=%s", config.DBSSLCert))
}
if config.DBSSLKey != "" {
parts = append(parts, fmt.Sprintf("sslkey=%s", config.DBSSLKey))
}
dsn := strings.Join(parts, " ")
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), Logger: logger.Default.LogMode(logger.Info),
@@ -0,0 +1,24 @@
BEGIN;
--? Child Indexes(optional, biar rapi tapi klo gada juga ilang pas di drop)
DROP INDEX IF EXISTS idx_recording_stocks_product;
DROP INDEX IF EXISTS idx_recording_stocks_recording;
DROP INDEX IF EXISTS idx_recording_depl_recording;
DROP INDEX IF EXISTS idx_recording_bws_recording;
--? Child Tables
DROP TABLE IF EXISTS recording_stocks;
DROP TABLE IF EXISTS recording_depletions;
DROP TABLE IF EXISTS recording_bws;
--? Parent Indexes ON recordings
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
--? Parent table
DROP TABLE IF EXISTS recordings;
COMMIT;
@@ -0,0 +1,150 @@
BEGIN;
--? RECORDINGS (tabel induk recording harian)
CREATE TABLE IF NOT EXISTS recordings (
id BIGSERIAL PRIMARY KEY,
project_flock_id BIGINT NOT NULL,
record_datetime TIMESTAMPTZ NOT NULL,
record_date DATE,
status INT NOT NULL DEFAULT 0, --? 0=draft,1=submitted,2=approved,3=rejected
ontime INT NOT NULL DEFAULT 0, --? 1=ontime,0=late (pakai INT/BOOLEAN sesuai preferensi)
day INT,
total_depletion INT,
cum_depletion_rate NUMERIC(7,3),
daily_gain NUMERIC(7,3),
avg_daily_gain NUMERIC(7,3),
cum_intake INT,
fcr_value NUMERIC(7,3),
total_chick BIGINT,
daily_depletion_rate NUMERIC(7,3),
cum_depletion INT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT,
CONSTRAINT fk_recordings_project_flock
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id),
CONSTRAINT fk_recordings_created_by
FOREIGN KEY (created_by) REFERENCES users(id),
CONSTRAINT chk_recordings_status
CHECK (status IN (0,1,2,3)),
CONSTRAINT chk_recordings_ontime
CHECK (ontime IN (0,1)),
CONSTRAINT chk_recordings_day
CHECK (day IS NULL OR day >= 1),
CONSTRAINT chk_recordings_nonnegatives
CHECK (
(total_depletion IS NULL OR total_depletion >= 0) AND
(cum_depletion IS NULL OR cum_depletion >= 0) AND
(total_chick IS NULL OR total_chick >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(fcr_value IS NULL OR fcr_value > 0) AND
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
)
);
--? Set record_date otomatis berdasarkan record_datetime (pakai zona Asia/Jakarta)
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
BEGIN
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
CREATE TRIGGER recordings_set_record_date_trg
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
CREATE INDEX IF NOT EXISTS idx_recordings_flock_datetime
ON recordings (project_flock_id, record_datetime);
--? Unique harian (1 recording per hari dan per flock)
CREATE UNIQUE INDEX IF NOT EXISTS uq_recordings_flock_record_date
ON recordings (project_flock_id, record_date)
WHERE deleted_at IS NULL;
--? RECORDING_BWS (BW per recording)
CREATE TABLE IF NOT EXISTS recording_bws (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
weight NUMERIC(8,2) NOT NULL, --? bobot per ekor/kelompok
qty INT NOT NULL DEFAULT 1, --? jumlah ekor pada bobot ini
notes VARCHAR,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_recording_bws_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT chk_recording_bws_nonneg
CHECK (weight >= 0 AND qty >= 1)
);
CREATE INDEX IF NOT EXISTS idx_recording_bws_recording
ON recording_bws (recording_id);
--? RECORDING_DEPLETIONS
CREATE TABLE IF NOT EXISTS recording_depletions (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
total BIGINT NOT NULL,
notes VARCHAR,
CONSTRAINT fk_recording_depl_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT fk_recording_depl_prodwh
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
CONSTRAINT chk_recording_depl_total
CHECK (total >= 0)
);
CREATE INDEX IF NOT EXISTS idx_recording_depl_recording
ON recording_depletions (recording_id);
--? RECORDING_STOCKS
CREATE TABLE IF NOT EXISTS recording_stocks (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
increase NUMERIC(10,3), --? penambahan (boleh NULL)
decrease NUMERIC(10,3), --? pengurangan (boleh NULL)
usage_amount BIGINT, --? pemakaian (opsional, jika konsep dipisah dari decrease)
notes VARCHAR,
CONSTRAINT fk_recording_stocks_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT fk_recording_stocks_prodwh
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
CONSTRAINT chk_recording_stocks_nonneg
CHECK (
(increase IS NULL OR increase >= 0) AND
(decrease IS NULL OR decrease >= 0) AND
(usage_amount IS NULL OR usage_amount >= 0)
)
);
CREATE INDEX IF NOT EXISTS idx_recording_stocks_recording
ON recording_stocks (recording_id);
CREATE INDEX IF NOT EXISTS idx_recording_stocks_product
ON recording_stocks (product_warehouse_id);
COMMIT;
@@ -0,0 +1,22 @@
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
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;
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;
@@ -0,0 +1,98 @@
BEGIN;
DROP INDEX IF EXISTS project_flocks_base_period_unique;
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS flock_id BIGINT;
WITH normalized AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name,
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
FROM project_flocks pf
),
seed_flocks AS (
SELECT DISTINCT
n.normalized_name,
MIN(n.created_by) AS created_by
FROM normalized n
GROUP BY n.normalized_name
)
INSERT INTO flocks (name, created_by, created_at, updated_at)
SELECT sf.normalized_name, sf.created_by, NOW(), NOW()
FROM seed_flocks sf
ON CONFLICT DO NOTHING;
WITH normalized AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name
FROM project_flocks pf
),
resolved AS (
SELECT
n.id,
f.id AS flock_id
FROM normalized n
JOIN flocks f ON LOWER(f.name) = LOWER(n.normalized_name)
)
UPDATE project_flocks pf
SET flock_id = resolved.flock_id
FROM resolved
WHERE pf.id = resolved.id;
WITH missing AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name,
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
FROM project_flocks pf
WHERE pf.flock_id IS NULL
),
seed_missing AS (
SELECT DISTINCT normalized_name, created_by FROM missing
)
INSERT INTO flocks (name, created_by, created_at, updated_at)
SELECT sm.normalized_name, sm.created_by, NOW(), NOW()
FROM seed_missing sm
ON CONFLICT DO NOTHING;
WITH missing AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name
FROM project_flocks pf
WHERE pf.flock_id IS NULL
)
UPDATE project_flocks pf
SET flock_id = f.id
FROM missing m
JOIN flocks f ON LOWER(f.name) = LOWER(m.normalized_name)
WHERE pf.id = m.id;
ALTER TABLE project_flocks
ALTER COLUMN flock_id SET NOT NULL;
DROP INDEX IF EXISTS project_flocks_flock_name_unique;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS flock_name;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_period_unique
ON project_flocks (flock_id, period)
WHERE deleted_at IS NULL;
COMMIT;
@@ -0,0 +1,55 @@
BEGIN;
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS flock_name VARCHAR(255);
WITH generated_names AS (
SELECT
pf.id,
COALESCE(f.name, CONCAT('Project Flock ', pf.id)) AS base_name,
pf.period,
ROW_NUMBER() OVER (PARTITION BY COALESCE(f.name, CONCAT('Project Flock ', pf.id)) ORDER BY pf.id) AS rn
FROM project_flocks pf
LEFT JOIN flocks f ON f.id = pf.flock_id
)
UPDATE project_flocks pf
SET flock_name = CASE
WHEN gn.period IS NOT NULL THEN
CASE
WHEN gn.rn = 1 THEN CONCAT(gn.base_name, ' ', gn.period)
ELSE CONCAT(gn.base_name, ' ', gn.period, ' ', gn.rn)
END
ELSE
CASE
WHEN gn.rn = 1 THEN gn.base_name
ELSE CONCAT(gn.base_name, ' ', gn.rn)
END
END
FROM generated_names gn
WHERE pf.id = gn.id
AND (pf.flock_name IS NULL OR pf.flock_name = '');
UPDATE project_flocks
SET flock_name = CONCAT('Project Flock ', id)
WHERE flock_name IS NULL OR flock_name = '';
ALTER TABLE project_flocks
ALTER COLUMN flock_name SET NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_name_unique
ON project_flocks (flock_name)
WHERE deleted_at IS NULL;
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique
ON project_flocks (
LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))),
period
)
WHERE deleted_at IS NULL;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS flock_id;
COMMIT;
@@ -0,0 +1,143 @@
BEGIN;
-- Drop newly introduced egg tables
DROP TABLE IF EXISTS grading_eggs;
DROP TABLE IF EXISTS recording_eggs;
-- Revert recording_stocks structure
ALTER TABLE recording_stocks
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
ALTER TABLE recording_stocks
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS pending_qty;
ALTER TABLE recording_stocks
ADD COLUMN increase NUMERIC(10,3),
ADD COLUMN decrease NUMERIC(10,3),
ADD COLUMN usage_amount BIGINT,
ADD COLUMN notes VARCHAR;
ALTER TABLE recording_stocks
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
(increase IS NULL OR increase >= 0) AND
(decrease IS NULL OR decrease >= 0) AND
(usage_amount IS NULL OR usage_amount >= 0)
);
-- Revert recording_depletions structure
ALTER TABLE recording_depletions
DROP CONSTRAINT IF EXISTS chk_recording_depl_qty;
ALTER TABLE recording_depletions
ALTER COLUMN qty TYPE BIGINT USING COALESCE(qty, 0)::BIGINT;
ALTER TABLE recording_depletions
RENAME COLUMN qty TO total;
ALTER TABLE recording_depletions
ADD COLUMN notes VARCHAR;
ALTER TABLE recording_depletions
ADD CONSTRAINT chk_recording_depl_total CHECK (total >= 0);
-- Revert recording_bws structure
ALTER TABLE recording_bws
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
ALTER TABLE recording_bws
ALTER COLUMN qty TYPE INT USING COALESCE(qty, 0)::INT;
ALTER TABLE recording_bws
DROP COLUMN IF EXISTS total_weight;
ALTER TABLE recording_bws
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
ALTER TABLE recording_bws
RENAME COLUMN avg_weight TO weight;
ALTER TABLE recording_bws
ADD COLUMN notes VARCHAR;
UPDATE recording_bws
SET qty = GREATEST(qty, 1);
ALTER TABLE recording_bws
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (weight >= 0 AND qty >= 1);
-- Revert recordings header
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock_kandang,
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2;
ALTER TABLE recordings
ALTER COLUMN total_depletion_qty TYPE INT USING COALESCE(total_depletion_qty, 0)::INT,
ALTER COLUMN total_chick_qty TYPE BIGINT USING COALESCE(total_chick_qty, 0)::BIGINT;
ALTER TABLE recordings
RENAME COLUMN total_depletion_qty TO total_depletion;
ALTER TABLE recordings
RENAME COLUMN total_chick_qty TO total_chick;
ALTER TABLE recordings
ADD COLUMN record_date DATE,
ADD COLUMN status INT NOT NULL DEFAULT 0,
ADD COLUMN ontime INT NOT NULL DEFAULT 0,
ADD COLUMN daily_depletion_rate NUMERIC(7,3),
ADD COLUMN cum_depletion INT;
ALTER TABLE recordings
RENAME COLUMN project_flock_kandangs_id TO project_flock_id;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_status CHECK (status IN (0,1,2,3));
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_ontime CHECK (ontime IN (0,1));
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives CHECK (
(total_depletion IS NULL OR total_depletion >= 0) AND
(cum_depletion IS NULL OR cum_depletion >= 0) AND
(total_chick IS NULL OR total_chick >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(fcr_value IS NULL OR fcr_value > 0) AND
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
);
-- Ensure new columns carry derived data
UPDATE recordings
SET record_date = (record_datetime AT TIME ZONE 'Asia/Jakarta')::date
WHERE record_date IS NULL;
-- Restore helper trigger/function and indexes
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
BEGIN
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER recordings_set_record_date_trg
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
CREATE INDEX idx_recordings_flock_datetime
ON recordings (project_flock_id, record_datetime);
CREATE UNIQUE INDEX uq_recordings_flock_record_date
ON recordings (project_flock_id, record_date)
WHERE deleted_at IS NULL;
COMMIT;
@@ -0,0 +1,168 @@
BEGIN;
-- Drop trigger & helper function tied to record_date before removing the column
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
DROP FUNCTION IF EXISTS trg_set_record_date();
-- Drop indexes and constraints that reference legacy columns
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock,
DROP CONSTRAINT IF EXISTS chk_recordings_status,
DROP CONSTRAINT IF EXISTS chk_recordings_ontime,
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives;
-- Align recordings header with the new schema
ALTER TABLE recordings
RENAME COLUMN project_flock_id TO project_flock_kandangs_id;
ALTER TABLE recordings
DROP COLUMN IF EXISTS record_date,
DROP COLUMN IF EXISTS status,
DROP COLUMN IF EXISTS ontime,
DROP COLUMN IF EXISTS daily_depletion_rate,
DROP COLUMN IF EXISTS cum_depletion;
ALTER TABLE recordings
RENAME COLUMN total_depletion TO total_depletion_qty;
ALTER TABLE recordings
RENAME COLUMN total_chick TO total_chick_qty;
ALTER TABLE recordings
ALTER COLUMN total_depletion_qty TYPE NUMERIC(15,3) USING COALESCE(total_depletion_qty, 0)::NUMERIC(15,3),
ALTER COLUMN total_chick_qty TYPE NUMERIC(15,3) USING COALESCE(total_chick_qty, 0)::NUMERIC(15,3),
ALTER COLUMN cum_intake TYPE INT USING COALESCE(cum_intake, 0)::INT;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock_kandang
FOREIGN KEY (project_flock_kandangs_id) REFERENCES project_flock_kandangs(id);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0)
);
CREATE INDEX idx_recordings_flock_datetime
ON recordings (project_flock_kandangs_id, record_datetime);
-- recording_bws reshape
ALTER TABLE recording_bws
RENAME COLUMN weight TO avg_weight;
ALTER TABLE recording_bws
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
ALTER TABLE recording_bws
ADD COLUMN total_weight NUMERIC(10,3);
UPDATE recording_bws
SET total_weight = COALESCE(avg_weight, 0) * COALESCE(qty, 0);
ALTER TABLE recording_bws
ALTER COLUMN total_weight SET NOT NULL;
ALTER TABLE recording_bws
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
ALTER TABLE recording_bws
DROP COLUMN IF EXISTS notes;
ALTER TABLE recording_bws
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
ALTER TABLE recording_bws
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (
avg_weight >= 0 AND qty >= 0 AND total_weight >= 0
);
-- recording_depletions reshape
ALTER TABLE recording_depletions
RENAME COLUMN total TO qty;
ALTER TABLE recording_depletions
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
ALTER TABLE recording_depletions
DROP COLUMN IF EXISTS notes;
ALTER TABLE recording_depletions
DROP CONSTRAINT IF EXISTS chk_recording_depl_total;
ALTER TABLE recording_depletions
ADD CONSTRAINT chk_recording_depl_qty CHECK (qty >= 0);
-- recording_stocks reshape
ALTER TABLE recording_stocks
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
ALTER TABLE recording_stocks
DROP COLUMN IF EXISTS increase,
DROP COLUMN IF EXISTS decrease,
DROP COLUMN IF EXISTS usage_amount,
DROP COLUMN IF EXISTS notes;
ALTER TABLE recording_stocks
ADD COLUMN usage_qty NUMERIC(15,3),
ADD COLUMN pending_qty NUMERIC(15,3);
ALTER TABLE recording_stocks
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
(usage_qty IS NULL OR usage_qty >= 0) AND
(pending_qty IS NULL OR pending_qty >= 0)
);
-- recording_eggs table
CREATE TABLE recording_eggs (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
qty INT NOT NULL,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_recording_eggs_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT fk_recording_eggs_product_warehouse
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
CONSTRAINT fk_recording_eggs_created_by
FOREIGN KEY (created_by) REFERENCES users(id),
CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0)
);
CREATE INDEX idx_recording_eggs_recording
ON recording_eggs (recording_id);
CREATE INDEX idx_recording_eggs_product
ON recording_eggs (product_warehouse_id);
-- grading_eggs table
CREATE TABLE grading_eggs (
id BIGSERIAL PRIMARY KEY,
recording_egg_id BIGINT NOT NULL,
qty NUMERIC(15,3) NOT NULL,
grade VARCHAR,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_grading_eggs_recording_egg
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
CONSTRAINT fk_grading_eggs_created_by
FOREIGN KEY (created_by) REFERENCES users(id),
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
);
CREATE INDEX idx_grading_eggs_recording_egg
ON grading_eggs (recording_egg_id);
COMMIT;
+91 -371
View File
@@ -8,7 +8,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -41,22 +40,15 @@ func Run(db *gorm.DB) error {
return err return err
} }
flocks, err := seedFlocks(tx, adminID) if _, err := seedFlocks(tx, adminID); err != nil {
if err != nil {
return err return err
} }
fcrs, err := seedFcr(tx, adminID) if _, err := seedFcr(tx, adminID); err != nil {
if err != nil {
return err return err
} }
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations) kandangs, err := seedKandangs(tx, adminID, locations, users)
if err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users, projectFlocks)
if err != nil { if err != nil {
return err return err
} }
@@ -93,10 +85,6 @@ func Run(db *gorm.DB) error {
if err := seedTransferStock(tx, adminID); err != nil { if err := seedTransferStock(tx, adminID); err != nil {
return err return err
} }
if err := seedChickin(tx, adminID); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed") fmt.Println("✅ Master data seeding completed")
return nil return nil
}) })
@@ -243,159 +231,16 @@ func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil return result, nil
} }
func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locations map[string]uint) (map[string]uint, error) { func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
seeds := []struct {
Key string
Flock string
Area string
Category utils.ProjectFlockCategory
Fcr string
Location string
Period int
}{
{
Key: "Singaparna Period 1",
Flock: "Flock Priangan",
Area: "Priangan",
Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer",
Location: "Singaparna",
Period: 1,
},
{
Key: "Cikaum Period 1",
Flock: "Flock Banten",
Area: "Banten",
Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer",
Location: "Cikaum",
Period: 1,
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
flockID, ok := flocks[seed.Flock]
if !ok {
return nil, fmt.Errorf("floc %s not seeded", seed.Flock)
}
areaID, ok := areas[seed.Area]
if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area)
}
fcrID, ok := fcrs[seed.Fcr]
if !ok {
return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr)
}
locationID, ok := locations[seed.Location]
if !ok {
return nil, fmt.Errorf("location %s not seeded", seed.Location)
}
var projectFlock entity.ProjectFlock
err := tx.Where("flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
flockID, areaID, seed.Category, fcrID, locationID, seed.Period).First(&projectFlock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
projectFlock = entity.ProjectFlock{
FlockId: flockID,
AreaId: areaID,
Category: string(seed.Category),
FcrId: fcrID,
LocationId: locationID,
Period: seed.Period,
CreatedBy: createdBy,
}
if err := tx.Create(&projectFlock).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
"flock_id": flockID,
"area_id": areaID,
"category": string(seed.Category),
"fcr_id": fcrID,
"location_id": locationID,
"period": seed.Period,
}).Error; err != nil {
return nil, err
}
}
if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil {
return nil, err
}
result[seed.Key] = projectFlock.Id
}
return result, nil
}
func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint) error {
if projectFlockID == 0 || actorID == 0 {
return nil
}
workflow := utils.ApprovalWorkflowProjectFlock.String()
steps := []struct {
step approvalutils.ApprovalStep
action entity.ApprovalAction
}{
{step: utils.ProjectFlockStepPengajuan, action: entity.ApprovalActionCreated},
{step: utils.ProjectFlockStepAktif, action: entity.ApprovalActionApproved},
}
for _, cfg := range steps {
var count int64
if err := tx.Model(&entity.Approval{}).
Where("approvable_type = ? AND approvable_id = ? AND step_number = ?", workflow, projectFlockID, uint16(cfg.step)).
Count(&count).Error; err != nil {
return err
}
if count > 0 {
continue
}
stepName, ok := utils.ProjectFlockApprovalSteps[cfg.step]
if !ok || strings.TrimSpace(stepName) == "" {
stepName = fmt.Sprintf("Step %d", cfg.step)
}
var actionPtr *entity.ApprovalAction
action := cfg.action
actionPtr = &action
record := entity.Approval{
ApprovableType: workflow,
ApprovableId: projectFlockID,
StepNumber: uint16(cfg.step),
StepName: stepName,
Action: actionPtr,
ActionBy: uintPtr(actorID),
}
if err := tx.Create(&record).Error; err != nil {
return err
}
}
return nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint, projectFlocks map[string]uint) (map[string]uint, error) {
seeds := []struct { seeds := []struct {
Name string Name string
Status utils.KandangStatus Status utils.KandangStatus
Location string Location string
PicKey string PicKey string
ProjectFlockKey *string
}{ }{
{Name: "Singaparna 1", Status: utils.KandangStatusActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")}, {Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"}, {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusActive, Location: "Cikaum", PicKey: "admin", ProjectFlockKey: strPtr("Cikaum Period 1")}, {Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"}, {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
} }
@@ -411,15 +256,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return nil, fmt.Errorf("user %s not seeded", seed.PicKey) return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
} }
var projectFlockID *uint
if seed.ProjectFlockKey != nil {
pfID, ok := projectFlocks[*seed.ProjectFlockKey]
if !ok {
return nil, fmt.Errorf("project flock %s not seeded", *seed.ProjectFlockKey)
}
projectFlockID = uintPtr(pfID)
}
var kandang entity.Kandang var kandang entity.Kandang
err := tx.Where("name = ?", seed.Name).First(&kandang).Error err := tx.Where("name = ?", seed.Name).First(&kandang).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -428,15 +264,11 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
Status: string(seed.Status), Status: string(seed.Status),
LocationId: locID, LocationId: locID,
PicId: picID, PicId: picID,
ProjectFlockId: projectFlockID,
CreatedBy: createdBy, CreatedBy: createdBy,
} }
if err := tx.Create(&kandang).Error; err != nil { if err := tx.Create(&kandang).Error; err != nil {
return nil, err return nil, err
} }
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err
}
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} else { } else {
@@ -445,17 +277,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
"pic_id": picID, "pic_id": picID,
"status": string(seed.Status), "status": string(seed.Status),
} }
if projectFlockID != nil {
updates["project_flock_id"] = *projectFlockID
} else {
updates["project_flock_id"] = nil
}
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil { if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
return nil, err return nil, err
} }
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err
}
} }
result[seed.Name] = kandang.Id result[seed.Name] = kandang.Id
} }
@@ -463,38 +287,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return result, nil return result, nil
} }
func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint) error {
if err := detachActivePivot(tx, kandangID); err != nil {
return err
}
if projectFlockID == nil {
return nil
}
return ensureActivePivot(tx, *projectFlockID, kandangID)
}
func detachActivePivot(tx *gorm.DB, kandangID uint) error {
return tx.Where("kandang_id = ?", kandangID).
Delete(&entity.ProjectFlockKandang{}).Error
}
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error {
var pivot entity.ProjectFlockKandang
err := tx.Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&pivot).Error
if err == nil {
return nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
newRecord := entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: kandangID,
}
return tx.Create(&newRecord).Error
}
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error { func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -571,8 +363,10 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error)
Name string Name string
Code string Code string
}{ }{
{"Pullet", "PLT"},
{"Bahan Baku", "RAW"}, {"Bahan Baku", "RAW"},
{"Day Old Chick", "DOC"}, {"Day Old Chick", "DOC"},
{"Telur", "EGG"},
} }
result := make(map[string]uint, len(seeds)) result := make(map[string]uint, len(seeds))
@@ -776,6 +570,54 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagDOC}, Flags: []utils.FlagType{utils.FlagDOC},
}, },
{
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Telur Konsumsi Baik",
Brand: "-",
Sku: "4",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{ {
Name: "281 SPECIAL STARTER", Name: "281 SPECIAL STARTER",
Brand: "281 STARTER", Brand: "281 STARTER",
@@ -1026,25 +868,44 @@ func seedBanks(tx *gorm.DB, createdBy uint) error {
} }
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error { func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
seeds := []struct { seeds := []struct {
ProductID uint ProductName string
WarehouseID uint WarehouseName string
Quantity float64 Quantity float64
}{ }{
{ProductID: 1, WarehouseID: 1, Quantity: 100}, {ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 100},
{ProductID: 2, WarehouseID: 2, Quantity: 200}, {ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
{ProductID: 2, WarehouseID: 1, Quantity: 300}, {ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
{ProductID: 1, WarehouseID: 3, Quantity: 5000}, {ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
} }
for _, seed := range seeds { for _, seed := range seeds {
var product entity.Product
if err := tx.Where("name = ?", seed.ProductName).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("product %q not found for product warehouse seeding", seed.ProductName)
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("name = ?", seed.WarehouseName).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("warehouse %q not found for product warehouse seeding", seed.WarehouseName)
}
return err
}
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
err := tx.Where("product_id = ? AND warehouse_id = ?", seed.ProductID, seed.WarehouseID).First(&productWarehouse).Error err := tx.Where("product_id = ? AND warehouse_id = ?", product.Id, warehouse.Id).First(&productWarehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
productWarehouse = entity.ProductWarehouse{ productWarehouse = entity.ProductWarehouse{
ProductId: seed.ProductID, ProductId: product.Id,
WarehouseId: seed.WarehouseID, WarehouseId: warehouse.Id,
Quantity: seed.Quantity, Quantity: seed.Quantity,
CreatedBy: createdBy, CreatedBy: createdBy,
} }
@@ -1053,6 +914,12 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
} }
} else if err != nil { } else if err != nil {
return err return err
} else {
if err := tx.Model(&productWarehouse).Updates(map[string]any{
"quantity": seed.Quantity,
}).Error; err != nil {
return err
}
} }
} }
@@ -1133,153 +1000,6 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error {
return nil return nil
} }
func seedChickin(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
ProjectFlockKandangId uint
ChickInDate string
Quantity float64
Note string
}{
{ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"},
{ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"},
}
for _, seed := range seeds {
chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate)
if err != nil {
return err
}
// Insert ProjectChickin jika belum ada
var chickin entity.ProjectChickin
err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate).
First(&chickin).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
chickin = entity.ProjectChickin{
ProjectFlockKandangId: seed.ProjectFlockKandangId,
ChickInDate: chickinDate,
Quantity: seed.Quantity,
Note: seed.Note,
CreatedBy: createdBy,
}
if err := tx.Create(&chickin).Error; err != nil {
return err
}
} else if err != nil {
return err
}
var population entity.ProjectFlockPopulation
err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
population = entity.ProjectFlockPopulation{
ProjectFlockKandangId: seed.ProjectFlockKandangId,
InitialQuantity: seed.Quantity,
CurrentQuantity: seed.Quantity,
ReservedQuantity: 0,
CreatedBy: createdBy,
}
if err := tx.Create(&population).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
// Update population quantities
if err := tx.Model(&entity.ProjectFlockPopulation{}).
Where("id = ?", population.Id).
Updates(map[string]any{
"initial_quantity": population.InitialQuantity + seed.Quantity,
"current_quantity": population.CurrentQuantity + seed.Quantity,
"reserved_quantity": 0,
}).Error; err != nil {
return err
}
}
var pfk entity.ProjectFlockKandang
if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// no pivot found; skip creating details
continue
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil {
// if warehouse not found, cannot create details
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
var productWarehouses []entity.ProductWarehouse
err = tx.Table("product_warehouses").
Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
if err != nil {
return err
}
// If no product warehouses found, keep existing chickin.Quantity and skip details
if len(productWarehouses) == 0 {
continue
}
// sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne)
totalQty := 0.0
for _, pw := range productWarehouses {
totalQty += pw.Quantity
}
if chickin.Quantity != totalQty {
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil {
return err
}
chickin.Quantity = totalQty
}
for _, pw := range productWarehouses {
// ensure detail exists or create it with full pw.Quantity
var detail entity.ProjectChickinDetail
err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
detail = entity.ProjectChickinDetail{
ProjectChickinId: chickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: createdBy,
}
if err := tx.Create(&detail).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if detail.Quantity != pw.Quantity {
if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil {
return err
}
}
}
// zero out pw quantity
if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil {
return err
}
}
}
return nil
}
func ptr[T any](v T) *T { func ptr[T any](v T) *T {
return &v return &v
} }
+1 -2
View File
@@ -12,7 +12,6 @@ type Kandang struct {
Status string `gorm:"type:varchar(50);not null"` Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"` PicId uint `gorm:"not null"`
ProjectFlockId *uint `gorm:"column:project_flock_id"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
@@ -20,5 +19,5 @@ type Kandang struct {
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"` Pic User `gorm:"foreignKey:PicId;references:Id"`
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
} }
+1 -1
View File
@@ -10,7 +10,7 @@ const ()
type ProjectChickin struct { type ProjectChickin struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null"` ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
ChickInDate time.Time `gorm:"not null"` ChickInDate time.Time `gorm:"not null"`
Quantity float64 `gorm:"not null"` Quantity float64 `gorm:"not null"`
Note string `gorm:"type:text"` Note string `gorm:"type:text"`
@@ -8,7 +8,7 @@ import (
type ProjectFlockPopulation struct { type ProjectFlockPopulation struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null"` ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"` InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"` CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
ReservedQuantity float64 `gorm:"type:numeric(15,3)"` ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
@@ -18,5 +18,6 @@ type ProjectFlockPopulation struct {
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
} }
+5 -5
View File
@@ -8,23 +8,23 @@ import (
type ProjectFlock struct { type ProjectFlock struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` FlockName string `gorm:"type:varchar(255);not null;uniqueIndex"`
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
Category string `gorm:"type:varchar(20);not null"` Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"` FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` Period int `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"` Area Area `gorm:"foreignKey:AreaId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"` Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
} }
@@ -7,6 +7,9 @@ type ProjectFlockKandang struct {
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"` 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"` KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
} }
+19 -3
View File
@@ -8,11 +8,27 @@ import (
type Recording struct { type Recording struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` ProjectFlockKandangId uint `gorm:"column:project_flock_kandangs_id;not null;index"`
CreatedBy uint `gorm:"not null"` RecordDatetime time.Time `gorm:"column:record_datetime;not null"`
Day *int `gorm:"column:day"`
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
DailyGain *float64 `gorm:"column:daily_gain"`
AvgDailyGain *float64 `gorm:"column:avg_daily_gain"`
CumIntake *int `gorm:"column:cum_intake"`
FcrValue *float64 `gorm:"column:fcr_value"`
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
} }
+15
View File
@@ -0,0 +1,15 @@
package entities
import "time"
type RecordingBW struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
AvgWeight float64 `gorm:"column:avg_weight;not null"`
Qty float64 `gorm:"column:qty;not null"`
TotalWeight float64 `gorm:"column:total_weight;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
}
+11
View File
@@ -0,0 +1,11 @@
package entities
type RecordingDepletion struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty float64 `gorm:"column:qty;not null"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+30
View File
@@ -0,0 +1,30 @@
package entities
import "time"
type RecordingEgg struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty int `gorm:"column:qty;not null"`
CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
}
type GradingEgg struct {
Id uint `gorm:"primaryKey"`
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
Qty float64 `gorm:"column:qty;not null"`
Grade string `gorm:"column:grade;type:varchar(50)"`
CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+12
View File
@@ -0,0 +1,12 @@
package entities
type RecordingStock struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
UsageQty *float64 `gorm:"column:usage_qty"`
PendingQty *float64 `gorm:"column:pending_qty"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+177 -85
View File
@@ -1,101 +1,193 @@
package middleware package middleware
// import ( import (
// "strings" "strings"
// "gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
// service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
// "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/sso"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
// "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
// ) )
// func Auth(userService service.UserService, requiredRights ...string) fiber.Handler { const (
// return func(c *fiber.Ctx) error { authContextLocalsKey = "auth.context"
// authHeader := c.Get("Authorization") authUserLocalsKey = "auth.user"
// token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")) )
// if token == "" { // AuthContext keeps authentication details captured by the middleware.
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") type AuthContext struct {
// } Token string
Verification *sso.VerificationResult
User *entity.User
Roles []sso.Role
Permissions map[string]struct{}
}
// userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess) // Auth validates the incoming request against the central SSO access token and
// if err != nil { // loads the corresponding local user. Optional scopes can be provided to enforce
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // fine-grained authorization using the SSO access token scopes.
// } func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
token := bearerToken(c)
if token == "" {
token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName))
}
if token == "" {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// // Only end-user subjects are allowed by this middleware. Service tokens verification, err := sso.VerifyAccessToken(token)
// if verification.UserID == 0 { if err != nil {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") utils.Log.WithError(err).Warn("auth: token verification failed")
// } return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// // Fail-closed on revocation check errors for stricter security posture. if verification.UserID == 0 {
// if revoker := session.GetRevocationStore(); revoker != nil { return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint")
// if fingerprint := session.TokenFingerprint(token); fingerprint != "" { }
// revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
// if err != nil {
// utils.Log.WithError(err).Warn("failed to check token revocation")
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// if revoked {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// }
// }
// user, err := userService.GetBySSOUserID(c, verification.UserID) if err := ensureNotRevoked(c, token, verification); err != nil {
// if err != nil || user == nil { return err
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") }
// }
// if len(requiredRights) > 0 && verification.Claims != nil { user, err := userService.GetBySSOUserID(c, verification.UserID)
// if !hasAllScopes(verification.Claims.Scopes(), requiredRights) { if err != nil || user == nil {
// return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") utils.Log.WithError(err).Warn("auth: failed to resolve user from repository")
// } return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// } }
// c.Locals("user", user) if len(requiredScopes) > 0 {
if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) {
return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
}
}
// // if len(requiredRights) > 0 { var roles []sso.Role
// // userRights, hasRights := config.RoleRights[user.Role] permissions := make(map[string]struct{})
// // if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID { if verification.UserID != 0 {
// // return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource") if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil {
// // } utils.Log.WithError(err).Warn("auth: failed to fetch sso profile")
// // } } else if profile != nil {
roles = profile.Roles
for _, perm := range profile.PermissionNames() {
if perm != "" {
permissions[perm] = struct{}{}
}
}
}
}
// return c.Next() ctx := &AuthContext{
// } Token: token,
// } Verification: verification,
User: user,
Roles: roles,
Permissions: permissions,
}
// // bearerToken extracts a Bearer token from the Authorization header using c.Locals(authContextLocalsKey, ctx)
// // case-insensitive scheme matching and tolerant whitespace handling. c.Locals(authUserLocalsKey, user)
// func bearerToken(c *fiber.Ctx) string {
// parts := strings.Fields(c.Get("Authorization"))
// if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
// return strings.TrimSpace(parts[1])
// }
// return ""
// }
// func hasAllScopes(have, required []string) bool { return c.Next()
// if len(required) == 0 { }
// return true }
// }
// set := make(map[string]struct{}, len(have)) // AuthenticatedUser returns the authenticated user populated by Auth.
// for _, s := range have { func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
// s = strings.ToLower(strings.TrimSpace(s)) value := c.Locals(authUserLocalsKey)
// if s != "" { if user, ok := value.(*entity.User); ok && user != nil {
// set[s] = struct{}{} return user, true
// } }
// } return nil, false
// for _, r := range required { }
// r = strings.ToLower(strings.TrimSpace(r))
// if r == "" { // AuthDetails returns the full authentication context (token, claims, user).
// continue func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) {
// } value := c.Locals(authContextLocalsKey)
// if _, ok := set[r]; !ok { if ctx, ok := value.(*AuthContext); ok && ctx != nil {
// return false return ctx, true
// } }
// } return nil, false
// return true }
// }
// ensureNotRevoked ensures the token is not revoked or superseded by a forced logout.
func ensureNotRevoked(c *fiber.Ctx, token string, verification *sso.VerificationResult) error {
revoker := session.GetRevocationStore()
if revoker == nil {
return nil
}
if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
if err != nil {
utils.Log.WithError(err).Warn("auth: token revocation check failed")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
if revoked {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
}
if verification.UserID == 0 {
return nil
}
logoutAt, err := revoker.UserLogoutTime(c.Context(), verification.UserID)
if err != nil {
utils.Log.WithError(err).Warn("auth: failed to load user logout marker")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
if logoutAt.IsZero() {
return nil
}
claims := verification.Claims
if claims == nil || claims.IssuedAt == nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
issuedAt := claims.IssuedAt.Time
// Treat tokens issued at or before the forced logout timestamp as invalid.
if !issuedAt.After(logoutAt) {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
return nil
}
// bearerToken extracts a Bearer token from the Authorization header using
// case-insensitive scheme matching and tolerant whitespace handling.
func bearerToken(c *fiber.Ctx) string {
parts := strings.Fields(c.Get("Authorization"))
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
return strings.TrimSpace(parts[1])
}
return ""
}
func hasAllScopes(have, required []string) bool {
if len(required) == 0 {
return true
}
set := make(map[string]struct{}, len(have))
for _, s := range have {
s = strings.ToLower(strings.TrimSpace(s))
if s != "" {
set[s] = struct{}{}
}
}
for _, r := range required {
r = strings.ToLower(strings.TrimSpace(r))
if r == "" {
continue
}
if _, ok := set[r]; !ok {
return false
}
}
return true
}
+75
View File
@@ -0,0 +1,75 @@
package middleware
import (
"strings"
"github.com/gofiber/fiber/v2"
)
// RequirePermissions ensures the authenticated user possesses all specified permissions.
func RequirePermissions(perms ...string) fiber.Handler {
required := canonicalPermissions(perms)
return func(c *fiber.Ctx) error {
if len(required) == 0 {
return c.Next()
}
ctx, ok := AuthDetails(c)
if !ok || ctx == nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
userPerms := ctx.permissionSet()
if len(userPerms) == 0 {
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
}
for _, perm := range required {
if _, has := userPerms[perm]; !has {
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
}
}
return c.Next()
}
}
// HasPermission reports whether the current request context includes the given permission.
func HasPermission(c *fiber.Ctx, perm string) bool {
ctx, ok := AuthDetails(c)
if !ok || ctx == nil {
return false
}
perm = canonicalPermission(perm)
if perm == "" {
return false
}
_, has := ctx.permissionSet()[perm]
return has
}
func (a *AuthContext) permissionSet() map[string]struct{} {
if a == nil || a.Permissions == nil {
return nil
}
return a.Permissions
}
func canonicalPermissions(perms []string) []string {
out := make([]string, 0, len(perms))
seen := make(map[string]struct{}, len(perms))
for _, perm := range perms {
if canonical := canonicalPermission(perm); canonical != "" {
if _, ok := seen[canonical]; ok {
continue
}
seen[canonical] = struct{}{}
out = append(out, canonical)
}
}
return out
}
func canonicalPermission(perm string) string {
return strings.ToLower(strings.TrimSpace(perm))
}
@@ -202,21 +202,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
if query.TransactionType != "" { if query.TransactionType != "" {
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType)) db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
} }
if query.ProductID > 0 { db = s.StockLogsRepository.ApplyProductWarehouseFilters(db, uint(query.ProductID), uint(query.WarehouseID))
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
Where("product_warehouses.product_id = ?", query.ProductID)
}
if query.WarehouseID > 0 {
if query.ProductID > 0 {
db = db.Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
} else {
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
}
}
return db.Order("created_at DESC") return db.Order("created_at DESC")
}) })
@@ -28,6 +28,11 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
ProductId: uint(c.QueryInt("product_id", 0)), ProductId: uint(c.QueryInt("product_id", 0)),
WarehouseId: uint(c.QueryInt("warehouse_id", 0)), WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
Flags: c.Query("flags", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
} }
result, totalResults, err := u.ProductWarehouseService.GetAll(c, query) result, totalResults, err := u.ProductWarehouseService.GetAll(c, query)
@@ -71,5 +76,3 @@ func (u *ProductWarehouseController) GetOne(c *fiber.Ctx) error {
Data: dto.ToProductWarehouseListDTO(*result), Data: dto.ToProductWarehouseListDTO(*result),
}) })
} }
@@ -23,4 +23,3 @@ func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, v
ProductWarehouseRoutes(router, userService, productWarehouseService) ProductWarehouseRoutes(router, userService, productWarehouseService)
} }
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -16,23 +17,36 @@ type ProductWarehouseRepository interface {
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
ExistsByID(ctx context.Context, id uint) (bool, error) ExistsByID(ctx context.Context, id uint) (bool, error)
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
} }
type ProductWarehouseRepositoryImpl struct { type ProductWarehouseRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProductWarehouse] *repository.BaseRepositoryImpl[entity.ProductWarehouse]
db *gorm.DB
} }
func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository { func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
return &ProductWarehouseRepositoryImpl{ return &ProductWarehouseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db),
db: db,
} }
} }
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.DB(), productId)
}
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
return repository.Exists[entity.Warehouse](ctx, r.DB(), warehouseId)
}
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
}
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
var count int64 var count int64
query := r.db.WithContext(ctx).Model(&entity.ProductWarehouse{}). query := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId) Where("product_id = ? AND warehouse_id = ?", productId, warehouseId)
if excludeID != nil { if excludeID != nil {
query = query.Where("id != ?", *excludeID) query = query.Where("id != ?", *excludeID)
@@ -43,20 +57,9 @@ func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Cont
return count > 0, nil return count > 0, nil
} }
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.db, productId)
}
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
return repository.Exists[entity.Warehouse](ctx, r.db, warehouseId)
}
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id)
}
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
var count int64 var count int64
if err := r.db.WithContext(ctx). if err := r.DB().WithContext(ctx).
Model(&entity.ProductWarehouse{}). Model(&entity.ProductWarehouse{}).
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId). Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
Count(&count).Error; err != nil { Count(&count).Error; err != nil {
@@ -72,3 +75,74 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
} }
return &productWarehouse, nil return &productWarehouse, nil
} }
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.*").
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
if err != nil {
return nil, err
}
return productWarehouses, nil
}
func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
query := r.DB()
if db != nil {
query = db
}
fmt.Println(warehouseId)
err := query.WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC").
First(&productWarehouse).Error
if err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB {
if len(flags) == 0 {
return db
}
return db.
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
Where("flags.name IN ?", flags)
}
func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error {
if len(deltas) == 0 {
return nil
}
base := r.DB().WithContext(ctx)
if modifier != nil {
base = modifier(base)
}
for id, delta := range deltas {
if delta == 0 {
continue
}
if err := base.Model(&entity.ProductWarehouse{}).
Where("id = ?", id).
Update("quantity", gorm.Expr("COALESCE(quantity,0) + ?", delta)).Error; err != nil {
return err
}
}
return nil
}
@@ -1,7 +1,7 @@
package productWarehouses package productWarehouses
import ( 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/inventory/product-warehouses/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/controllers"
productWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services" productWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func ProductWarehouseRoutes(v1 fiber.Router, u user.UserService, s productWareho
ctrl := controller.NewProductWarehouseController(s) ctrl := controller.NewProductWarehouseController(s)
route := v1.Group("/product-warehouses") route := v1.Group("/product-warehouses")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne) route.Get("/:id", ctrl.GetOne)
@@ -49,8 +49,30 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
return nil, 0, err return nil, 0, err
} }
if params.ProductId > 0 {
isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId)
if err != nil {
return nil, 0, err
}
if !isProductExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
}
if params.WarehouseId > 0 {
isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), params.WarehouseId)
if err != nil {
return nil, 0, err
}
if !isWarehouseExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
cleanFlags := utils.ParseFlags(params.Flags)
productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
@@ -62,6 +84,8 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
db = db.Where("warehouse_id = ?", params.WarehouseId) db = db.Where("warehouse_id = ?", params.WarehouseId)
} }
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -17,4 +17,5 @@ type Query struct {
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
Flags string `query:"flags" validate:"omitempty"`
} }
+1 -1
View File
@@ -7,8 +7,8 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments" adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers" transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -1,7 +1,7 @@
package transfers package transfers
import ( 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/inventory/transfers/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/controllers"
transfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services" transfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
ctrl := controller.NewTransferController(s) ctrl := controller.NewTransferController(s)
route := v1.Group("/transfers") route := v1.Group("/transfers")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -29,6 +29,10 @@ func (u *AreaController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), 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.AreaService.GetAll(c, query) result, totalResults, err := u.AreaService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
-1
View File
@@ -23,4 +23,3 @@ func (AreaModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *val
AreaRoutes(router, userService, areaService) AreaRoutes(router, userService, areaService)
} }
+2 -7
View File
@@ -1,7 +1,7 @@
package areas package areas
import ( 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/areas/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/controllers"
area "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services" area "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func AreaRoutes(v1 fiber.Router, u user.UserService, s area.AreaService) {
ctrl := controller.NewAreaController(s) ctrl := controller.NewAreaController(s)
route := v1.Group("/areas") route := v1.Group("/areas")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -29,6 +29,10 @@ func (u *BankController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), 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.BankService.GetAll(c, query) result, totalResults, err := u.BankService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
-1
View File
@@ -23,4 +23,3 @@ func (BankModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *val
BankRoutes(router, userService, bankService) BankRoutes(router, userService, bankService)
} }
@@ -11,6 +11,7 @@ import (
type BankRepository interface { type BankRepository interface {
repository.BaseRepository[entity.Bank] repository.BaseRepository[entity.Bank]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
AccountNumberExists(ctx context.Context, accountNumber string, excludeID *uint) (bool, error)
} }
type BankRepositoryImpl struct { type BankRepositoryImpl struct {
@@ -28,3 +29,7 @@ func NewBankRepository(db *gorm.DB) BankRepository {
func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID) return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID)
} }
func (r *BankRepositoryImpl) AccountNumberExists(ctx context.Context, accountNumber string, excludeID *uint) (bool, error) {
return repository.ExistsByField[entity.Bank](ctx, r.db, "account_number", accountNumber, excludeID)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package banks package banks
import ( 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/banks/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/controllers"
bank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services" bank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func BankRoutes(v1 fiber.Router, u user.UserService, s bank.BankService) {
ctrl := controller.NewBankController(s) ctrl := controller.NewBankController(s)
route := v1.Group("/banks") route := v1.Group("/banks")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -87,6 +87,13 @@ func (s *bankService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.B
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name)) return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name))
} }
if exists, err := s.Repository.AccountNumberExists(c.Context(), req.AccountNumber, nil); err != nil {
s.Log.Errorf("Failed to check bank account number: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank account number")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with account number %s already exists", req.AccountNumber))
}
createBody := &entity.Bank{ createBody := &entity.Bank{
Name: req.Name, Name: req.Name,
Alias: req.Alias, Alias: req.Alias,
@@ -29,6 +29,10 @@ func (u *CustomerController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), 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.CustomerService.GetAll(c, query) result, totalResults, err := u.CustomerService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -23,4 +23,3 @@ func (CustomerModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
CustomerRoutes(router, userService, customerService) CustomerRoutes(router, userService, customerService)
} }
+2 -7
View File
@@ -1,7 +1,7 @@
package customers package customers
import ( 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/customers/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/controllers"
customer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services" customer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func CustomerRoutes(v1 fiber.Router, u user.UserService, s customer.CustomerServ
ctrl := controller.NewCustomerController(s) ctrl := controller.NewCustomerController(s)
route := v1.Group("/customers") route := v1.Group("/customers")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -29,6 +29,10 @@ func (u *FcrController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), 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.FcrService.GetAll(c, query) result, totalResults, err := u.FcrService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
+2 -7
View File
@@ -1,7 +1,7 @@
package fcrs package fcrs
import ( 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/fcrs/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/controllers"
fcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services" fcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func FcrRoutes(v1 fiber.Router, u user.UserService, s fcr.FcrService) {
ctrl := controller.NewFcrController(s) ctrl := controller.NewFcrController(s)
route := v1.Group("/fcrs") route := v1.Group("/fcrs")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -29,6 +29,10 @@ func (u *FlockController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), 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.FlockService.GetAll(c, query) result, totalResults, err := u.FlockService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -11,6 +11,7 @@ import (
type FlockRepository interface { type FlockRepository interface {
repository.BaseRepository[entity.Flock] repository.BaseRepository[entity.Flock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
GetByName(ctx context.Context, name string) (*entity.Flock, error)
} }
type FlockRepositoryImpl struct { type FlockRepositoryImpl struct {
@@ -28,3 +29,15 @@ func NewFlockRepository(db *gorm.DB) FlockRepository {
func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID) return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID)
} }
func (r *FlockRepositoryImpl) GetByName(ctx context.Context, name string) (*entity.Flock, error) {
var flock entity.Flock
err := r.db.WithContext(ctx).
Where("LOWER(name) = LOWER(?)", name).
Where("deleted_at IS NULL").
First(&flock).Error
if err != nil {
return nil, err
}
return &flock, nil
}
+2 -7
View File
@@ -1,7 +1,7 @@
package flocks package flocks
import ( 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/flocks/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/controllers"
flock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/services" flock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func FlockRoutes(v1 fiber.Router, u user.UserService, s flock.FlockService) {
ctrl := controller.NewFlockController(s) ctrl := controller.NewFlockController(s)
route := v1.Group("/flocks") route := v1.Group("/flocks")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -31,6 +31,10 @@ func (u *KandangController) GetAll(c *fiber.Ctx) error {
PicId: c.QueryInt("pic_id", 0), PicId: c.QueryInt("pic_id", 0),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.KandangService.GetAll(c, query) result, totalResults, err := u.KandangService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -23,4 +23,3 @@ func (KandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
KandangRoutes(router, userService, kandangService) KandangRoutes(router, userService, kandangService)
} }
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"errors"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -18,6 +19,8 @@ type KandangRepository interface {
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error)
HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error)
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error
UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error
} }
type KandangRepositoryImpl struct { type KandangRepositoryImpl struct {
@@ -59,12 +62,13 @@ func (r *KandangRepositoryImpl) ProjectFlockExists(ctx context.Context, projectF
func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) { func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) {
var count int64 var count int64
q := r.db.WithContext(ctx). q := r.db.WithContext(ctx).
Model(&entity.Kandang{}). Table("kandangs k").
Where("project_flock_id = ?", projectFlockID). Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
Where("status = ?", utils.KandangStatusActive). Where("pfk.project_flock_id = ?", projectFlockID).
Where("deleted_at IS NULL") Where("k.status = ?", utils.KandangStatusActive).
Where("k.deleted_at IS NULL")
if excludeID != nil { if excludeID != nil {
q = q.Where("id <> ?", *excludeID) q = q.Where("k.id <> ?", *excludeID)
} }
if err := q.Count(&count).Error; err != nil { if err := q.Count(&count).Error; err != nil {
return false, err return false, err
@@ -75,17 +79,58 @@ func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Cont
func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) { func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) {
kandang := new(entity.Kandang) kandang := new(entity.Kandang)
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Where("project_flock_id = ?", projectFlockID). Table("kandangs k").
First(kandang).Error Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
Where("pfk.project_flock_id = ?", projectFlockID).
Where("k.deleted_at IS NULL").
Order("k.id ASC").
Limit(1).
Find(kandang).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
if kandang.Id == 0 {
return nil, gorm.ErrRecordNotFound
}
return kandang, nil return kandang, nil
} }
func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error { func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error {
sub := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Select("kandang_id").
Where("project_flock_id = ?", projectFlockID)
return r.db.WithContext(ctx). return r.db.WithContext(ctx).
Model(&entity.Kandang{}). Model(&entity.Kandang{}).
Where("project_flock_id = ?", projectFlockID). Where("id IN (?)", sub).
Where("deleted_at IS NULL").
Update("status", string(status)).Error
}
func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error {
var link entity.ProjectFlockKandang
err := r.db.WithContext(ctx).
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&link).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
link = entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: kandangID,
}
return r.db.WithContext(ctx).Create(&link).Error
}
return err
}
func (r *KandangRepositoryImpl) UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error {
if len(kandangIDs) == 0 {
return nil
}
return r.db.WithContext(ctx).
Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs).
Where("deleted_at IS NULL").
Update("status", string(status)).Error Update("status", string(status)).Error
} }
+2 -7
View File
@@ -1,7 +1,7 @@
package kandangs package kandangs
import ( 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" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services" kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService
ctrl := controller.NewKandangController(s) ctrl := controller.NewKandangController(s)
route := v1.Group("/kandangs") route := v1.Group("/kandangs")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -40,7 +40,8 @@ func NewKandangService(repo repository.KandangRepository, validate *validator.Va
} }
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB { func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").Preload("Location").Preload("Pic") return db.Preload("CreatedUser").Preload("Location").Preload("Pic").Preload("ProjectFlockKandangs.ProjectFlock")
} }
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) { func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
@@ -110,7 +111,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status")
} }
var projectFlockID *uint
if req.ProjectFlockId != nil { if req.ProjectFlockId != nil {
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil { if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
s.Log.Errorf("Failed to check project flock existence: %+v", err) s.Log.Errorf("Failed to check project flock existence: %+v", err)
@@ -128,8 +128,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
} }
} }
idCopy := *req.ProjectFlockId
projectFlockID = &idCopy
} }
//TODO: created by dummy //TODO: created by dummy
@@ -138,7 +136,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
LocationId: req.LocationId, LocationId: req.LocationId,
Status: status, Status: status,
PicId: req.PicId, PicId: req.PicId,
ProjectFlockId: projectFlockID,
CreatedBy: 1, CreatedBy: 1,
} }
@@ -147,6 +144,12 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
if req.ProjectFlockId != nil {
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, createBody.Id); err != nil {
s.Log.Errorf("Failed to link kandang to project_flock via pivot: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
}
}
return s.GetOne(c, createBody.Id) return s.GetOne(c, createBody.Id)
} }
@@ -201,7 +204,6 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
finalStatus = status finalStatus = status
} }
projectFlockIDToUse := existing.ProjectFlockId
if req.ProjectFlockId != nil { if req.ProjectFlockId != nil {
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil { if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
s.Log.Errorf("Failed to check project flock existence: %+v", err) s.Log.Errorf("Failed to check project flock existence: %+v", err)
@@ -209,24 +211,19 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} else if !exists { } else if !exists {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId)) return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId))
} }
idCopy := *req.ProjectFlockId
projectFlockIDToUse = &idCopy
updateBody["project_flock_id"] = idCopy
}
if projectFlockIDToUse != nil && finalStatus == string(utils.KandangStatusActive) { // Kalau status jadi ACTIVE, pastikan tidak ada kandang aktif lain pada project flock tsb (hitung via pivot)
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *projectFlockIDToUse, &id); err != nil { if finalStatus == string(utils.KandangStatusActive) {
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *projectFlockIDToUse, err) if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *req.ProjectFlockId, &id); err != nil {
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *req.ProjectFlockId, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock")
} else if active { } else if active {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang") return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang")
} }
} }
if len(updateBody) == 0 {
return s.GetOne(c, id)
} }
if len(updateBody) > 0 {
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found") return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
@@ -234,6 +231,14 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
s.Log.Errorf("Failed to update kandang: %+v", err) s.Log.Errorf("Failed to update kandang: %+v", err)
return nil, err return nil, err
} }
}
if req.ProjectFlockId != nil {
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, id); err != nil {
s.Log.Errorf("Failed to upsert pivot kandang-project_flock: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
}
}
return s.GetOne(c, id) return s.GetOne(c, id)
} }
@@ -30,6 +30,10 @@ func (u *LocationController) GetAll(c *fiber.Ctx) error {
AreaId: c.QueryInt("area_id", 0), AreaId: c.QueryInt("area_id", 0),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.LocationService.GetAll(c, query) result, totalResults, err := u.LocationService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -23,4 +23,3 @@ func (LocationModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
LocationRoutes(router, userService, locationService) LocationRoutes(router, userService, locationService)
} }
+2 -7
View File
@@ -1,7 +1,7 @@
package locations package locations
import ( 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/locations/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/controllers"
location "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services" location "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func LocationRoutes(v1 fiber.Router, u user.UserService, s location.LocationServ
ctrl := controller.NewLocationController(s) ctrl := controller.NewLocationController(s)
route := v1.Group("/locations") route := v1.Group("/locations")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -29,6 +29,10 @@ func (u *NonstockController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), 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.NonstockService.GetAll(c, query) result, totalResults, err := u.NonstockService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -23,4 +23,3 @@ func (NonstockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
NonstockRoutes(router, userService, nonstockService) NonstockRoutes(router, userService, nonstockService)
} }
+2 -7
View File
@@ -1,7 +1,7 @@
package nonstocks package nonstocks
import ( 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/nonstocks/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/controllers"
nonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services" nonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func NonstockRoutes(v1 fiber.Router, u user.UserService, s nonstock.NonstockServ
ctrl := controller.NewNonstockController(s) ctrl := controller.NewNonstockController(s)
route := v1.Group("/nonstocks") route := v1.Group("/nonstocks")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -29,6 +29,10 @@ func (u *ProductCategoryController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), 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.ProductCategoryService.GetAll(c, query) result, totalResults, err := u.ProductCategoryService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -1,7 +1,7 @@
package productcategories package productcategories
import ( 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/product-categories/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/controllers"
productCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services" productCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func ProductCategoryRoutes(v1 fiber.Router, u user.UserService, s productCategor
ctrl := controller.NewProductCategoryController(s) ctrl := controller.NewProductCategoryController(s)
route := v1.Group("/product-categories") route := v1.Group("/product-categories")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -30,6 +30,10 @@ func (u *ProductController) GetAll(c *fiber.Ctx) error {
ProductCategoryID: c.QueryInt("product_category_id", 0), ProductCategoryID: c.QueryInt("product_category_id", 0),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ProductService.GetAll(c, query) result, totalResults, err := u.ProductService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -23,4 +23,3 @@ func (ProductModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
ProductRoutes(router, userService, productService) ProductRoutes(router, userService, productService)
} }
+2 -7
View File
@@ -1,7 +1,7 @@
package products package products
import ( 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/products/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/controllers"
product "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services" product "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func ProductRoutes(v1 fiber.Router, u user.UserService, s product.ProductService
ctrl := controller.NewProductController(s) ctrl := controller.NewProductController(s)
route := v1.Group("/products") route := v1.Group("/products")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
+1 -1
View File
@@ -11,6 +11,7 @@ import (
banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks" banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks"
customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers" customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers"
fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs" fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs"
flocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks"
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs" kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations" locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks" nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks"
@@ -19,7 +20,6 @@ import (
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers" suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms" uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses" warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
flocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -29,6 +29,10 @@ func (u *SupplierController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), 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.SupplierService.GetAll(c, query) result, totalResults, err := u.SupplierService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -23,4 +23,3 @@ func (SupplierModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
SupplierRoutes(router, userService, supplierService) SupplierRoutes(router, userService, supplierService)
} }
@@ -11,7 +11,7 @@ import (
type SupplierRepository interface { type SupplierRepository interface {
repository.BaseRepository[entity.Supplier] repository.BaseRepository[entity.Supplier]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
AliasExists(ctx context.Context, alias string, excludeID *uint) (bool, error)
} }
type SupplierRepositoryImpl struct { type SupplierRepositoryImpl struct {
@@ -29,3 +29,7 @@ func NewSupplierRepository(db *gorm.DB) SupplierRepository {
func (r *SupplierRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { func (r *SupplierRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Supplier](ctx, r.db, name, excludeID) return repository.ExistsByName[entity.Supplier](ctx, r.db, name, excludeID)
} }
func (r *SupplierRepositoryImpl) AliasExists(ctx context.Context, alias string, excludeID *uint) (bool, error) {
return repository.ExistsByField[entity.Supplier](ctx, r.db, "alias", alias, excludeID)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package suppliers package suppliers
import ( 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/suppliers/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/controllers"
supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services" supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func SupplierRoutes(v1 fiber.Router, u user.UserService, s supplier.SupplierServ
ctrl := controller.NewSupplierController(s) ctrl := controller.NewSupplierController(s)
route := v1.Group("/suppliers") route := v1.Group("/suppliers")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -88,6 +88,13 @@ func (s *supplierService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with name %s already exists", req.Name)) return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with name %s already exists", req.Name))
} }
if exists, err := s.Repository.AliasExists(c.Context(), strings.TrimSpace(strings.ToUpper(req.Alias)), nil); err != nil {
s.Log.Errorf("Failed to check supplier alias: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier alias")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with alias %s already exists", strings.TrimSpace(strings.ToUpper(req.Alias))))
}
typ := strings.ToUpper(req.Type) typ := strings.ToUpper(req.Type)
if !utils.IsValidCustomerSupplierType(typ) { if !utils.IsValidCustomerSupplierType(typ) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier type") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier type")
@@ -143,6 +150,12 @@ func (s supplierService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
} }
if req.Alias != nil { if req.Alias != nil {
if exists, err := s.Repository.AliasExists(c.Context(), strings.TrimSpace(strings.ToUpper(*req.Alias)), &id); err != nil {
s.Log.Errorf("Failed to check supplier alias: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier alias")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with alias %s already exists", strings.TrimSpace(strings.ToUpper(*req.Alias))))
}
updateBody["alias"] = strings.TrimSpace(strings.ToUpper(*req.Alias)) updateBody["alias"] = strings.TrimSpace(strings.ToUpper(*req.Alias))
} }
@@ -29,6 +29,10 @@ func (u *UomController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), 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.UomService.GetAll(c, query) result, totalResults, err := u.UomService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
-1
View File
@@ -23,4 +23,3 @@ func (UomModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *vali
UomRoutes(router, userService, uomService) UomRoutes(router, userService, uomService)
} }
+2 -7
View File
@@ -1,7 +1,7 @@
package uoms package uoms
import ( 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/uoms/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/controllers"
uom "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/services" uom "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func UomRoutes(v1 fiber.Router, u user.UserService, s uom.UomService) {
ctrl := controller.NewUomController(s) ctrl := controller.NewUomController(s)
route := v1.Group("/uoms") route := v1.Group("/uoms")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -30,6 +30,10 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
AreaId: c.QueryInt("area_id", 0), AreaId: c.QueryInt("area_id", 0),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.WarehouseService.GetAll(c, query) result, totalResults, err := u.WarehouseService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -23,4 +23,3 @@ func (WarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
WarehouseRoutes(router, userService, warehouseService) WarehouseRoutes(router, userService, warehouseService)
} }
+2 -7
View File
@@ -1,7 +1,7 @@
package warehouses package warehouses
import ( 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/warehouses/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/controllers"
warehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services" warehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func WarehouseRoutes(v1 fiber.Router, u user.UserService, s warehouse.WarehouseS
ctrl := controller.NewWarehouseController(s) ctrl := controller.NewWarehouseController(s)
route := v1.Group("/warehouses") route := v1.Group("/warehouses")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -9,6 +9,7 @@ import (
flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
@@ -88,9 +89,9 @@ func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO {
func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
var flock *flockBaseDTO.FlockBaseDTO var flock *flockBaseDTO.FlockBaseDTO
if e.Flock.Id != 0 { if base := pfutils.DeriveBaseName(e.FlockName); base != "" {
mapped := flockBaseDTO.ToFlockBaseDTO(e.Flock) summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base}
flock = &mapped flock = &summary
} }
var area *areaBaseDTO.AreaBaseDTO var area *areaBaseDTO.AreaBaseDTO
if e.Area.Id != 0 { if e.Area.Id != 0 {
@@ -1,7 +1,7 @@
package chickins package chickins
import ( 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/chickins/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/controllers"
chickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" chickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
ctrl := controller.NewChickinController(s) ctrl := controller.NewChickinController(s)
route := v1.Group("/chickins") route := v1.Group("/chickins")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -63,7 +63,6 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB {
Preload("ProjectFlockKandang.Kandang.Location.Area"). Preload("ProjectFlockKandang.Kandang.Location.Area").
Preload("ProjectFlockKandang.Kandang.Pic"). Preload("ProjectFlockKandang.Kandang.Pic").
Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.Flock").
Preload("ProjectFlockKandang.ProjectFlock.Area"). Preload("ProjectFlockKandang.ProjectFlock.Area").
Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("ProjectFlockKandang.ProjectFlock.Fcr").
Preload("ProjectFlockKandang.ProjectFlock.Location"). Preload("ProjectFlockKandang.ProjectFlock.Location").
@@ -121,14 +120,8 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
var productWarehouses []entity.ProductWarehouse // move complex DB query into repository for cleaner service
err = s.ProductWarehouseRepo.DB(). productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
WithContext(c.Context()).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("created_at DESC").
Find(&productWarehouses).Error
if err != nil { if err != nil {
s.Log.Errorf("Failed to get product warehouses: %+v", err) s.Log.Errorf("Failed to get product warehouses: %+v", err)
return nil, err return nil, err
@@ -136,8 +129,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
if len(productWarehouses) == 0 { if len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
} }
// Jumlahkan semua quantity DOC
totalQuantity := 0.0 totalQuantity := 0.0
for _, pw := range productWarehouses { for _, pw := range productWarehouses {
totalQuantity += pw.Quantity totalQuantity += pw.Quantity
@@ -147,7 +138,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses") return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
} }
// Buat satu chickin dengan total quantity
chickinDate, err := utils.ParseDateString(req.ChickInDate) chickinDate, err := utils.ParseDateString(req.ChickInDate)
if err != nil { if err != nil {
s.Log.Errorf("Failed to parse chickin date: %+v", err) s.Log.Errorf("Failed to parse chickin date: %+v", err)
@@ -157,7 +147,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
ProjectFlockKandangId: projectflockkandang.Id, ProjectFlockKandangId: projectflockkandang.Id,
ChickInDate: chickinDate, ChickInDate: chickinDate,
Quantity: totalQuantity, Quantity: totalQuantity,
Note: "", Note: req.Note,
CreatedBy: 1, //todo: ganti dengan user login CreatedBy: 1, //todo: ganti dengan user login
} }
err = s.Repository.CreateOne(c.Context(), newChickin, nil) err = s.Repository.CreateOne(c.Context(), newChickin, nil)
@@ -176,7 +166,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
// add ke detail chickin
newChickinDetail := &entity.ProjectChickinDetail{ newChickinDetail := &entity.ProjectChickinDetail{
ProjectChickinId: newChickin.Id, ProjectChickinId: newChickin.Id,
ProductWarehouseId: pw.Id, ProductWarehouseId: pw.Id,
@@ -232,6 +221,9 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if req.ChickInDate != "" { if req.ChickInDate != "" {
updateBody["chick_in_date"] = req.ChickInDate updateBody["chick_in_date"] = req.ChickInDate
} }
if req.Note != "" {
updateBody["note"] = req.Note
}
if len(updateBody) == 0 { if len(updateBody) == 0 {
return s.GetOne(c, id) return s.GetOne(c, id)
} }
@@ -293,7 +285,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
return rollback(err) return rollback(err)
} }
// helper: restore quantities from details; returns (restored bool, error)
restoreFromDetails := func() (bool, error) { restoreFromDetails := func() (bool, error) {
var details []entity.ProjectChickinDetail var details []entity.ProjectChickinDetail
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil { if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
@@ -348,15 +339,12 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
return rollback(err) return rollback(err)
} }
var productWarehouse entity.ProductWarehouse productWarehouse, err := s.ProductWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(
err = tx.WithContext(c.Context()).Table("product_warehouses"). c.Context(),
Select("product_warehouses.*"). "DOC",
Joins("JOIN products ON products.id = product_warehouses.product_id"). warehouse.Id,
Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). tx,
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id). )
Order("product_warehouses.created_at DESC").
First(&productWarehouse).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")) return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse"))
@@ -3,10 +3,12 @@ package validation
type Create struct { type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
Note string `json:"note" validate:"omitempty`
} }
type Update struct { type Update struct {
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
Note string `json:"note" validate:"omitempty"`
} }
type Query struct { type Query struct {
@@ -222,11 +222,11 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
} }
func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
param := c.Params("flock_id") param := c.Params("project_flock_kandang_id")
id, err := strconv.Atoi(param) id, err := strconv.Atoi(param)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Flock Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
} }
summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id)) summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id))
@@ -246,17 +246,39 @@ func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
} }
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
projectFlockIdStr := c.Query("project_flock_id", "") projectFlockId := c.QueryInt("project_flock_id", 0)
kandangIdStr := c.Query("kandang_id", "") kandangId := c.QueryInt("kandang_id", 0)
result, err := u.ProjectflockService.GetProjectFlockKandangByParams(c, "", projectFlockIdStr, kandangIdStr) if projectFlockId == 0 || kandangId == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id")
}
result, availableStock, err := u.ProjectflockService.GetProjectFlockKandangByProjectAndKandang(c, uint(projectFlockId), uint(kandangId))
if err != nil { if err != nil {
return err return err
} }
dtoResult := dto.ToProjectFlockKandangDTO(*result)
dtoResult.AvailableQuantity = float64(availableStock)
// populate available quantity for each kandang inside project_flock
if dtoResult.ProjectFlock != nil {
for i := range dtoResult.ProjectFlock.Kandangs {
kand := &dtoResult.ProjectFlock.Kandangs[i]
if kand.Id == 0 {
continue
}
if q, qerr := u.ProjectflockService.GetAvailableDocQuantity(c, kand.Id); qerr == nil {
kand.AvailableQuantity = q
}
}
// remove inner kandangs from project_flock to avoid duplication
dtoResult.ProjectFlock.Kandangs = nil
}
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.Success{Code: fiber.StatusOK, JSON(response.Success{Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get projectflock kandang successfully", Message: "Get projectflock kandang successfully",
Data: dto.ToProjectFlockKandangDTO(*result)}) Data: dtoResult})
} }
@@ -10,6 +10,7 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -18,11 +19,12 @@ import (
type ProjectFlockBaseDTO struct { type ProjectFlockBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Period int `json:"period"` Period int `json:"period"`
FlockName string `json:"flock_name"`
} }
type ProjectFlockListDTO struct { type ProjectFlockListDTO struct {
ProjectFlockBaseDTO ProjectFlockBaseDTO
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` // Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
@@ -58,11 +60,11 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
} }
} }
var flockSummary *flockDTO.FlockBaseDTO // var flockSummary *flockDTO.FlockBaseDTO
if e.Flock.Id != 0 { // if baseName := pfutils.DeriveBaseName(e.FlockName); baseName != "" {
mapped := flockDTO.ToFlockBaseDTO(e.Flock) // summary := flockDTO.FlockBaseDTO{Id: 0, Name: baseName}
flockSummary = &mapped // flockSummary = &summary
} // }
var areaSummary *areaDTO.AreaBaseDTO var areaSummary *areaDTO.AreaBaseDTO
if e.Area.Id != 0 { if e.Area.Id != 0 {
@@ -90,7 +92,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
return ProjectFlockListDTO{ return ProjectFlockListDTO{
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
Flock: flockSummary, // Flock: flockSummary,
Area: areaSummary, Area: areaSummary,
Kandangs: kandangSummaries, Kandangs: kandangSummaries,
Category: e.Category, Category: e.Category,
@@ -146,6 +148,7 @@ func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
return ProjectFlockBaseDTO{ return ProjectFlockBaseDTO{
Id: e.Id, Id: e.Id,
Period: e.Period, Period: e.Period,
FlockName: e.FlockName,
} }
} }
@@ -7,13 +7,13 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
// internal DTO used only for lookup response: project flock with kandangs carrying pivot ids
type KandangWithPivotDTO struct { type KandangWithPivotDTO struct {
kandangDTO.KandangBaseDTO kandangDTO.KandangBaseDTO
ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty"` AvailableQuantity float64 `json:"available_quantity"`
} }
type ProjectFlockWithPivotDTO struct { type ProjectFlockWithPivotDTO struct {
@@ -29,10 +29,12 @@ type ProjectFlockWithPivotDTO struct {
type ProjectFlockKandangDTO struct { type ProjectFlockKandangDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ProjectFlockId uint `json:"project_flock_id"` ProjectFlockId uint `json:"project_flock_id"`
KandangId uint `json:"kandang_id"` KandangId uint `json:"kandang_id"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
AvailableQuantity float64 `json:"available_quantity"`
} }
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
@@ -44,19 +46,19 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
var pf *ProjectFlockWithPivotDTO var pf *ProjectFlockWithPivotDTO
if e.ProjectFlock.Id != 0 { if e.ProjectFlock.Id != 0 {
// build project flock with kandangs that include pivot ids
pfLocal := ProjectFlockWithPivotDTO{ pfLocal := ProjectFlockWithPivotDTO{
ProjectFlockBaseDTO: ProjectFlockBaseDTO{ ProjectFlockBaseDTO: ProjectFlockBaseDTO{
Id: e.ProjectFlock.Id, Id: e.ProjectFlock.Id,
Period: e.ProjectFlock.Period, Period: e.ProjectFlock.Period,
FlockName: e.ProjectFlock.FlockName,
}, },
Category: e.ProjectFlock.Category, Category: e.ProjectFlock.Category,
} }
// fill related small summaries if base := pfutils.DeriveBaseName(e.ProjectFlock.FlockName); base != "" {
if e.ProjectFlock.Flock.Id != 0 { summary := flockDTO.FlockBaseDTO{Id: 0, Name: base}
mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock) pfLocal.Flock = &summary
pfLocal.Flock = &mapped
} }
if e.ProjectFlock.Area.Id != 0 { if e.ProjectFlock.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.ProjectFlock.Area) mapped := areaDTO.ToAreaBaseDTO(e.ProjectFlock.Area)
@@ -75,23 +77,11 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
pfLocal.CreatedUser = &mapped pfLocal.CreatedUser = &mapped
} }
// build pivot map
pivotMap := make(map[uint]uint)
for _, ph := range e.ProjectFlock.KandangHistory {
pivotMap[ph.KandangId] = ph.Id
}
// populate kandangs with pivot ids
for _, k := range e.ProjectFlock.Kandangs { for _, k := range e.ProjectFlock.Kandangs {
kb := kandangDTO.ToKandangBaseDTO(k) kb := kandangDTO.ToKandangBaseDTO(k)
var pid *uint
if v, ok := pivotMap[k.Id]; ok {
vv := v
pid = &vv
}
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{ pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
KandangBaseDTO: kb, KandangBaseDTO: kb,
ProjectFlockKandangId: pid, AvailableQuantity: 0,
}) })
} }
@@ -100,9 +90,11 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
return ProjectFlockKandangDTO{ return ProjectFlockKandangDTO{
Id: e.Id, Id: e.Id,
ProjectFlockKandangId: e.Id,
ProjectFlockId: e.ProjectFlockId, ProjectFlockId: e.ProjectFlockId,
KandangId: e.KandangId, KandangId: e.KandangId,
Kandang: kandang, Kandang: kandang,
ProjectFlock: pf, ProjectFlock: pf,
AvailableQuantity: 0,
} }
} }
@@ -9,8 +9,10 @@ import (
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gorm.io/gorm" "gorm.io/gorm"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/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"
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
@@ -27,6 +29,8 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
kandangRepo := rKandang.NewKandangRepository(db) kandangRepo := rKandang.NewKandangRepository(db)
projectflockRepo := rProjectflock.NewProjectflockRepository(db) projectflockRepo := rProjectflock.NewProjectflockRepository(db)
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db) projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
@@ -35,7 +39,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err)) panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
} }
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, approvalService, validate) projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
ProjectflockRoutes(router, userService, projectflockService) ProjectflockRoutes(router, userService, projectflockService)
@@ -3,19 +3,30 @@ package repository
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))"
type ProjectflockRepository interface { type ProjectflockRepository interface {
repository.BaseRepository[entity.ProjectFlock] repository.BaseRepository[entity.ProjectFlock]
GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error)
GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error)
GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) GetNextSequenceForBase(ctx context.Context, baseName string) (int, error)
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error)
WithDefaultRelations() func(*gorm.DB) *gorm.DB
ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error)
AreaExists(ctx context.Context, id uint) (bool, error)
FcrExists(ctx context.Context, id uint) (bool, error)
LocationExists(ctx context.Context, id uint) (bool, error)
} }
type ProjectflockRepositoryImpl struct { type ProjectflockRepositoryImpl struct {
@@ -28,11 +39,11 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
} }
} }
func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) { func (r *ProjectflockRepositoryImpl) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) {
var records []entity.ProjectFlock var records []entity.ProjectFlock
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
Unscoped(). Unscoped().
Where("flock_id = ?", flockID). Where(baseNameExpression+" = LOWER(?)", baseName).
Order("period ASC"). Order("period ASC").
Find(&records).Error; err != nil { Find(&records).Error; err != nil {
return nil, err return nil, err
@@ -40,10 +51,10 @@ func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID
return records, nil return records, nil
} }
func (r *ProjectflockRepositoryImpl) GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) { func (r *ProjectflockRepositoryImpl) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) {
var record entity.ProjectFlock var record entity.ProjectFlock
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
Where("flock_id = ?", flockID). Where(baseNameExpression+" = LOWER(?)", baseName).
Order("period DESC"). Order("period DESC").
First(&record).Error First(&record).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -55,11 +66,11 @@ func (r *ProjectflockRepositoryImpl) GetActiveByFlock(ctx context.Context, flock
return &record, nil return &record, nil
} }
func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) { func (r *ProjectflockRepositoryImpl) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
var max int var max int
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlock{}). Model(&entity.ProjectFlock{}).
Where("flock_id = ?", flockID). Where(baseNameExpression+" = LOWER(?)", baseName).
Select("COALESCE(MAX(period), 0)"). Select("COALESCE(MAX(period), 0)").
Scan(&max).Error; err != nil { Scan(&max).Error; err != nil {
return 0, err return 0, err
@@ -67,13 +78,13 @@ func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, fl
return max, nil return max, nil
} }
func (r *ProjectflockRepositoryImpl) GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) { func (r *ProjectflockRepositoryImpl) GetNextSequenceForBase(ctx context.Context, baseName string) (int, error) {
var payload struct { var payload struct {
Period int Period int
} }
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlock{}). Model(&entity.ProjectFlock{}).
Where("flock_id = ?", flockID). Where(baseNameExpression+" = LOWER(?)", baseName).
Clauses(clause.Locking{Strength: "UPDATE"}). Clauses(clause.Locking{Strength: "UPDATE"}).
Order("period DESC"). Order("period DESC").
Limit(1). Limit(1).
@@ -86,3 +97,164 @@ func (r *ProjectflockRepositoryImpl) GetNextPeriodForFlock(ctx context.Context,
} }
return payload.Period + 1, nil return payload.Period + 1, nil
} }
func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
db = r.withDefaultRelations(db)
return r.applyQueryFilters(db, params)
})
}
func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withDefaultRelations(db)
}
}
func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs")
}
func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB {
if params == nil {
return db
}
if params.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", params.AreaId)
}
if params.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", params.LocationId)
}
if params.Period > 0 {
db = db.Where("project_flocks.period = ?", params.Period)
}
if len(params.KandangIds) > 0 {
db = db.Where(`
EXISTS (
SELECT 1
FROM project_flock_kandangs pfk
WHERE pfk.project_flock_id = project_flocks.id
AND pfk.kandang_id IN ?
)`, params.KandangIds)
}
db = r.applySearchFilters(db, params.Search)
for _, expr := range r.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
}
func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB {
if rawSearch == "" {
return db
}
normalized := strings.ToLower(strings.TrimSpace(rawSearch))
if normalized == "" {
return db
}
likeQuery := "%" + normalized + "%"
return db.
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(`
LOWER(areas.name) LIKE ?
OR LOWER(project_flocks.category) LIKE ?
OR LOWER(fcrs.name) LIKE ?
OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ?
OR LOWER(created_users.name) LIKE ?
OR LOWER(created_users.email) LIKE ?
OR LOWER(project_flocks.flock_name) LIKE ?
OR LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))) LIKE ?
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
OR EXISTS (
SELECT 1 FROM kandangs
WHERE kandangs.project_flock_id = project_flocks.id
AND LOWER(kandangs.name) LIKE ?
)
`,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
)
}
func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Area](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) FcrExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Fcr](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) LocationExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Location](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder string) []string {
direction := "ASC"
if strings.ToLower(sortOrder) == "desc" {
direction = "DESC"
}
switch sortBy {
case "area":
return []string{
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "location":
return []string{
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "kandangs":
return []string{
fmt.Sprintf("(SELECT COUNT(*) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "period":
return []string{
fmt.Sprintf("project_flocks.period %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
default:
return []string{
"project_flocks.created_at DESC",
"project_flocks.updated_at DESC",
}
}
}
func (r *ProjectflockRepositoryImpl) ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) {
var count int64
q := r.DB().WithContext(ctx).Model(&entity.ProjectFlock{}).Where("flock_name = ?", flockName)
if excludeID != nil && *excludeID != 0 {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
@@ -13,6 +14,10 @@ type ProjectFlockKandangRepository interface {
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB DB() *gorm.DB
} }
@@ -21,6 +26,8 @@ type projectFlockKandangRepositoryImpl struct {
db *gorm.DB db *gorm.DB
} }
const flockBaseNameExpression = "LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')))"
func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository { func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository {
return &projectFlockKandangRepositoryImpl{db: db} return &projectFlockKandangRepositoryImpl{db: db}
} }
@@ -45,7 +52,6 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
var records []entity.ProjectFlockKandang var records []entity.ProjectFlockKandang
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
@@ -72,7 +78,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
record := new(entity.ProjectFlockKandang) record := new(entity.ProjectFlockKandang)
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
@@ -91,7 +96,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID). Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
@@ -104,3 +108,62 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
} }
return record, nil return record, nil
} }
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
if len(kandangIDs) == 0 {
return nil, nil
}
var existing []uint
err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
Pluck("kandang_id", &existing).Error
return existing, err
}
func (r *projectFlockKandangRepositoryImpl) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) {
if len(kandangIDs) == 0 {
return false, nil
}
q := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("kandang_id IN ?", kandangIDs)
if exceptProjectID != nil {
q = q.Where("project_flock_id <> ?", *exceptProjectID)
}
var count int64
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *projectFlockKandangRepositoryImpl) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) {
if len(kandangIDs) == 0 {
return nil, nil
}
var kandangs []entity.Kandang
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("pfk.kandang_id AS id, COALESCE(k.name, '') AS name").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("LEFT JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("pfk.project_flock_id = ? AND pfk.kandang_id IN ?", projectFlockID, kandangIDs).
Group("pfk.kandang_id, k.name").
Scan(&kandangs).Error
return kandangs, err
}
func (r *projectFlockKandangRepositoryImpl) MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
if strings.TrimSpace(baseName) == "" {
return 0, nil
}
var max int
err := r.db.WithContext(ctx).
Table("project_flock_kandangs pfk").
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Where(flockBaseNameExpression+" = LOWER(?)", baseName).
Select("COALESCE(MAX(pf.period), 0)").
Scan(&max).Error
return max, err
}
@@ -1,7 +1,7 @@
package project_flocks package project_flocks
import ( 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" 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" projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -12,13 +12,8 @@ import (
func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) { func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) {
ctrl := controller.NewProjectflockController(s) ctrl := controller.NewProjectflockController(s)
route := v1.Group("/project_flocks") route := v1.Group("/project-flocks")
route.Use(m.Auth(u))
// 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("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -27,5 +22,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval) route.Post("/approvals", ctrl.Approval)
route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary) route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
} }
@@ -10,9 +10,13 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -28,8 +32,9 @@ type ProjectflockService interface {
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
} }
@@ -40,6 +45,8 @@ type projectflockService struct {
Repository repository.ProjectflockRepository Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository KandangRepo kandangRepository.KandangRepository
WarehouseRepo warehouseRepository.WarehouseRepository
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
PivotRepo repository.ProjectFlockKandangRepository PivotRepo repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey approvalWorkflow approvalutils.ApprovalWorkflowKey
@@ -55,6 +62,8 @@ func NewProjectflockService(
flockRepo flockRepository.FlockRepository, flockRepo flockRepository.FlockRepository,
kandangRepo kandangRepository.KandangRepository, kandangRepo kandangRepository.KandangRepository,
pivotRepo repository.ProjectFlockKandangRepository, pivotRepo repository.ProjectFlockKandangRepository,
warehouseRepo warehouseRepository.WarehouseRepository,
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
validate *validator.Validate, validate *validator.Validate,
) ProjectflockService { ) ProjectflockService {
@@ -64,22 +73,14 @@ func NewProjectflockService(
Repository: repo, Repository: repo,
FlockRepo: flockRepo, FlockRepo: flockRepo,
KandangRepo: kandangRepo, KandangRepo: kandangRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
PivotRepo: pivotRepo, PivotRepo: pivotRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock, approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
} }
} }
func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Flock").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs")
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return nil, 0, err return nil, 0, err
@@ -94,74 +95,11 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
db = s.withRelations(db)
if params.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", params.AreaId)
}
if params.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", params.LocationId)
}
if params.Period > 0 {
db = db.Where("project_flocks.period = ?", params.Period)
}
if len(params.KandangIds) > 0 {
db = db.Where("EXISTS (SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds)
}
if params.Search != "" {
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
if normalizedSearch == "" {
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
}
likeQuery := "%" + normalizedSearch + "%"
db = db.
Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id").
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(`
LOWER(flocks.name) LIKE ?
OR LOWER(areas.name) LIKE ?
OR LOWER(project_flocks.category) LIKE ?
OR LOWER(fcrs.name) LIKE ?
OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ?
OR LOWER(created_users.name) LIKE ?
OR LOWER(created_users.email) LIKE ?
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
OR EXISTS (
SELECT 1 FROM kandangs
WHERE kandangs.project_flock_id = project_flocks.id
AND LOWER(kandangs.name) LIKE ?
)
`,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
)
}
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
})
if err != nil { if err != nil {
s.Log.Errorf("Failed to get projectflocks: %+v", err) s.Log.Errorf("Failed to get projectflocks: %+v", err)
return nil, 0, err return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks")
} }
if s.ApprovalSvc != nil && len(projectflocks) > 0 { if s.ApprovalSvc != nil && len(projectflocks) > 0 {
@@ -188,13 +126,13 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
} }
func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
projectflock, err := s.Repository.GetByID(c.Context(), id, s.withRelations) projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
if err != nil { if err != nil {
s.Log.Errorf("Failed get projectflock by id: %+v", err) s.Log.Errorf("Failed get projectflock by id: %+v", err)
return nil, err return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
if s.ApprovalSvc != nil { if s.ApprovalSvc != nil {
@@ -221,6 +159,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err return nil, err
} }
actorID, err := actorIDFromContext(c)
if err != nil {
return nil, err
}
cat := strings.ToUpper(req.Category) cat := strings.ToUpper(req.Category)
if !utils.IsValidProjectFlockCategory(cat) { if !utils.IsValidProjectFlockCategory(cat) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
@@ -230,15 +173,28 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
} }
baseName := strings.TrimSpace(req.FlockName)
if baseName == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists},
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
canonicalBase := baseName
if s.FlockRepo != nil {
baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
if err != nil {
return nil, err
}
canonicalBase = baseFlock.Name
}
kandangIDs := uniqueUintSlice(req.KandangIds) kandangIDs := uniqueUintSlice(req.KandangIds)
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil) kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
if err != nil { if err != nil {
@@ -250,29 +206,34 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
if len(kandangs) != len(kandangIDs) { if len(kandangs) != len(kandangIDs) {
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
} }
for _, kandang := range kandangs { // larang kalau ada yg sudah terikat ke project lain
if kandang.ProjectFlockId != nil { if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah memiliki project flock", kandang.Name)) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
} } else if linked {
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
} }
createBody := &entity.ProjectFlock{ createBody := &entity.ProjectFlock{
FlockId: req.FlockId,
AreaId: req.AreaId, AreaId: req.AreaId,
Category: cat, Category: cat,
FcrId: req.FcrId, FcrId: req.FcrId,
LocationId: req.LocationId, LocationId: req.LocationId,
CreatedBy: 1, CreatedBy: actorID,
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction) projectRepo := repository.NewProjectflockRepository(dbTransaction)
period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId) nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), canonicalBase)
if err != nil { if err != nil {
return err return err
} }
createBody.Period = period generatedName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, nextSeq, nil)
if err != nil {
return err
}
createBody.FlockName = generatedName
createBody.Period = seq
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
return err return err
@@ -282,7 +243,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return err return err
} }
actorID := uint(1) //TODO: Change From Auth
action := entity.ApprovalActionCreated action := entity.ApprovalActionCreated
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
_, err = approvalSvcTx.CreateApproval( _, err = approvalSvcTx.CreateApproval(
@@ -298,11 +258,14 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
}) })
if err != nil { if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
if errors.Is(err, gorm.ErrDuplicatedKey) { if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists") return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
} }
s.Log.Errorf("Failed to create projectflock: %+v", err) s.Log.Errorf("Failed to create projectflock: %+v", err)
return nil, err return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project flock")
} }
return s.GetOne(c, createBody.Id) return s.GetOne(c, createBody.Id)
@@ -313,7 +276,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, err return nil, err
} }
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations) actorID, err := actorIDFromContext(c)
if err != nil {
return nil, err
}
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
@@ -324,15 +292,28 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
updateBody := make(map[string]any) updateBody := make(map[string]any)
hasBodyChanges := false hasBodyChanges := false
var relationChecks []commonSvc.RelationCheck var relationChecks []commonSvc.RelationCheck
existingBase := pfutils.DeriveBaseName(existing.FlockName)
targetBaseName := existingBase
needFlockNameRegenerate := false
if req.FlockId != nil { if req.FlockName != nil {
updateBody["flock_id"] = *req.FlockId trimmed := strings.TrimSpace(*req.FlockName)
if trimmed == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
canonicalBase := trimmed
if s.FlockRepo != nil {
flockEntity, err := s.ensureFlockByName(c.Context(), actorID, trimmed)
if err != nil {
return nil, err
}
canonicalBase = flockEntity.Name
}
if !strings.EqualFold(canonicalBase, existingBase) {
needFlockNameRegenerate = true
targetBaseName = canonicalBase
hasBodyChanges = true hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{ }
Name: "Flock",
ID: req.FlockId,
Exists: relationExistsChecker[entity.Flock](s.Repository.DB()),
})
} }
if req.AreaId != nil { if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId updateBody["area_id"] = *req.AreaId
@@ -340,7 +321,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
relationChecks = append(relationChecks, commonSvc.RelationCheck{ relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Area", Name: "Area",
ID: req.AreaId, ID: req.AreaId,
Exists: relationExistsChecker[entity.Area](s.Repository.DB()), Exists: s.Repository.AreaExists,
}) })
} }
if req.Category != nil { if req.Category != nil {
@@ -357,7 +338,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
relationChecks = append(relationChecks, commonSvc.RelationCheck{ relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "FCR", Name: "FCR",
ID: req.FcrId, ID: req.FcrId,
Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()), Exists: s.Repository.FcrExists,
}) })
} }
if req.LocationId != nil { if req.LocationId != nil {
@@ -366,7 +347,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
relationChecks = append(relationChecks, commonSvc.RelationCheck{ relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Location", Name: "Location",
ID: req.LocationId, ID: req.LocationId,
Exists: relationExistsChecker[entity.Location](s.Repository.DB()), Exists: s.Repository.LocationExists,
}) })
} }
@@ -394,11 +375,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
if len(kandangs) != len(newKandangIDs) { if len(kandangs) != len(newKandangIDs) {
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
} }
for _, k := range kandangs { if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
if k.ProjectFlockId != nil && *k.ProjectFlockId != id { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah terikat dengan project flock lain", k.Name)) } else if linked {
} return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
} }
} }
hasChanges := hasBodyChanges || hasKandangChanges hasChanges := hasBodyChanges || hasKandangChanges
@@ -409,6 +391,29 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction) projectRepo := repository.NewProjectflockRepository(dbTransaction)
baseForGeneration := targetBaseName
if strings.TrimSpace(baseForGeneration) == "" {
baseForGeneration = existingBase
}
if strings.TrimSpace(baseForGeneration) == "" {
baseForGeneration = strings.TrimSpace(existing.FlockName)
}
if needFlockNameRegenerate {
nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), baseForGeneration)
if err != nil {
return err
}
newName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, nextSeq, &id)
if err != nil {
return err
}
updateBody["flock_name"] = newName
if seq != existing.Period {
updateBody["period"] = seq
}
}
if len(updateBody) > 0 { if len(updateBody) > 0 {
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return err return err
@@ -457,7 +462,6 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
} }
if hasChanges { if hasChanges {
actorID := uint(1) //TODO: Change From Auth
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
if approvalSvc != nil { if approvalSvc != nil {
latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil) latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
@@ -497,7 +501,10 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
s.Log.Errorf("Failed to update projectflock %d: %+v", id, err) s.Log.Errorf("Failed to update projectflock %d: %+v", id, err)
return nil, err if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock")
} }
return s.GetOne(c, id) return s.GetOne(c, id)
@@ -508,7 +515,11 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
return nil, err return nil, err
} }
actorID := uint(1) // TODO: change from auth context actorID, err := actorIDFromContext(c)
if err != nil {
return nil, err
}
var action entity.ApprovalAction var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) { switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected): case string(entity.ApprovalActionRejected):
@@ -529,7 +540,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
step = utils.ProjectFlockStepAktif step = utils.ProjectFlockStepAktif
} }
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction) kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
projectRepoTx := repository.NewProjectflockRepository(dbTransaction) projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
@@ -601,7 +612,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
} }
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error { func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations) existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
@@ -635,30 +646,32 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
return fiberErr return fiberErr
} }
s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err) s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err)
return err return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete project flock")
} }
return nil return nil
} }
func (s projectflockService) GetProjectFlockKandang(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, error) { func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) {
// keep for backward compatibility; delegate to new consolidated method
return s.GetProjectFlockKandangByParams(ctx, fmt.Sprintf("%d", id), "", "")
}
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) {
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID) pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
} }
return nil, err s.Log.Errorf("Failed to fetch project_flock_kandang by project %d and kandang %d: %+v", projectFlockID, kandangID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
} }
return pfk, nil
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
if err != nil {
return nil, 0, err
}
return pfk, availableQuantity, nil
} }
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error) { func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
idStr = strings.TrimSpace(idStr) idStr = strings.TrimSpace(idStr)
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr) projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
kandangIdStr = strings.TrimSpace(kandangIdStr) kandangIdStr = strings.TrimSpace(kandangIdStr)
@@ -666,52 +679,107 @@ func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idSt
if idStr != "" { if idStr != "" {
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 { if err != nil || id <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid Id") return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
} }
pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id)) pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
} }
return nil, err s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", id, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
} }
return pfk, nil
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
if err != nil {
return nil, 0, err
}
return pfk, availableQuantity, nil
} }
if projectFlockIdStr == "" || kandangIdStr == "" { if projectFlockIdStr == "" || kandangIdStr == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters") return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
} }
pfid, err := strconv.Atoi(projectFlockIdStr) pfid, err := strconv.Atoi(projectFlockIdStr)
if err != nil || pfid <= 0 { if err != nil || pfid <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
} }
kid, err := strconv.Atoi(kandangIdStr) kid, err := strconv.Atoi(kandangIdStr)
if err != nil || kid <= 0 { if err != nil || kid <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id") return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
} }
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid)) return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
} }
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) { func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser") wh, err := s.WarehouseRepo.GetByKandangID(ctx.Context(), kandangID)
})
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Flock not found")
}
if err != nil { if err != nil {
s.Log.Errorf("Failed get flock %d for period summary: %+v", flockID, err) return 0, err
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
} }
maxPeriod, err := s.Repository.GetMaxPeriodByFlock(c.Context(), flockID) productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "DOC", wh.Id)
if err != nil { if err != nil {
s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err) return 0, err
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period") }
total := 0.0
for _, pw := range productWarehouses {
total += pw.Quantity
}
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")
}
pivot, err := s.pivotRepo().GetByID(c.Context(), projectFlockKandangID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
if err != nil {
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")
}
var baseName string
var referenceFlock *entity.Flock
if pivot.ProjectFlock.Id != 0 {
baseName = pfutils.DeriveBaseName(pivot.ProjectFlock.FlockName)
}
if strings.TrimSpace(baseName) != "" {
referenceFlock, err = s.FlockRepo.GetByName(c.Context(), baseName)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch flock %q: %+v", baseName, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
}
}
if referenceFlock == nil {
referenceFlock = &entity.Flock{Name: pivot.ProjectFlock.FlockName}
}
maxPeriod := pivot.ProjectFlock.Period
if strings.TrimSpace(baseName) != "" {
if headerMax, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), baseName); err != nil {
s.Log.Warnf("Unable to compute header period for base %q: %+v", baseName, err)
} else if headerMax > maxPeriod {
maxPeriod = headerMax
}
if pivotMax, err := s.pivotRepo().MaxPeriodByBaseName(c.Context(), baseName); err != nil {
s.Log.Warnf("Unable to compute pivot period for base %q: %+v", baseName, err)
} else if pivotMax > maxPeriod {
maxPeriod = pivotMax
}
} }
return &FlockPeriodSummary{ return &FlockPeriodSummary{
Flock: *flock, Flock: *referenceFlock,
NextPeriod: maxPeriod + 1, NextPeriod: maxPeriod + 1,
}, nil }, nil
} }
@@ -729,45 +797,64 @@ func uniqueUintSlice(values []uint) []uint {
return result return result
} }
func relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool, error) { func (s projectflockService) generateSequentialFlockName(ctx context.Context, repo repository.ProjectflockRepository, baseName string, startNumber int, excludeID *uint) (string, int, error) {
return func(ctx context.Context, id uint) (bool, error) { name := strings.TrimSpace(baseName)
return commonRepo.Exists[T](ctx, db, id) if name == "" {
return "", 0, fiber.NewError(fiber.StatusBadRequest, "Base flock name cannot be empty")
}
number := startNumber
if number <= 0 {
number = 1
}
attempts := 0
for {
candidate := fmt.Sprintf("%s %03d", name, number)
exists, err := repo.ExistsByFlockName(ctx, candidate, excludeID)
if err != nil {
s.Log.Errorf("Failed checking project flock name uniqueness for %q: %+v", candidate, err)
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate flock name")
}
if !exists {
return candidate, number, nil
}
number++
attempts++
if attempts > 9999 {
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Unable to generate unique flock name")
}
} }
} }
func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []string { func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint, name string) (*entity.Flock, error) {
direction := "ASC" trimmed := strings.TrimSpace(name)
if strings.ToLower(sortOrder) == "desc" { if trimmed == "" {
direction = "DESC" return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
} }
switch sortBy { flock, err := s.FlockRepo.GetByName(ctx, trimmed)
case "area": if err == nil {
return []string{ return flock, nil
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
} }
case "location": if !errors.Is(err, gorm.ErrRecordNotFound) {
return []string{ s.Log.Errorf("Failed to fetch flock by name %q: %+v", trimmed, err)
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction), return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
fmt.Sprintf("project_flocks.id %s", direction),
} }
case "kandangs":
return []string{ newFlock := &entity.Flock{
fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction), Name: trimmed,
fmt.Sprintf("project_flocks.id %s", direction), CreatedBy: actorID,
} }
case "period": if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
return []string{ if errors.Is(err, gorm.ErrDuplicatedKey) {
fmt.Sprintf("project_flocks.period %s", direction), return s.FlockRepo.GetByName(ctx, trimmed)
fmt.Sprintf("project_flocks.id %s", direction),
}
default:
return []string{
"project_flocks.created_at DESC",
"project_flocks.updated_at DESC",
} }
s.Log.Errorf("Failed to create flock %q: %+v", trimmed, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
} }
return newFlock, nil
} }
func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error { func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error {
@@ -775,24 +862,45 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
return nil return nil
} }
if err := dbTransaction.Model(&entity.Kandang{}). if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusPengajuan); err != nil {
Where("id IN ?", kandangIDs). return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
Updates(map[string]any{
"project_flock_id": projectFlockID,
"status": string(utils.KandangStatusPengajuan),
}).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
} }
pivotRepo := s.pivotRepoWithTx(dbTransaction) already, err := s.pivotRepoWithTx(dbTransaction).ListExistingKandangIDs(ctx, projectFlockID, kandangIDs)
records := make([]*entity.ProjectFlockKandang, len(kandangIDs)) if err != nil {
for i, id := range kandangIDs { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot")
records[i] = &entity.ProjectFlockKandang{ }
exists := make(map[uint]struct{}, len(already))
for _, id := range already {
exists[id] = struct{}{}
}
var toAttach []uint
seen := make(map[uint]struct{}, len(kandangIDs))
for _, id := range kandangIDs {
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
if _, ok := exists[id]; !ok {
toAttach = append(toAttach, id)
}
}
if len(toAttach) == 0 {
return nil
}
records := make([]*entity.ProjectFlockKandang, 0, len(toAttach))
for _, id := range toAttach {
records = append(records, &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID, ProjectFlockId: projectFlockID,
KandangId: id, KandangId: id,
})
} }
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terhubung dengan project flock ini")
} }
if err := pivotRepo.CreateMany(ctx, records); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
} }
return nil return nil
@@ -803,15 +911,27 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return nil return nil
} }
updates := map[string]any{"project_flock_id": nil} blocked, err := s.pivotRepoWithTx(dbTransaction).FindKandangsWithRecordings(ctx, projectFlockID, kandangIDs)
if resetStatus { if err != nil {
updates["status"] = string(utils.KandangStatusNonActive) s.Log.Errorf("Failed to check recordings before detaching kandangs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandang detachment")
}
if len(blocked) > 0 {
names := make([]string, 0, len(blocked))
for _, item := range blocked {
label := fmt.Sprintf("ID %d", item.Id)
if strings.TrimSpace(item.Name) != "" {
label = fmt.Sprintf("%s (%s)", label, item.Name)
}
names = append(names, label)
}
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
} }
if err := dbTransaction.Model(&entity.Kandang{}). if resetStatus {
Where("id IN ?", kandangIDs). if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
Updates(updates).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") }
} }
if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil { if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
@@ -821,8 +941,33 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
} }
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository { func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
if s.PivotRepo == nil { if dbTransaction == nil {
return repository.NewProjectFlockKandangRepository(dbTransaction) return s.pivotRepo()
} }
return s.PivotRepo.WithTx(dbTransaction) return s.pivotRepo().WithTx(dbTransaction)
}
func (s projectflockService) pivotRepo() repository.ProjectFlockKandangRepository {
if s.PivotRepo != nil {
return s.PivotRepo
}
return repository.NewProjectFlockKandangRepository(s.Repository.DB())
}
func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.KandangRepository {
if tx != nil {
return kandangRepository.NewKandangRepository(tx)
}
if s.KandangRepo != nil {
return s.KandangRepo
}
return kandangRepository.NewKandangRepository(s.Repository.DB())
}
func actorIDFromContext(c *fiber.Ctx) (uint, error) {
user, ok := authmiddleware.AuthenticatedUser(c)
if !ok || user == nil || user.Id == 0 {
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
return user.Id, nil
} }
@@ -0,0 +1,25 @@
package utils
import (
"strconv"
"strings"
)
// DeriveBaseName removes trailing numeric tokens from the flock name.
func DeriveBaseName(name string) string {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return ""
}
parts := strings.Fields(trimmed)
for len(parts) > 0 {
if _, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
parts = parts[:len(parts)-1]
continue
}
break
}
return strings.TrimSpace(strings.Join(parts, " "))
}
@@ -1,7 +1,7 @@
package validation package validation
type Create struct { type Create struct {
FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` FlockName string `json:"flock_name" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"` Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
@@ -10,7 +10,7 @@ type Create struct {
} }
type Update struct { type Update struct {
FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` FlockName *string `json:"flock_name,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
Category *string `json:"category,omitempty" validate:"omitempty"` Category *string `json:"category,omitempty" validate:"omitempty"`
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"` FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
@@ -23,10 +23,14 @@ func NewRecordingController(recordingService service.RecordingService) *Recordin
} }
func (u *RecordingController) GetAll(c *fiber.Ctx) error { func (u *RecordingController) GetAll(c *fiber.Ctx) error {
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), }
if projectFlockID > 0 {
query.ProjectFlockKandangId = uint(projectFlockID)
} }
result, totalResults, err := u.RecordingService.GetAll(c, query) result, totalResults, err := u.RecordingService.GetAll(c, query)
@@ -67,7 +71,30 @@ func (u *RecordingController) GetOne(c *fiber.Ctx) error {
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get recording successfully", Message: "Get recording successfully",
Data: dto.ToRecordingListDTO(*result), Data: dto.ToRecordingDetailDTO(*result),
})
}
func (u *RecordingController) GetNextDay(c *fiber.Ctx) error {
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
if projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
}
nextDay, err := u.RecordingService.GetNextDay(c, uint(projectFlockID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get next recording day successfully",
Data: fiber.Map{
"project_flock_kandang_id": projectFlockID,
"next_day": nextDay,
},
}) })
} }
@@ -88,7 +115,7 @@ func (u *RecordingController) CreateOne(c *fiber.Ctx) error {
Code: fiber.StatusCreated, Code: fiber.StatusCreated,
Status: "success", Status: "success",
Message: "Create recording successfully", Message: "Create recording successfully",
Data: dto.ToRecordingListDTO(*result), Data: dto.ToRecordingDetailDTO(*result),
}) })
} }
@@ -115,7 +142,61 @@ func (u *RecordingController) UpdateOne(c *fiber.Ctx) error {
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Update recording successfully", Message: "Update recording successfully",
Data: dto.ToRecordingListDTO(*result), Data: dto.ToRecordingDetailDTO(*result),
})
}
func (u *RecordingController) SubmitGrading(c *fiber.Ctx) error {
req := new(validation.SubmitGrading)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.RecordingService.SubmitGrading(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Submit grading eggs successfully",
Data: dto.ToRecordingDetailDTO(*result),
})
}
func (u *RecordingController) Approve(c *fiber.Ctx) error {
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
results, err := u.RecordingService.Approval(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit recording approvals successfully"
)
if len(results) == 1 {
message = "Submit recording approval successfully"
data = dto.ToRecordingDetailDTO(results[0])
} else {
data = dto.ToRecordingListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
}) })
} }
@@ -1,17 +1,35 @@
package dto package dto
import ( import (
"math"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
) )
// === DTO Structs === // === DTO Structs ===
type RecordingBaseDTO struct { type RecordingBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` 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"`
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"`
} }
type RecordingListDTO struct { type RecordingListDTO struct {
@@ -23,21 +41,94 @@ type RecordingListDTO struct {
type RecordingDetailDTO struct { type RecordingDetailDTO struct {
RecordingListDTO RecordingListDTO
BodyWeights []RecordingBodyWeightDTO `json:"body_weights"`
Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"`
}
type RecordingBodyWeightDTO struct {
AvgWeight float64 `json:"avg_weight"`
Qty float64 `json:"qty"`
TotalWeight float64 `json:"total_weight"`
}
type RecordingDepletionDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"`
Qty float64 `json:"qty"`
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
}
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"`
}
type RecordingEggDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"`
Qty int `json:"qty"`
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"`
}
type RecordingProductWarehouseDTO struct {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
ProductName string `json:"product_name"`
WarehouseId uint `json:"warehouse_id"`
WarehouseName string `json:"warehouse_name"`
}
type RecordingEggGradingDTO struct {
Grade string `json:"grade,omitempty"`
Qty float64 `json:"qty"`
} }
// === Mapper Functions === // === Mapper Functions ===
func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
var projectFlockCategory *string
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
category := e.ProjectFlockKandang.ProjectFlock.Category
if category != "" {
projectFlockCategory = &category
}
}
latestApproval := defaultRecordingLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = snapshot
}
gradingStatus, gradingPending, gradingCompleted := computeEggGradingStatus(e)
return RecordingBaseDTO{ return RecordingBaseDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, ProjectFlockKandangId: e.ProjectFlockKandangId,
RecordDatetime: e.RecordDatetime,
Day: e.Day,
ProjectFlockCategory: projectFlockCategory,
TotalDepletionQty: e.TotalDepletionQty,
CumDepletionRate: e.CumDepletionRate,
DailyGain: e.DailyGain,
AvgDailyGain: e.AvgDailyGain,
CumIntake: e.CumIntake,
FcrValue: e.FcrValue,
TotalChickQty: e.TotalChickQty,
Approval: latestApproval,
EggGradingStatus: gradingStatus,
EggGradingPendingQty: gradingPending,
EggGradingCompletedQty: gradingCompleted,
} }
} }
func ToRecordingListDTO(e entity.Recording) RecordingListDTO { func ToRecordingListDTO(e entity.Recording) RecordingListDTO {
var createdUser *userDTO.UserBaseDTO var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 { if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser) mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped createdUser = &mapped
} }
@@ -60,5 +151,174 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO { func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
return RecordingDetailDTO{ return RecordingDetailDTO{
RecordingListDTO: ToRecordingListDTO(e), RecordingListDTO: ToRecordingListDTO(e),
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: ToRecordingEggDTOs(e.Eggs),
} }
} }
func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBodyWeightDTO {
result := make([]RecordingBodyWeightDTO, len(bodyWeights))
for i, bw := range bodyWeights {
result[i] = RecordingBodyWeightDTO{
AvgWeight: bw.AvgWeight,
Qty: bw.Qty,
TotalWeight: bw.TotalWeight,
}
}
return result
}
func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []RecordingDepletionDTO {
result := make([]RecordingDepletionDTO, len(depletions))
for i, d := range depletions {
result[i] = RecordingDepletionDTO{
ProductWarehouseId: d.ProductWarehouseId,
Qty: d.Qty,
ProductWarehouse: toRecordingProductWarehouseDTO(&d.ProductWarehouse),
}
}
return result
}
func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
result := make([]RecordingStockDTO, len(stocks))
for i, s := range stocks {
result[i] = RecordingStockDTO{
ProductWarehouseId: s.ProductWarehouseId,
UsageAmount: s.UsageQty,
PendingQty: s.PendingQty,
ProductWarehouse: toRecordingProductWarehouseDTO(&s.ProductWarehouse),
}
}
return result
}
func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
result := make([]RecordingEggDTO, len(eggs))
for i, egg := range eggs {
result[i] = RecordingEggDTO{
ProductWarehouseId: egg.ProductWarehouseId,
Qty: egg.Qty,
ProductWarehouse: toRecordingProductWarehouseDTO(&egg.ProductWarehouse),
Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs),
}
}
return result
}
func ToRecordingEggGradingDTOs(gradings []entity.GradingEgg) []RecordingEggGradingDTO {
if len(gradings) == 0 {
return nil
}
result := make([]RecordingEggGradingDTO, len(gradings))
for i, grading := range gradings {
result[i] = RecordingEggGradingDTO{
Grade: grading.Grade,
Qty: grading.Qty,
}
}
return result
}
func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProductWarehouseDTO {
if pw == nil || pw.Id == 0 {
return nil
}
dto := RecordingProductWarehouseDTO{
Id: pw.Id,
ProductId: pw.ProductId,
WarehouseId: pw.WarehouseId,
}
if pw.Product.Id != 0 {
dto.ProductName = pw.Product.Name
}
if pw.Warehouse.Id != 0 {
dto.WarehouseName = pw.Warehouse.Name
}
return &dto
}
const goodEggProductWarehouseID uint = 5
func computeEggGradingStatus(e entity.Recording) (*string, *int, *int) {
goodEggs := filterGoodEggs(e.Eggs)
if len(goodEggs) == 0 {
return nil, nil, nil
}
totalEggs := 0
totalGraded := 0.0
for _, egg := range goodEggs {
totalEggs += egg.Qty
for _, grading := range egg.GradingEggs {
totalGraded += grading.Qty
}
}
if totalEggs == 0 {
return nil, nil, nil
}
pendingFloat := float64(totalEggs) - totalGraded
if pendingFloat < 0 {
pendingFloat = 0
}
pendingInt := int(math.Round(pendingFloat))
completedInt := int(math.Round(totalGraded))
if completedInt < 0 {
completedInt = 0
}
if pendingInt > 0 {
status := "GRADING_TELUR"
return &status, &pendingInt, &completedInt
}
status := "GRADING_SELESAI"
zero := 0
return &status, &zero, &completedInt
}
func filterGoodEggs(eggs []entity.RecordingEgg) []entity.RecordingEgg {
if len(eggs) == 0 {
return nil
}
result := make([]entity.RecordingEgg, 0, len(eggs))
for _, egg := range eggs {
if egg.ProductWarehouseId == goodEggProductWarehouseID {
result = append(result, egg)
}
}
return result
}
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalBaseDTO {
result := approvalDTO.ApprovalBaseDTO{}
step := utils.RecordingStepPengajuan
result.StepNumber = uint16(step)
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowRecording, step); ok {
result.StepName = label
} else if label, ok := utils.RecordingApprovalSteps[step]; ok {
result.StepName = label
}
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
result.ActionBy = userDTO.ToUserBaseDTO(*e.CreatedUser)
} else if e.CreatedBy != 0 {
result.ActionBy = userDTO.UserBaseDTO{
Id: e.CreatedBy,
IdUser: int64(e.CreatedBy),
}
}
return result
}
@@ -1,12 +1,19 @@
package recordings package recordings
import ( import (
"fmt"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -16,11 +23,27 @@ type RecordingModule struct{}
func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
recordingRepo := rRecording.NewRecordingRepository(db) recordingRepo := rRecording.NewRecordingRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowRecording, utils.RecordingApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register recording approval workflow: %v", err))
}
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
recordingService := sRecording.NewRecordingService(recordingRepo, validate) recordingService := sRecording.NewRecordingService(
recordingRepo,
projectFlockKandangRepo,
productWarehouseRepo,
projectFlockPopulationRepo,
approvalRepo,
approvalService,
validate,
)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
RecordingRoutes(router, userService, recordingService) RecordingRoutes(router, userService, recordingService)
} }

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