mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 23:05:44 +00:00
Merge branch 'dev/teguh' into 'feat/BE/US-159,160/marketing'
[FEAT/BE][US#159/TASK#221,222] create migration and API SO DO See merge request mbugroup/lti-api!58
This commit is contained in:
@@ -3,13 +3,9 @@ root = "."
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
# Build binary utama
|
cmd = "go build -o ./tmp/main ./cmd/api"
|
||||||
cmd = "go build -o /lti-api/tmp/main ./cmd/api"
|
bin = "tmp/main"
|
||||||
# Lokasi binary hasil build
|
full_bin = "APP_ENV=dev ./tmp/main"
|
||||||
bin = "/lti-api/tmp/main"
|
|
||||||
# Jalankan binary langsung dengan environment dev
|
|
||||||
full_bin = "APP_ENV=dev /lti-api/tmp/main"
|
|
||||||
# File yang dipantau oleh Air
|
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
exclude_dir = ["vendor", "tmp"]
|
exclude_dir = ["vendor", "tmp"]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
services:
|
||||||
|
postgresdb:
|
||||||
|
image: postgres:alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT_HOST:-5542}:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||||
|
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
|
||||||
|
volumes:
|
||||||
|
- dbdata:/var/lib/postgresql/data
|
||||||
|
- ./internal/database/init:/docker-entrypoint-initdb.d
|
||||||
|
networks: [go-network]
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
|
||||||
|
]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${REDIS_PORT_HOST:-6381}:6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
networks: [go-network]
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.local
|
||||||
|
image: cosmtrek/air:v1.52.3
|
||||||
|
working_dir: /lti-api
|
||||||
|
volumes:
|
||||||
|
- .:/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
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgresdb
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: ${DB_USER:-postgres}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||||
|
DB_NAME: ${DB_NAME:-db_lti_erp}
|
||||||
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-8081}:8081"
|
||||||
|
depends_on:
|
||||||
|
postgresdb:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [go-network]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
|
go-mod-cache:
|
||||||
|
go-build-cache:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
go-network:
|
||||||
|
name: lti-api_go-network
|
||||||
|
driver: bridge
|
||||||
@@ -10,6 +10,7 @@ require (
|
|||||||
github.com/gofiber/contrib/jwt v1.0.10
|
github.com/gofiber/contrib/jwt v1.0.10
|
||||||
github.com/gofiber/fiber/v2 v2.52.5
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgconn v1.14.1
|
github.com/jackc/pgconn v1.14.1
|
||||||
github.com/redis/go-redis/v9 v9.14.0
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
@@ -33,7 +34,6 @@ require (
|
|||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
github.com/jackc/pgio v1.0.0 // indirect
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package validation
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
)
|
)
|
||||||
@@ -21,34 +23,41 @@ var customMessages = map[string]string{
|
|||||||
"alphanum": "Field %s must contain only alphanumeric characters",
|
"alphanum": "Field %s must contain only alphanumeric characters",
|
||||||
"oneof": "Invalid value for field %s",
|
"oneof": "Invalid value for field %s",
|
||||||
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
|
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
|
||||||
|
"gt": "Invalid %s, must be greater than %s",
|
||||||
}
|
}
|
||||||
|
|
||||||
func CustomErrorMessages(err error) map[string]string {
|
func CustomErrorMessages(err error) (string, map[string]string) {
|
||||||
var validationErrors validator.ValidationErrors
|
var validationErrors validator.ValidationErrors
|
||||||
if errors.As(err, &validationErrors) {
|
if errors.As(err, &validationErrors) {
|
||||||
return generateErrorMessages(validationErrors)
|
return generateErrorMessages(validationErrors)
|
||||||
}
|
}
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
|
func generateErrorMessages(validationErrors validator.ValidationErrors) (string, map[string]string) {
|
||||||
errorsMap := make(map[string]string)
|
errorsMap := make(map[string]string)
|
||||||
for _, err := range validationErrors {
|
var firstMessage string
|
||||||
|
for i, err := range validationErrors {
|
||||||
fieldName := err.StructNamespace()
|
fieldName := err.StructNamespace()
|
||||||
tag := err.Tag()
|
tag := err.Tag()
|
||||||
|
|
||||||
customMessage := customMessages[tag]
|
customMessage := customMessages[tag]
|
||||||
|
var msg string
|
||||||
if customMessage != "" {
|
if customMessage != "" {
|
||||||
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
|
msg = formatErrorMessage(customMessage, err, tag)
|
||||||
} else {
|
} else {
|
||||||
errorsMap[fieldName] = defaultErrorMessage(err)
|
msg = defaultErrorMessage(err)
|
||||||
|
}
|
||||||
|
errorsMap[fieldName] = msg
|
||||||
|
if i == 0 {
|
||||||
|
firstMessage = msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errorsMap
|
return firstMessage, errorsMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
|
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
|
||||||
if tag == "min" || tag == "max" || tag == "len" {
|
if tag == "min" || tag == "max" || tag == "len" || tag == "gt" {
|
||||||
return fmt.Sprintf(customMessage, err.Field(), err.Param())
|
return fmt.Sprintf(customMessage, err.Field(), err.Param())
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(customMessage, err.Field())
|
return fmt.Sprintf(customMessage, err.Field())
|
||||||
@@ -61,6 +70,16 @@ func defaultErrorMessage(err validator.FieldError) string {
|
|||||||
func Validator() *validator.Validate {
|
func Validator() *validator.Validate {
|
||||||
validate := validator.New()
|
validate := validator.New()
|
||||||
|
|
||||||
|
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
|
||||||
|
if jsonTag := getTagName(fld, "json"); jsonTag != "" {
|
||||||
|
return jsonTag
|
||||||
|
}
|
||||||
|
if queryTag := getTagName(fld, "query"); queryTag != "" {
|
||||||
|
return queryTag
|
||||||
|
}
|
||||||
|
return fld.Name
|
||||||
|
})
|
||||||
|
|
||||||
if err := validate.RegisterValidation("password", Password); err != nil {
|
if err := validate.RegisterValidation("password", Password); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -72,3 +91,16 @@ func Validator() *validator.Validate {
|
|||||||
}
|
}
|
||||||
return validate
|
return validate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTagName(fld reflect.StructField, tag string) string {
|
||||||
|
value, ok := fld.Tag.Lookup(tag)
|
||||||
|
if !ok || value == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.Split(value, ",")[0]
|
||||||
|
if name == "" || name == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
@@ -1,36 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS project_chickins (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
project_flock_kandang_id BIGINT NOT NULL,
|
|
||||||
chick_in_date DATE NOT NULL,
|
|
||||||
quantity NUMERIC(15, 3) NOT NULL,
|
|
||||||
note TEXT,
|
|
||||||
created_by BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
|
||||||
ALTER TABLE project_chickins
|
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
|
||||||
REFERENCES project_flock_kandangs(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE project_chickins
|
|
||||||
ADD CONSTRAINT fk_created_by
|
|
||||||
FOREIGN KEY (created_by)
|
|
||||||
REFERENCES users(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- INDEXES
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by);
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS project_flock_populations (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
project_flock_kandang_id BIGINT NOT NULL,
|
|
||||||
initial_quantity NUMERIC(15, 3) NOT NULL,
|
|
||||||
current_quantity NUMERIC(15, 3) NOT NULL,
|
|
||||||
reserved_quantity NUMERIC(15, 3),
|
|
||||||
created_by BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
|
||||||
ALTER TABLE project_flock_populations
|
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
|
||||||
REFERENCES project_flock_kandangs(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE project_flock_populations
|
|
||||||
ADD CONSTRAINT fk_created_by
|
|
||||||
FOREIGN KEY (created_by)
|
|
||||||
REFERENCES users(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- INDEXES
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
|
|
||||||
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_chickin_details (
|
|||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
||||||
|
|||||||
+26
-18
@@ -1,22 +1,30 @@
|
|||||||
|
|
||||||
ALTER TABLE kandangs
|
ALTER TABLE kandangs
|
||||||
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
||||||
|
|
||||||
ALTER TABLE kandangs
|
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
|
||||||
DROP COLUMN IF EXISTS project_flock_id;
|
|
||||||
|
|
||||||
ALTER TABLE project_chickins
|
-- Only alter if tables exist
|
||||||
DROP CONSTRAINT fk_project_flock_kandang_id,
|
DO $$
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
BEGIN
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
||||||
REFERENCES project_flock_kandangs(id)
|
ALTER TABLE project_chickins
|
||||||
ON UPDATE CASCADE
|
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||||
ON DELETE CASCADE;
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
ALTER TABLE project_flock_populations
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
|
||||||
DROP CONSTRAINT fk_project_flock_kandang_id,
|
ALTER TABLE project_flock_populations
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
ALTER TABLE project_flock_populations
|
||||||
REFERENCES project_flock_kandangs(id)
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
ON UPDATE CASCADE
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
ON DELETE CASCADE;
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS laying_transfers CASCADE;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS laying_transfers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
transfer_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
from_project_flock_id BIGINT NOT NULL,
|
||||||
|
to_project_flock_id BIGINT NOT NULL,
|
||||||
|
transfer_date DATE NOT NULL,
|
||||||
|
pending_usage_qty NUMERIC(15, 3),
|
||||||
|
usage_qty NUMERIC(15, 3),
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_from_project_flock
|
||||||
|
FOREIGN KEY (from_project_flock_id)
|
||||||
|
REFERENCES project_flocks(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_to_project_flock
|
||||||
|
FOREIGN KEY (to_project_flock_id)
|
||||||
|
REFERENCES project_flocks(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_created_by
|
||||||
|
FOREIGN KEY (created_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- INDEXES
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_laying_transfers_transfer_number ON laying_transfers (transfer_number)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers (deleted_at);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- MIGRATION: project_chickins
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- STEP 1: Hapus tabel jika sudah ada
|
||||||
|
DROP TABLE IF EXISTS project_chickins;
|
||||||
|
|
||||||
|
-- STEP 2: Buat tabel project_chickins
|
||||||
|
CREATE TABLE IF NOT EXISTS project_chickins (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_kandang_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
chick_in_date DATE NOT NULL,
|
||||||
|
usage_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
pending_usage_qty NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- STEP 3: FOREIGN KEYS
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Relasi ke project_flock_kandangs
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke product_warehouses
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke users
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_chickins_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- STEP 4: INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by);
|
||||||
|
|
||||||
|
-- Composite index for common queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_deleted ON project_chickins (
|
||||||
|
project_flock_kandang_id,
|
||||||
|
deleted_at
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for soft delete queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_deleted_at ON project_chickins (deleted_at);
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- MIGRATION: project_flock_populations
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- STEP 1: Hapus tabel jika sudah ada
|
||||||
|
DROP TABLE IF EXISTS project_flock_populations;
|
||||||
|
|
||||||
|
-- STEP 2: Buat tabel project_flock_populations
|
||||||
|
CREATE TABLE IF NOT EXISTS project_flock_populations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_chickin_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
total_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_used_qty NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- STEP 3: FOREIGN KEYS
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Relasi ke project_chickins
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_populations_chickin FOREIGN KEY (project_chickin_id) REFERENCES project_chickins (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke product_warehouses
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_populations_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke users
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_populations_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- STEP 4: INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by);
|
||||||
|
|
||||||
|
-- Composite index for common queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_chickin_deleted ON project_flock_populations (
|
||||||
|
project_chickin_id,
|
||||||
|
deleted_at
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for soft delete queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_deleted_at ON project_flock_populations (deleted_at);
|
||||||
|
|
||||||
|
-- Unique constraint: one population per chickin
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_populations_chickin_unique ON project_flock_populations (project_chickin_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
-- Rollback laying_transfer_sources dan laying_transfer_targets tables
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS laying_transfer_targets CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS laying_transfer_sources CASCADE;
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
-- Create laying_transfer_sources dan laying_transfer_targets tables
|
||||||
|
|
||||||
|
-- 1. Create laying_transfer_sources table (detail sumber - kandang asal growing)
|
||||||
|
CREATE TABLE laying_transfer_sources (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
laying_transfer_id BIGINT NOT NULL,
|
||||||
|
source_project_flock_kandang_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
note TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add foreign keys untuk laying_transfer_sources
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_sources_laying_transfer_id
|
||||||
|
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_sources_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (source_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_sources_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 2. Create laying_transfer_targets table (detail tujuan - kandang laying)
|
||||||
|
CREATE TABLE laying_transfer_targets (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
laying_transfer_id BIGINT NOT NULL,
|
||||||
|
target_project_flock_kandang_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
note TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add foreign keys untuk laying_transfer_targets
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_targets_laying_transfer_id
|
||||||
|
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_targets_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (target_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_targets_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 3. Create indexes untuk laying_transfer_sources
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_laying_transfer_id ON laying_transfer_sources (laying_transfer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_source_kandang_id ON laying_transfer_sources (
|
||||||
|
source_project_flock_kandang_id
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_product_warehouse_id ON laying_transfer_sources (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_deleted_at ON laying_transfer_sources (deleted_at);
|
||||||
|
|
||||||
|
-- 4. Create indexes untuk laying_transfer_targets
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_laying_transfer_id ON laying_transfer_targets (laying_transfer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_target_kandang_id ON laying_transfer_targets (
|
||||||
|
target_project_flock_kandang_id
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_product_warehouse_id ON laying_transfer_targets (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_deleted_at ON laying_transfer_targets (deleted_at);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS purchase_items;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS purchase_items (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
purchase_id BIGINT NOT NULL,
|
||||||
|
product_id BIGINT NOT NULL,
|
||||||
|
warehouse_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
received_date TIMESTAMPTZ,
|
||||||
|
travel_number VARCHAR,
|
||||||
|
travel_number_docs VARCHAR,
|
||||||
|
vehicle_number VARCHAR,
|
||||||
|
sub_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_qty NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
total_used NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
price NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
total_price NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_product
|
||||||
|
FOREIGN KEY (product_id)
|
||||||
|
REFERENCES products(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_warehouse
|
||||||
|
FOREIGN KEY (warehouse_id)
|
||||||
|
REFERENCES warehouses(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_product_warehouse
|
||||||
|
FOREIGN KEY (product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchase_items_unique_allocation
|
||||||
|
ON purchase_items (purchase_id, product_id, warehouse_id)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_deleted_at ON purchase_items (deleted_at);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_purchase_items_purchase'
|
||||||
|
AND conrelid = 'purchase_items'::regclass
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_purchase;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS purchases;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS purchases (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
pr_number VARCHAR NOT NULL,
|
||||||
|
po_number VARCHAR,
|
||||||
|
po_date TIMESTAMPTZ,
|
||||||
|
supplier_id BIGINT NOT NULL,
|
||||||
|
credit_term INT,
|
||||||
|
due_date TIMESTAMPTZ,
|
||||||
|
grand_total NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchases
|
||||||
|
ADD CONSTRAINT fk_purchases_supplier
|
||||||
|
FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchases
|
||||||
|
ADD CONSTRAINT fk_purchases_created_by
|
||||||
|
FOREIGN KEY (created_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_tables WHERE tablename = 'purchase_items'
|
||||||
|
) AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_purchase_items_purchase'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_purchase
|
||||||
|
FOREIGN KEY (purchase_id)
|
||||||
|
REFERENCES purchases(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_pr_number_unique
|
||||||
|
ON purchases (pr_number)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_po_number_unique
|
||||||
|
ON purchases (po_number)
|
||||||
|
WHERE deleted_at IS NULL AND po_number IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_deleted_at ON purchases (deleted_at);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
DROP TABLE IF EXISTS marketing_delivery_products CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS marketing_products CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS marketings CASCADE;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
CREATE TABLE marketings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
so_number VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
customer_id BIGINT NOT NULL,
|
||||||
|
so_docs VARCHAR(20),
|
||||||
|
so_date DATE NOT NULL,
|
||||||
|
sales_person_id BIGINT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'customers') THEN
|
||||||
|
ALTER TABLE marketings
|
||||||
|
ADD CONSTRAINT fk_marketings_customer_id
|
||||||
|
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE marketings
|
||||||
|
ADD CONSTRAINT fk_marketings_sales_person_id
|
||||||
|
FOREIGN KEY (sales_person_id) REFERENCES users(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE marketings
|
||||||
|
ADD CONSTRAINT fk_marketings_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_customer_id ON marketings (customer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_sales_person_id ON marketings (sales_person_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_created_by ON marketings (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_so_date ON marketings (so_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_deleted_at ON marketings (deleted_at);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS marketing_products CASCADE;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
CREATE TABLE marketing_products (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
marketing_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
avg_weight NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_weight NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketings') THEN
|
||||||
|
ALTER TABLE marketing_products
|
||||||
|
ADD CONSTRAINT fk_marketing_products_marketing_id
|
||||||
|
FOREIGN KEY (marketing_id) REFERENCES marketings(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE marketing_products
|
||||||
|
ADD CONSTRAINT fk_marketing_products_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_products_marketing_id ON marketing_products (marketing_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_products_product_warehouse_id ON marketing_products (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_products_deleted_at ON marketing_products (deleted_at);
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
DROP TABLE IF EXISTS marketing_delivery_products CASCADE;
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
CREATE TABLE marketing_delivery_products (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
marketing_product_id BIGINT UNIQUE NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_weight NUMERIC(15, 3) NOT NULL,
|
||||||
|
avg_weight NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
delivery_date DATE,
|
||||||
|
vehicle_number VARCHAR(50),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketing_products') THEN
|
||||||
|
ALTER TABLE marketing_delivery_products
|
||||||
|
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
|
||||||
|
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_delivery_products_marketing_product_id ON marketing_delivery_products (marketing_product_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_delivery_products_delivery_date ON marketing_delivery_products (delivery_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_delivery_products_deleted_at ON marketing_delivery_products (deleted_at);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
DROP COLUMN IF EXISTS capacity;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
ADD COLUMN capacity NUMERIC(15,3) NOT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS expenses;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
CREATE TABLE expenses (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
reference_number VARCHAR, -- format => BOP-LTI-0001 = 0001 is increment
|
||||||
|
supplier_id BIGINT NULL,
|
||||||
|
category VARCHAR(50) NOT NULL CHECK (
|
||||||
|
category IN ('BOP', 'NON-BOP')
|
||||||
|
),
|
||||||
|
po_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
document_path JSON,
|
||||||
|
expense_date DATE NOT NULL,
|
||||||
|
grand_total NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
note TEXT,
|
||||||
|
created_by BIGINT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke suppliers
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
|
||||||
|
ALTER TABLE expenses
|
||||||
|
ADD CONSTRAINT fk_expenses_supplier_id
|
||||||
|
FOREIGN KEY (supplier_id) REFERENCES suppliers(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke users (created_by)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE expenses
|
||||||
|
ADD CONSTRAINT fk_expenses_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Index
|
||||||
|
CREATE INDEX idx_expenses_supplier_id ON expenses (supplier_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expenses_expense_date ON expenses (expense_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expenses_deleted_at ON expenses (deleted_at);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS expense_nonstocks;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
CREATE TABLE expense_nonstocks (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
expense_id BIGINT,
|
||||||
|
project_flock_kandang_id BIGINT,
|
||||||
|
nonstock_id BIGINT,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
note TEXT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke expenses
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expenses') THEN
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_expense_id
|
||||||
|
FOREIGN KEY (expense_id) REFERENCES expenses(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke project_flock_kandangs
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke nonstocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_nonstock_id
|
||||||
|
FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Index
|
||||||
|
CREATE INDEX idx_expense_nonstocks_expense_id ON expense_nonstocks (expense_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expense_nonstocks_nonstock_id ON expense_nonstocks (nonstock_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expense_nonstocks_deleted_at ON expense_nonstocks (deleted_at);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS expense_realizations;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
CREATE TABLE expense_realizations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
expense_nonstock_id BIGINT,
|
||||||
|
realization_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
realization_unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
realization_total_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
realization_date DATE NOT NULL,
|
||||||
|
note TEXT,
|
||||||
|
created_by BIGINT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke expense_nonstocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD CONSTRAINT fk_expense_realizations_nonstock_id
|
||||||
|
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke users (created_by)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD CONSTRAINT fk_expense_realizations_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Index
|
||||||
|
CREATE INDEX idx_expense_realizations_nonstock_id ON expense_realizations (expense_nonstock_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expense_realizations_date ON expense_realizations (realization_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expense_realizations_deleted_at ON expense_realizations (deleted_at);
|
||||||
@@ -235,13 +235,14 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
seeds := []struct {
|
seeds := []struct {
|
||||||
Name string
|
Name string
|
||||||
Status utils.KandangStatus
|
Status utils.KandangStatus
|
||||||
|
Capacity float64
|
||||||
Location string
|
Location string
|
||||||
PicKey string
|
PicKey string
|
||||||
}{
|
}{
|
||||||
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
|
||||||
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
|
||||||
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
|
||||||
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]uint, len(seeds))
|
result := make(map[string]uint, len(seeds))
|
||||||
@@ -571,52 +572,54 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Flags: []utils.FlagType{utils.FlagDOC},
|
Flags: []utils.FlagType{utils.FlagDOC},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Afkir",
|
Name: "Ayam Pullet",
|
||||||
Brand: "-",
|
Brand: "MBU Pullet",
|
||||||
Sku: "1",
|
Sku: "PLT0001",
|
||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Pullet",
|
||||||
Price: 1,
|
Price: 15000,
|
||||||
|
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||||
|
Flags: []utils.FlagType{utils.FlagPullet},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Mati",
|
Name: "Ayam Afkir",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "2",
|
Sku: "1",
|
||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Culling",
|
Name: "Ayam Mati",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "3",
|
Sku: "2",
|
||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Konsumsi Baik",
|
Name: "Ayam Culling",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "4",
|
Sku: "3",
|
||||||
Uom: "Unit",
|
Uom: "Ekor",
|
||||||
Category: "Telur",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Pecah",
|
Name: "Telur Konsumsi Baik",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "5",
|
Sku: "4",
|
||||||
Uom: "Unit",
|
Uom: "Unit",
|
||||||
Category: "Telur",
|
Category: "Telur",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Telur Pecah",
|
||||||
|
Brand: "-",
|
||||||
|
Sku: "5",
|
||||||
|
Uom: "Unit",
|
||||||
|
Category: "Telur",
|
||||||
|
Price: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "281 SPECIAL STARTER",
|
Name: "281 SPECIAL STARTER",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Expense struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
}
|
||||||
@@ -7,17 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Kandang struct {
|
type Kandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
||||||
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"`
|
Capacity float64 `gorm:"not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
PicId uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
|
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||||
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingKandangTransfer struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
KandangId uint
|
||||||
|
ProductWarehouseId uint
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
LayingTransferId uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransfer struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
TransferNumber string `gorm:"uniqueIndex;not null"`
|
||||||
|
FromProjectFlockId uint `gorm:"not null"`
|
||||||
|
ToProjectFlockId uint `gorm:"not null"`
|
||||||
|
TransferDate time.Time `gorm:"type:date;not null"`
|
||||||
|
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
UsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
Notes string `gorm:"type:text"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||||
|
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
|
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransferSource struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
|
SourceProjectFlockKandangId uint `gorm:"not null"`
|
||||||
|
ProductWarehouseId *uint `gorm:""`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Note string `gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
|
||||||
|
SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransferTarget struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
|
TargetProjectFlockKandangId uint `gorm:"not null"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
ProductWarehouseId *uint `gorm:""`
|
||||||
|
Note string `gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
|
||||||
|
TargetProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:TargetProjectFlockKandangId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Marketing struct {
|
||||||
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
SoNumber string `gorm:"uniqueIndex;not null"`
|
||||||
|
CustomerId uint `gorm:"not null"`
|
||||||
|
SoDocs string `gorm:"type:varchar(20)"`
|
||||||
|
SoDate time.Time `gorm:"type:date;not null"`
|
||||||
|
SalesPersonId uint `gorm:"not null"`
|
||||||
|
Notes string `gorm:"type:text"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
Customer Customer `gorm:"foreignKey:CustomerId;references:Id"`
|
||||||
|
SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"`
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingDeliveryProduct struct {
|
||||||
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||||
|
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingProduct struct {
|
||||||
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
MarketingId uint `gorm:"not null"`
|
||||||
|
ProductWarehouseId uint `gorm:"not null"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
AvgWeight float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
TotalWeight float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"`
|
||||||
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
DeliveryProduct *MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||||
|
}
|
||||||
@@ -12,13 +12,16 @@ type ProjectChickin struct {
|
|||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
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"`
|
ProductWarehouseId uint `gorm:"not null"`
|
||||||
Note string `gorm:"type:text"`
|
UsageQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Notes string `gorm:"type:text"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
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:"-"`
|
||||||
|
|
||||||
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectFlockPopulation struct {
|
type ProjectFlockPopulation struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
ProjectChickinId uint `gorm:"not null"`
|
||||||
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
ProductWarehouseId uint `gorm:"not null"`
|
||||||
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
TotalQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
|
TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
Notes string `gorm:"type:text"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,3 +28,4 @@ type ProjectFlock struct {
|
|||||||
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package entities
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ProjectFlockKandang struct {
|
type ProjectFlockKandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
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"`
|
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
|
||||||
|
|
||||||
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Purchase struct {
|
||||||
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
PrNumber string `gorm:"not null"`
|
||||||
|
PoNumber *string
|
||||||
|
PoDate *time.Time
|
||||||
|
SupplierId uint64 `gorm:"not null"`
|
||||||
|
CreditTerm *int
|
||||||
|
DueDate *time.Time
|
||||||
|
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Notes *string
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
|
CreatedBy uint64 `gorm:"not null"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
Items []PurchaseItem `gorm:"foreignKey:PurchaseId"`
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseItem struct {
|
||||||
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
PurchaseId uint64 `gorm:"not null"`
|
||||||
|
ProductId uint64 `gorm:"not null"`
|
||||||
|
WarehouseId uint64 `gorm:"not null"`
|
||||||
|
ProductWarehouseId *uint64
|
||||||
|
ReceivedDate *time.Time
|
||||||
|
TravelNumber *string
|
||||||
|
TravelNumberDocs *string
|
||||||
|
VehicleNumber *string
|
||||||
|
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||||
|
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
|
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ApprovalBaseDTO struct {
|
type ApprovalBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
StepNumber uint16 `json:"step_number"`
|
StepNumber uint16 `json:"step_number"`
|
||||||
StepName string `json:"step_name"`
|
StepName string `json:"step_name"`
|
||||||
Action *string `json:"action"`
|
Action *string `json:"action"`
|
||||||
@@ -27,6 +28,7 @@ type ApprovalGroupDTO struct {
|
|||||||
|
|
||||||
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
||||||
dto := ApprovalBaseDTO{
|
dto := ApprovalBaseDTO{
|
||||||
|
Id: e.Id,
|
||||||
Notes: e.Notes,
|
Notes: e.Notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
|
|||||||
"LOKASI",
|
"LOKASI",
|
||||||
"KANDANG",
|
"KANDANG",
|
||||||
},
|
},
|
||||||
|
"stock_log": map[string][]string{
|
||||||
|
"log_types": []string{"TRANSFER", "ADJUSTMENT"},
|
||||||
|
"transaction_types": []string{"INCREASE", "DECREASE"},
|
||||||
|
},
|
||||||
"supplier_categories": []string{
|
"supplier_categories": []string{
|
||||||
"BOP",
|
"BOP",
|
||||||
"SAPRONAK",
|
"SAPRONAK",
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExpenseController struct {
|
||||||
|
ExpenseService service.ExpenseService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExpenseController(expenseService service.ExpenseService) *ExpenseController {
|
||||||
|
return &ExpenseController{
|
||||||
|
ExpenseService: expenseService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ExpenseService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ExpenseListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all expenses successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToExpenseListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ExpenseService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get expense successfully",
|
||||||
|
Data: dto.ToExpenseListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ExpenseService.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Create expense successfully",
|
||||||
|
Data: dto.ToExpenseListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Update)
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ExpenseService.UpdateOne(c, req, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Update expense successfully",
|
||||||
|
Data: dto.ToExpenseListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.ExpenseService.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Delete expense successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type ExpenseBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseDetailDTO struct {
|
||||||
|
ExpenseListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO {
|
||||||
|
return ExpenseBaseDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Name: e.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToExpenseListDTO(e entity.Expense) ExpenseListDTO {
|
||||||
|
var createdUser *userDTO.UserBaseDTO
|
||||||
|
if e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExpenseListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Name: e.Name,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToExpenseListDTOs(e []entity.Expense) []ExpenseListDTO {
|
||||||
|
result := make([]ExpenseListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToExpenseListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToExpenseDetailDTO(e entity.Expense) ExpenseDetailDTO {
|
||||||
|
return ExpenseDetailDTO{
|
||||||
|
ExpenseListDTO: ToExpenseListDTO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package expenses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
|
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExpenseModule struct{}
|
||||||
|
|
||||||
|
func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
expenseRepo := rExpense.NewExpenseRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
expenseService := sExpense.NewExpenseService(expenseRepo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
ExpenseRoutes(router, userService, expenseService)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExpenseRepository interface {
|
||||||
|
repository.BaseRepository[entity.Expense]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.Expense]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExpenseRepository(db *gorm.DB) ExpenseRepository {
|
||||||
|
return &ExpenseRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package expenses
|
||||||
|
|
||||||
|
import (
|
||||||
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/controllers"
|
||||||
|
expense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService) {
|
||||||
|
ctrl := controller.NewExpenseController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/expenses")
|
||||||
|
|
||||||
|
// 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.Post("/", ctrl.CreateOne)
|
||||||
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExpenseService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.Expense, error)
|
||||||
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Expense, error)
|
||||||
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error)
|
||||||
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type expenseService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.ExpenseRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExpenseService(repo repository.ExpenseRepository, validate *validator.Validate) ExpenseService {
|
||||||
|
return &expenseService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("CreatedUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = s.withRelations(db)
|
||||||
|
if params.Search != "" {
|
||||||
|
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||||
|
}
|
||||||
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get expenses: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return expenses, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*entity.Expense, error) {
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get expense by id: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return expense, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Expense, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createBody := &entity.Expense{
|
||||||
|
Name: req.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create expense: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, createBody.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBody := make(map[string]any)
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
updateBody["name"] = *req.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateBody) == 0 {
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to update expense: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to delete expense: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingDeliveryProductRepository interface {
|
||||||
|
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
||||||
|
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||||
|
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingDeliveryProductRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
|
||||||
|
return &MarketingDeliveryProductRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
|
||||||
|
var deliveryProduct entity.MarketingDeliveryProduct
|
||||||
|
if err := r.DB().WithContext(ctx).Where("marketing_product_id = ?", marketingProductID).First(&deliveryProduct).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &deliveryProduct, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||||
|
|
||||||
|
// Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct
|
||||||
|
// Filter: hanya ambil yang sudah memiliki delivery_date (delivery date tidak null)
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Preload("MarketingProduct").
|
||||||
|
Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id").
|
||||||
|
Where("mp.marketing_id = ?", marketingId).
|
||||||
|
Where("marketing_delivery_products.delivery_date IS NOT NULL").
|
||||||
|
Order("marketing_delivery_products.id ASC").
|
||||||
|
Find(&deliveryProducts).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveryProducts, nil
|
||||||
|
}
|
||||||
+1
@@ -29,6 +29,7 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
|
|||||||
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", ""),
|
Flags: c.Query("flags", ""),
|
||||||
|
KandangId: uint(c.QueryInt("kandang_id", 0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
@@ -15,13 +16,19 @@ type ProductWarehouseBaseDTO struct {
|
|||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductWarehousNestedDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Product *productDTO.ProductBaseDTO `json:"product,omitempty"`
|
||||||
|
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProductWarehouseListDTO struct {
|
type ProductWarehouseListDTO struct {
|
||||||
ProductWarehouseBaseDTO
|
ProductWarehouseBaseDTO
|
||||||
Product *ProductBaseDTO `json:"product,omitempty"`
|
Product *productDTO.ProductBaseDTO `json:"product,omitempty"`
|
||||||
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
|
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||||
CreatedUser *UserBaseDTO `json:"created_user,omitempty"`
|
CreatedUser *UserBaseDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserBaseDTO struct {
|
type UserBaseDTO struct {
|
||||||
@@ -75,6 +82,19 @@ func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO {
|
||||||
|
product := productDTO.ToProductBaseDTO(e.Product)
|
||||||
|
|
||||||
|
return ProductWarehousNestedDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Product: &product,
|
||||||
|
Warehouse: &WarehouseBaseDTO{
|
||||||
|
Id: e.Warehouse.Id,
|
||||||
|
Name: e.Warehouse.Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
|
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
|
||||||
dto := ProductWarehouseListDTO{
|
dto := ProductWarehouseListDTO{
|
||||||
ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(e),
|
ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(e),
|
||||||
@@ -84,18 +104,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
|
|||||||
|
|
||||||
// Map Product relation jika ada
|
// Map Product relation jika ada
|
||||||
if e.Product.Id != 0 {
|
if e.Product.Id != 0 {
|
||||||
product := ProductBaseDTO{
|
product := productDTO.ToProductBaseDTO(e.Product)
|
||||||
Id: e.Product.Id,
|
|
||||||
Name: e.Product.Name,
|
|
||||||
}
|
|
||||||
if e.Product.Sku != nil {
|
|
||||||
product.Sku = *e.Product.Sku
|
|
||||||
}
|
|
||||||
if len(e.Product.Flags) > 0 {
|
|
||||||
for _, f := range e.Product.Flags {
|
|
||||||
product.Flags = append(product.Flags, f.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dto.Product = &product
|
dto.Product = &product
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +129,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if &e.Warehouse.Area != nil && e.Warehouse.Area.Id != 0 {
|
if e.Warehouse.Area.Id != 0 {
|
||||||
warehouse.Area = &AreaBaseDTO{
|
warehouse.Area = &AreaBaseDTO{
|
||||||
Id: e.Warehouse.Area.Id,
|
Id: e.Warehouse.Area.Id,
|
||||||
Name: e.Warehouse.Area.Name,
|
Name: e.Warehouse.Area.Name,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
sProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
|
sProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
|
||||||
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
|
||||||
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"
|
||||||
@@ -17,8 +18,9 @@ type ProductWarehouseModule struct{}
|
|||||||
func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
kandangRepo := rKandang.NewKandangRepository(db)
|
||||||
|
|
||||||
productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate)
|
productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate, kandangRepo)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ProductWarehouseRoutes(router, userService, productWarehouseService)
|
ProductWarehouseRoutes(router, userService, productWarehouseService)
|
||||||
|
|||||||
+68
-7
@@ -19,8 +19,12 @@ type ProductWarehouseRepository interface {
|
|||||||
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
|
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
|
||||||
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||||
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
|
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
|
||||||
|
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||||
|
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
|
||||||
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
||||||
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseRepositoryImpl struct {
|
type ProductWarehouseRepositoryImpl struct {
|
||||||
@@ -44,6 +48,10 @@ func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint
|
|||||||
return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
|
return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) IdExists(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{}).
|
||||||
@@ -78,14 +86,14 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
|
|||||||
|
|
||||||
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
||||||
var productWarehouses []entity.ProductWarehouse
|
var productWarehouses []entity.ProductWarehouse
|
||||||
err := r.DB().WithContext(ctx).
|
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||||
Table("product_warehouses").
|
|
||||||
Select("product_warehouses.*").
|
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||||
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
||||||
Order("product_warehouses.created_at DESC").
|
Order("product_warehouses.created_at DESC")
|
||||||
Find(&productWarehouses).Error
|
|
||||||
|
// preload relations so nested Product and Warehouse are populated
|
||||||
|
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,12 +108,12 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c
|
|||||||
}
|
}
|
||||||
fmt.Println(warehouseId)
|
fmt.Println(warehouseId)
|
||||||
err := query.WithContext(ctx).
|
err := query.WithContext(ctx).
|
||||||
Table("product_warehouses").
|
Model(&entity.ProductWarehouse{}).
|
||||||
Select("product_warehouses.*").
|
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||||
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
||||||
Order("product_warehouses.created_at DESC").
|
Order("product_warehouses.created_at DESC").
|
||||||
|
Preload("Product").Preload("Warehouse").
|
||||||
First(&productWarehouse).Error
|
First(&productWarehouse).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -146,3 +154,56 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
|
||||||
|
var productWarehouse entity.ProductWarehouse
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Preload("Product").
|
||||||
|
Preload("Warehouse").
|
||||||
|
Preload("Warehouse.Area").
|
||||||
|
Preload("Warehouse.Location").
|
||||||
|
First(&productWarehouse, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &productWarehouse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
|
||||||
|
var product entity.Product
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||||
|
Where("product_categories.code = ?", categoryCode).
|
||||||
|
First(&product).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &product, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
||||||
|
var productWarehouses []entity.ProductWarehouse
|
||||||
|
err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||||
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||||
|
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
||||||
|
Order("product_warehouses.created_at DESC").
|
||||||
|
Preload("Product").Preload("Warehouse").
|
||||||
|
Find(&productWarehouses).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return productWarehouses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) {
|
||||||
|
var product entity.Product
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||||
|
Where("flags.name = ?", flagName).
|
||||||
|
First(&product).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &product, nil
|
||||||
|
}
|
||||||
|
|||||||
+25
-7
@@ -6,6 +6,7 @@ import (
|
|||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
|
||||||
|
kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -20,16 +21,18 @@ type ProductWarehouseService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type productWarehouseService struct {
|
type productWarehouseService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ProductWarehouseRepository
|
Repository repository.ProductWarehouseRepository
|
||||||
|
KandangRepo kandangrepo.KandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate) ProductWarehouseService {
|
func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate, kandangRepo kandangrepo.KandangRepository) ProductWarehouseService {
|
||||||
return &productWarehouseService{
|
return &productWarehouseService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
|
KandangRepo: kandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +72,16 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.KandangId > 0 {
|
||||||
|
isKandangExist, err := s.KandangRepo.IdExists(c.Context(), params.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if !isKandangExist {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
cleanFlags := utils.ParseFlags(params.Flags)
|
cleanFlags := utils.ParseFlags(params.Flags)
|
||||||
@@ -80,6 +93,11 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
|||||||
db = db.Where("product_id = ?", params.ProductId)
|
db = db.Where("product_id = ?", params.ProductId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.KandangId != 0 {
|
||||||
|
db = db.Joins("JOIN warehouses ON product_warehouses.warehouse_id = warehouses.id").
|
||||||
|
Where("warehouses.kandang_id = ?", params.KandangId)
|
||||||
|
}
|
||||||
|
|
||||||
if params.WarehouseId != 0 {
|
if params.WarehouseId != 0 {
|
||||||
db = db.Where("warehouse_id = ?", params.WarehouseId)
|
db = db.Where("warehouse_id = ?", params.WarehouseId)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -18,4 +18,5 @@ type Query struct {
|
|||||||
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"`
|
Flags string `query:"flags" validate:"omitempty"`
|
||||||
|
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
|
||||||
}
|
}
|
||||||
|
|||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeliveryOrdersController struct {
|
||||||
|
DeliveryOrdersService service.DeliveryOrdersService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersService) *DeliveryOrdersController {
|
||||||
|
return &DeliveryOrdersController{
|
||||||
|
DeliveryOrdersService: deliveryOrdersService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
MarketingId: uint(c.QueryInt("marketing_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.DeliveryOrdersService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.MarketingListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all deliveryOrderss successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DeliveryOrdersController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.DeliveryOrdersService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get deliveryOrders successfully",
|
||||||
|
Data: *result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.DeliveryOrdersService.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Create delivery products successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Update)
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.DeliveryOrdersService.UpdateOne(c, req, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Update deliveryOrders successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
|
productwarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
|
||||||
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
SoNumber string `json:"so_number"`
|
||||||
|
SoDate time.Time `json:"so_date"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingListDTO struct {
|
||||||
|
MarketingBaseDTO
|
||||||
|
Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"`
|
||||||
|
SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"`
|
||||||
|
SoDocs string `json:"so_docs,omitempty"`
|
||||||
|
SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingDetailDTO struct {
|
||||||
|
MarketingBaseDTO
|
||||||
|
Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"`
|
||||||
|
SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"`
|
||||||
|
SoDocs string `json:"so_docs,omitempty"`
|
||||||
|
SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"`
|
||||||
|
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"`
|
||||||
|
}
|
||||||
|
type MarketingDeliveryProductDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
MarketingProductId uint `json:"marketing_product_id"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
TotalWeight float64 `json:"total_weight"`
|
||||||
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
DeliveryDate *time.Time `json:"delivery_date"`
|
||||||
|
VehicleNumber string `json:"vehicle_number"`
|
||||||
|
ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryItemDTO struct {
|
||||||
|
ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
TotalWeight float64 `json:"total_weight"`
|
||||||
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
VehicleNumber string `json:"vehicle_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryGroupDTO struct {
|
||||||
|
DoNumber string `json:"do_number"`
|
||||||
|
DeliveryDate *time.Time `json:"delivery_date"`
|
||||||
|
Warehouse *productwarehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||||
|
Deliveries []DeliveryItemDTO `json:"deliveries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingProductDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
MarketingId uint `json:"marketing_id"`
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
|
TotalWeight float64 `json:"total_weight"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"`
|
||||||
|
VehicleNumber string `json:"vehicle_number,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMarketingBaseDTO(marketing *entity.Marketing) MarketingBaseDTO {
|
||||||
|
return MarketingBaseDTO{
|
||||||
|
Id: marketing.Id,
|
||||||
|
SoNumber: marketing.SoNumber,
|
||||||
|
SoDate: marketing.SoDate,
|
||||||
|
Notes: marketing.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO {
|
||||||
|
var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO
|
||||||
|
if e.ProductWarehouse.Id != 0 {
|
||||||
|
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
|
||||||
|
productWarehouse = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return MarketingProductDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
MarketingId: e.MarketingId,
|
||||||
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
|
Qty: e.Qty,
|
||||||
|
UnitPrice: e.UnitPrice,
|
||||||
|
AvgWeight: e.AvgWeight,
|
||||||
|
TotalWeight: e.TotalWeight,
|
||||||
|
TotalPrice: e.TotalPrice,
|
||||||
|
ProductWarehouse: productWarehouse,
|
||||||
|
VehicleNumber: getVehicleNumber(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO {
|
||||||
|
return MarketingDeliveryProductDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
MarketingProductId: e.MarketingProductId,
|
||||||
|
Qty: e.Qty,
|
||||||
|
UnitPrice: e.UnitPrice,
|
||||||
|
TotalWeight: e.TotalWeight,
|
||||||
|
AvgWeight: e.AvgWeight,
|
||||||
|
TotalPrice: e.TotalPrice,
|
||||||
|
DeliveryDate: e.DeliveryDate,
|
||||||
|
VehicleNumber: e.VehicleNumber,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingListDTO {
|
||||||
|
var createdUser *userDTO.UserBaseDTO
|
||||||
|
if marketing.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer *customerDTO.CustomerBaseDTO
|
||||||
|
if marketing.Customer.Id != 0 {
|
||||||
|
mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer)
|
||||||
|
customer = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var salesPerson *userDTO.UserBaseDTO
|
||||||
|
if marketing.SalesPerson.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson)
|
||||||
|
salesPerson = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestApproval *approvalDTO.ApprovalBaseDTO
|
||||||
|
if marketing.LatestApproval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
|
||||||
|
latestApproval = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var salesOrderProducts []MarketingProductDTO
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
|
||||||
|
for i, product := range marketing.Products {
|
||||||
|
salesOrderProducts[i] = ToMarketingProductDTO(product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MarketingListDTO{
|
||||||
|
MarketingBaseDTO: ToMarketingBaseDTO(marketing),
|
||||||
|
Customer: customer,
|
||||||
|
SalesPerson: salesPerson,
|
||||||
|
SoDocs: marketing.SoDocs,
|
||||||
|
SalesOrder: salesOrderProducts,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
CreatedAt: marketing.CreatedAt,
|
||||||
|
UpdatedAt: marketing.UpdatedAt,
|
||||||
|
LatestApproval: latestApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingDetailDTO {
|
||||||
|
var createdUser *userDTO.UserBaseDTO
|
||||||
|
if marketing.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer *customerDTO.CustomerBaseDTO
|
||||||
|
if marketing.Customer.Id != 0 {
|
||||||
|
mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer)
|
||||||
|
customer = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var salesPerson *userDTO.UserBaseDTO
|
||||||
|
if marketing.SalesPerson.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson)
|
||||||
|
salesPerson = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var salesOrderProducts []MarketingProductDTO
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
|
||||||
|
for i, product := range marketing.Products {
|
||||||
|
salesOrderProducts[i] = ToMarketingProductDTO(product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var deliveryProductsDTOs []MarketingDeliveryProductDTO
|
||||||
|
if len(deliveryProducts) > 0 {
|
||||||
|
deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(deliveryProducts))
|
||||||
|
for i, dp := range deliveryProducts {
|
||||||
|
deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp)
|
||||||
|
}
|
||||||
|
deliveryProductsDTOs = enrichDeliveryProductDTOsWithWarehouse(deliveryProductsDTOs, marketing)
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs, marketing.SoNumber)
|
||||||
|
|
||||||
|
var latestApproval *approvalDTO.ApprovalBaseDTO
|
||||||
|
if marketing.LatestApproval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
|
||||||
|
latestApproval = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return MarketingDetailDTO{
|
||||||
|
MarketingBaseDTO: ToMarketingBaseDTO(marketing),
|
||||||
|
SoDocs: marketing.SoDocs,
|
||||||
|
Customer: customer,
|
||||||
|
SalesPerson: salesPerson,
|
||||||
|
SalesOrder: salesOrderProducts,
|
||||||
|
DeliveryOrder: deliveryGroups,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
CreatedAt: marketing.CreatedAt,
|
||||||
|
UpdatedAt: marketing.UpdatedAt,
|
||||||
|
LatestApproval: latestApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMarketingListDTOs(marketings []entity.Marketing) []MarketingListDTO {
|
||||||
|
result := make([]MarketingListDTO, len(marketings))
|
||||||
|
for i, m := range marketings {
|
||||||
|
result[i] = ToMarketingListDTO(&m, []entity.MarketingDeliveryProduct{})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func enrichDeliveryProductDTOsWithWarehouse(deliveryProductDTOs []MarketingDeliveryProductDTO, marketing *entity.Marketing) []MarketingDeliveryProductDTO {
|
||||||
|
if len(deliveryProductDTOs) == 0 || marketing == nil || len(marketing.Products) == 0 {
|
||||||
|
return deliveryProductDTOs
|
||||||
|
}
|
||||||
|
|
||||||
|
productMap := make(map[uint]*entity.MarketingProduct)
|
||||||
|
for i := range marketing.Products {
|
||||||
|
productMap[marketing.Products[i].Id] = &marketing.Products[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range deliveryProductDTOs {
|
||||||
|
if product, exists := productMap[deliveryProductDTOs[i].MarketingProductId]; exists {
|
||||||
|
if product.ProductWarehouse.Id != 0 {
|
||||||
|
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(product.ProductWarehouse)
|
||||||
|
deliveryProductDTOs[i].ProductWarehouse = &mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveryProductDTOs
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber string) []DeliveryGroupDTO {
|
||||||
|
groupMap := make(map[string]*DeliveryGroupDTO)
|
||||||
|
|
||||||
|
for _, product := range products {
|
||||||
|
if product.DeliveryDate == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var warehouseId uint
|
||||||
|
var warehouseName string
|
||||||
|
if product.ProductWarehouse != nil {
|
||||||
|
warehouseId = product.ProductWarehouse.Warehouse.Id
|
||||||
|
warehouseName = product.ProductWarehouse.Warehouse.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
key := fmt.Sprintf("%d_%s", warehouseId, product.DeliveryDate.Format("2006-01-02"))
|
||||||
|
|
||||||
|
group, exists := groupMap[key]
|
||||||
|
if !exists {
|
||||||
|
group = &DeliveryGroupDTO{
|
||||||
|
DeliveryDate: product.DeliveryDate,
|
||||||
|
Warehouse: &productwarehouseDTO.WarehouseBaseDTO{
|
||||||
|
Id: warehouseId,
|
||||||
|
Name: warehouseName,
|
||||||
|
},
|
||||||
|
Deliveries: []DeliveryItemDTO{},
|
||||||
|
}
|
||||||
|
groupMap[key] = group
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryItem := DeliveryItemDTO{
|
||||||
|
ProductWarehouse: product.ProductWarehouse,
|
||||||
|
Qty: product.Qty,
|
||||||
|
UnitPrice: product.UnitPrice,
|
||||||
|
TotalWeight: product.TotalWeight,
|
||||||
|
AvgWeight: product.AvgWeight,
|
||||||
|
TotalPrice: product.TotalPrice,
|
||||||
|
VehicleNumber: product.VehicleNumber,
|
||||||
|
}
|
||||||
|
group.Deliveries = append(group.Deliveries, deliveryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups []DeliveryGroupDTO
|
||||||
|
for _, group := range groupMap {
|
||||||
|
groups = append(groups, *group)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(groups, func(i, j int) bool {
|
||||||
|
if groups[i].DeliveryDate == nil || groups[j].DeliveryDate == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return groups[i].DeliveryDate.Before(*groups[j].DeliveryDate)
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := range groups {
|
||||||
|
if groups[i].DeliveryDate != nil {
|
||||||
|
dateStr := groups[i].DeliveryDate.Format("20060102")
|
||||||
|
groups[i].DoNumber = fmt.Sprintf("%s-%s-%d", soNumber, dateStr, groups[i].Warehouse.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVehicleNumber(e entity.MarketingProduct) string {
|
||||||
|
if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" {
|
||||||
|
return e.DeliveryProduct.VehicleNumber
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package delivery_orderss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
||||||
|
sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
||||||
|
rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeliveryOrdersModule struct{}
|
||||||
|
|
||||||
|
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
marketingRepo := rMarketing.NewMarketingRepository(db)
|
||||||
|
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
|
||||||
|
marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
|
// Register workflow steps for MARKETINGS approval
|
||||||
|
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
DeliveryOrdersRoutes(router, userService, deliveryOrdersService)
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeliveryOrdersRepository interface {
|
||||||
|
repository.BaseRepository[entity.DeliveryOrders]
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryOrdersRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.DeliveryOrders]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeliveryOrdersRepository(db *gorm.DB) DeliveryOrdersRepository {
|
||||||
|
return &DeliveryOrdersRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.DeliveryOrders](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package delivery_orderss
|
||||||
|
|
||||||
|
import (
|
||||||
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers"
|
||||||
|
deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) {
|
||||||
|
ctrl := controller.NewDeliveryOrdersController(s)
|
||||||
|
|
||||||
|
v1.Get("/", ctrl.GetAll)
|
||||||
|
v1.Get("/:id", ctrl.GetOne)
|
||||||
|
|
||||||
|
// Sisanya di group /delivery-orders
|
||||||
|
route := v1.Group("/delivery-orders")
|
||||||
|
|
||||||
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
||||||
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
||||||
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeliveryOrdersService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
|
||||||
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error)
|
||||||
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type deliveryOrdersService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
MarketingRepo marketingRepo.MarketingRepository
|
||||||
|
MarketingProductRepo marketingRepo.MarketingProductRepository
|
||||||
|
MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository
|
||||||
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeliveryOrdersService(
|
||||||
|
marketingRepo marketingRepo.MarketingRepository,
|
||||||
|
marketingProductRepo marketingRepo.MarketingProductRepository,
|
||||||
|
marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository,
|
||||||
|
approvalSvc commonSvc.ApprovalService,
|
||||||
|
validate *validator.Validate,
|
||||||
|
) DeliveryOrdersService {
|
||||||
|
return &deliveryOrdersService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
MarketingRepo: marketingRepo,
|
||||||
|
MarketingProductRepo: marketingProductRepo,
|
||||||
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
|
ApprovalSvc: approvalSvc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("Customer").
|
||||||
|
Preload("SalesPerson").
|
||||||
|
Preload("Products.ProductWarehouse.Product").
|
||||||
|
Preload("Products.ProductWarehouse.Warehouse").
|
||||||
|
Preload("Products.DeliveryProduct")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) {
|
||||||
|
marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketingId, nil)
|
||||||
|
if err != nil {
|
||||||
|
}
|
||||||
|
marketing.LatestApproval = latestApproval
|
||||||
|
|
||||||
|
allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketingId)
|
||||||
|
if err != nil {
|
||||||
|
allDeliveryProducts = []entity.MarketingDeliveryProduct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts)
|
||||||
|
return &responseDTO, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("Customer").
|
||||||
|
Preload("SalesPerson").
|
||||||
|
Preload("Products.ProductWarehouse.Product").
|
||||||
|
Preload("Products.ProductWarehouse.Warehouse").
|
||||||
|
Preload("Products.DeliveryProduct")
|
||||||
|
|
||||||
|
if params.MarketingId != 0 {
|
||||||
|
return db.Where("id = ?", params.MarketingId)
|
||||||
|
}
|
||||||
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get marketings: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
for i := range marketings {
|
||||||
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("ActionUser")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err)
|
||||||
|
}
|
||||||
|
marketings[i].LatestApproval = latestApproval
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]dto.MarketingListDTO, len(marketings))
|
||||||
|
for i, marketing := range marketings {
|
||||||
|
result[i] = dto.ToMarketingListDTO(&marketing, []entity.MarketingDeliveryProduct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) {
|
||||||
|
|
||||||
|
marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
allDeliveryProducts = []entity.MarketingDeliveryProduct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketing.Id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("ActionUser")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
} else if len(approvals) > 0 {
|
||||||
|
if marketing.LatestApproval == nil {
|
||||||
|
latest := approvals[len(approvals)-1]
|
||||||
|
marketing.LatestApproval = &latest
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
marketing.LatestApproval = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts)
|
||||||
|
return &responseDTO, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB()))
|
||||||
|
|
||||||
|
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
|
}
|
||||||
|
if latestApproval == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval")
|
||||||
|
}
|
||||||
|
if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order")
|
||||||
|
}
|
||||||
|
if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing")
|
||||||
|
}
|
||||||
|
if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing is not approved - current status: %v", *latestApproval.Action))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
||||||
|
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
|
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, requestedProduct := range req.DeliveryProducts {
|
||||||
|
var foundMarketingProduct *entity.MarketingProduct
|
||||||
|
for i := range allMarketingProducts {
|
||||||
|
if allMarketingProducts[i].Id == requestedProduct.MarketingProductId {
|
||||||
|
foundMarketingProduct = &allMarketingProducts[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundMarketingProduct == nil {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemDeliveryDate *time.Time
|
||||||
|
if requestedProduct.DeliveryDate != "" {
|
||||||
|
parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err))
|
||||||
|
}
|
||||||
|
itemDeliveryDate = &parsedDate
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryProduct.Qty = requestedProduct.Qty
|
||||||
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||||
|
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
||||||
|
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
||||||
|
deliveryProduct.TotalPrice = requestedProduct.TotalPrice
|
||||||
|
deliveryProduct.DeliveryDate = itemDeliveryDate
|
||||||
|
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
||||||
|
|
||||||
|
if requestedProduct.Qty > 0 {
|
||||||
|
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, requestedProduct.Qty); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := uint(1) // TODO: ambil dari auth context
|
||||||
|
approvalAction := entity.ApprovalActionApproved
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowMarketing,
|
||||||
|
req.MarketingId,
|
||||||
|
utils.MarketingDeliveryOrder,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
nil); err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.getMarketingWithDeliveries(c, req.MarketingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
||||||
|
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
|
|
||||||
|
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.DeliveryProducts) > 0 {
|
||||||
|
for _, requestedProduct := range req.DeliveryProducts {
|
||||||
|
|
||||||
|
var foundMarketingProduct *entity.MarketingProduct
|
||||||
|
for i := range allMarketingProducts {
|
||||||
|
if allMarketingProducts[i].Id == requestedProduct.MarketingProductId {
|
||||||
|
foundMarketingProduct = &allMarketingProducts[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundMarketingProduct == nil {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemDeliveryDate *time.Time
|
||||||
|
if requestedProduct.DeliveryDate != "" {
|
||||||
|
parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err))
|
||||||
|
}
|
||||||
|
itemDeliveryDate = &parsedDate
|
||||||
|
} else if deliveryProduct.DeliveryDate != nil {
|
||||||
|
itemDeliveryDate = deliveryProduct.DeliveryDate
|
||||||
|
}
|
||||||
|
|
||||||
|
oldQty := deliveryProduct.Qty
|
||||||
|
deliveryProduct.Qty = requestedProduct.Qty
|
||||||
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||||
|
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
||||||
|
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
||||||
|
deliveryProduct.TotalPrice = requestedProduct.TotalPrice
|
||||||
|
deliveryProduct.DeliveryDate = itemDeliveryDate
|
||||||
|
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
||||||
|
|
||||||
|
qtyChange := requestedProduct.Qty - oldQty
|
||||||
|
if qtyChange > 0 {
|
||||||
|
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if qtyChange < 0 {
|
||||||
|
if err := s.restoreProductWarehouseStock(c.Context(), dbTransaction, foundMarketingProduct, -qtyChange); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery order")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.getMarketingWithDeliveries(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s deliveryOrdersService) validateAndReduceProductWarehouse(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyDeliver float64) error {
|
||||||
|
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
||||||
|
|
||||||
|
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pw.Quantity < qtyDeliver {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver))
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Quantity = pw.Quantity - qtyDeliver
|
||||||
|
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s deliveryOrdersService) restoreProductWarehouseStock(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyRestore float64) error {
|
||||||
|
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
||||||
|
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Quantity = pw.Quantity + qtyRestore
|
||||||
|
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type DeliveryProduct struct {
|
||||||
|
MarketingProductId uint `json:"marketing_product_id" validate:"required,gt=0"`
|
||||||
|
Qty float64 `json:"qty" validate:"omitempty,gte=0"`
|
||||||
|
UnitPrice float64 `json:"unit_price" validate:"omitempty,gte=0"`
|
||||||
|
AvgWeight float64 `json:"avg_weight" validate:"omitempty,gte=0"`
|
||||||
|
TotalWeight float64 `json:"total_weight" validate:"omitempty,gte=0"`
|
||||||
|
TotalPrice float64 `json:"total_price" validate:"omitempty,gte=0"`
|
||||||
|
DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
MarketingId uint `json:"marketing_id" validate:"required,gt=0"`
|
||||||
|
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Approve struct {
|
||||||
|
Action string `json:"action" validate:"required_strict"`
|
||||||
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package marketing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingModule struct{}
|
||||||
|
|
||||||
|
func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
RegisterRoutes(router, db, validate)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package marketing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders"
|
||||||
|
deliveryOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss"
|
||||||
|
// MODULE IMPORTS
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
group := router.Group("/marketing")
|
||||||
|
|
||||||
|
allModules := []modules.Module{
|
||||||
|
salesOrderss.SalesOrdersModule{},
|
||||||
|
deliveryOrderss.DeliveryOrdersModule{},
|
||||||
|
// MODULE REGISTRY
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range allModules {
|
||||||
|
m.RegisterRoutes(group, db, validate)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SalesOrdersController struct {
|
||||||
|
SalesOrdersService service.SalesOrdersService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSalesOrdersController(salesOrdersService service.SalesOrdersService) *SalesOrdersController {
|
||||||
|
return &SalesOrdersController{
|
||||||
|
SalesOrdersService: salesOrdersService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.SalesOrdersService.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Create salesOrders successfully",
|
||||||
|
Data: dto.ToSalesOrdersListDTOFromMarketing(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Update)
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.SalesOrdersService.UpdateOne(c, req, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Update salesOrders successfully",
|
||||||
|
Data: dto.ToSalesOrdersListDTOFromMarketing(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SalesOrdersController) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.SalesOrdersService.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Delete salesOrders successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *SalesOrdersController) Approval(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Approve)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := u.SalesOrdersService.Approval(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
data interface{}
|
||||||
|
message = "Submit sales order approval successfully"
|
||||||
|
)
|
||||||
|
if len(results) == 1 {
|
||||||
|
data = dto.ToSalesOrdersListDTOFromMarketing(results[0])
|
||||||
|
} else {
|
||||||
|
message = "Submit sales order approvals successfully"
|
||||||
|
data = dto.ToSalesOrdersListDTOsFromMarketing(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: message,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type MarketingProductDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
|
TotalWeight float64 `json:"total_weight"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SalesOrdersListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
SoNumber string `json:"so_number"`
|
||||||
|
SoDate time.Time `json:"so_date"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO {
|
||||||
|
var productWarehouse *productWarehouseDTO.ProductWarehousNestedDTO
|
||||||
|
|
||||||
|
if e.ProductWarehouse.Id != 0 {
|
||||||
|
mapped := productWarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
|
||||||
|
productWarehouse = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return MarketingProductDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Qty: e.Qty,
|
||||||
|
UnitPrice: e.UnitPrice,
|
||||||
|
AvgWeight: e.AvgWeight,
|
||||||
|
TotalWeight: e.TotalWeight,
|
||||||
|
TotalPrice: e.TotalPrice,
|
||||||
|
ProductWarehouse: productWarehouse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSalesOrdersListDTO(e entity.Marketing) SalesOrdersListDTO {
|
||||||
|
products := make([]MarketingProductDTO, len(e.Products))
|
||||||
|
for i, p := range e.Products {
|
||||||
|
products[i] = ToMarketingProductDTO(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SalesOrdersListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
SoNumber: e.SoNumber,
|
||||||
|
SoDate: e.SoDate,
|
||||||
|
Notes: e.Notes,
|
||||||
|
SalesOrder: products,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO {
|
||||||
|
var salesOrder []MarketingProductDTO
|
||||||
|
if len(e.Products) > 0 {
|
||||||
|
salesOrder = make([]MarketingProductDTO, len(e.Products))
|
||||||
|
for i, product := range e.Products {
|
||||||
|
salesOrder[i] = ToMarketingProductDTO(product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SalesOrdersListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
SoNumber: e.SoNumber,
|
||||||
|
SoDate: e.SoDate,
|
||||||
|
Notes: e.Notes,
|
||||||
|
SalesOrder: salesOrder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListDTO {
|
||||||
|
result := make([]SalesOrdersListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToSalesOrdersListDTOFromMarketing(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package sales_orders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||||
|
sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
||||||
|
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SalesOrdersModule struct{}
|
||||||
|
|
||||||
|
func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
marketingRepo := rSalesOrders.NewMarketingRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
customerRepo := rCustomer.NewCustomerRepository(db)
|
||||||
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
|
||||||
|
|
||||||
|
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
salesOrdersService := sSalesOrders.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
SalesOrdersRoutes(router, userService, salesOrdersService)
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingDeliveryProductRepository interface {
|
||||||
|
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingDeliveryProductRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
|
||||||
|
return &MarketingDeliveryProductRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingProductRepository interface {
|
||||||
|
repository.BaseRepository[entity.MarketingProduct]
|
||||||
|
GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error)
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingProductRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.MarketingProduct]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarketingProductRepository(db *gorm.DB) MarketingProductRepository {
|
||||||
|
return &MarketingProductRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingProduct](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) {
|
||||||
|
var products []entity.MarketingProduct
|
||||||
|
if err := r.DB().WithContext(ctx).Where("marketing_id = ?", marketingID).Find(&products).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(products) == 0 {
|
||||||
|
return products, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return products, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingProductRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.MarketingProduct](ctx, r.DB(), id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingRepository interface {
|
||||||
|
repository.BaseRepository[entity.Marketing]
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
|
GetNextSequence(ctx context.Context) (uint, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.Marketing]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarketingRepository(db *gorm.DB) MarketingRepository {
|
||||||
|
return &MarketingRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.Marketing](ctx, r.DB(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) {
|
||||||
|
var maxID uint
|
||||||
|
if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return maxID + 1, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package sales_orders
|
||||||
|
|
||||||
|
import (
|
||||||
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
|
||||||
|
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) {
|
||||||
|
ctrl := controller.NewSalesOrdersController(s)
|
||||||
|
|
||||||
|
v1.Delete("/:id", ctrl.DeleteOne)
|
||||||
|
route := v1.Group("/sales-orders")
|
||||||
|
|
||||||
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
|
|
||||||
|
route.Post("/approvals", ctrl.Approval)
|
||||||
|
}
|
||||||
@@ -0,0 +1,551 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
||||||
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
||||||
|
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||||
|
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SalesOrdersService interface {
|
||||||
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Marketing, error)
|
||||||
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error)
|
||||||
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type salesOrdersService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
MarketingRepo repository.MarketingRepository
|
||||||
|
CustomerRepo customerRepo.CustomerRepository
|
||||||
|
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||||
|
UserRepo userRepo.UserRepository
|
||||||
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService {
|
||||||
|
return &salesOrdersService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
MarketingRepo: marketingRepo,
|
||||||
|
CustomerRepo: customerRepo,
|
||||||
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
|
UserRepo: userRepo,
|
||||||
|
ApprovalSvc: approvalSvc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("Customer").
|
||||||
|
Preload("SalesPerson").
|
||||||
|
Preload("Products.ProductWarehouse.Product").
|
||||||
|
Preload("Products.ProductWarehouse.Warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) {
|
||||||
|
marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get marketing by id: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("ActionUser")
|
||||||
|
})
|
||||||
|
if err == nil && len(approvals) > 0 {
|
||||||
|
latest := approvals[len(approvals)-1]
|
||||||
|
marketing.LatestApproval = &latest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return marketing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range req.MarketingProducts {
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
soDate, err := utils.ParseDateString(req.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSeq, err := s.MarketingRepo.GetNextSequence(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number")
|
||||||
|
}
|
||||||
|
soNumber := fmt.Sprintf("SO-%05d", nextSeq)
|
||||||
|
|
||||||
|
var marketing *entity.Marketing
|
||||||
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||||
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
|
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
|
marketing = &entity.Marketing{
|
||||||
|
CustomerId: req.CustomerId,
|
||||||
|
SoNumber: soNumber,
|
||||||
|
SoDate: soDate,
|
||||||
|
SalesPersonId: req.SalesPersonId,
|
||||||
|
Notes: req.Notes,
|
||||||
|
CreatedBy: 1,
|
||||||
|
}
|
||||||
|
if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.MarketingProducts) > 0 {
|
||||||
|
for _, product := range req.MarketingProducts {
|
||||||
|
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := uint(1) // TODO: ambil dari auth context
|
||||||
|
approvalAction := entity.ApprovalActionCreated
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowMarketing,
|
||||||
|
marketing.Id,
|
||||||
|
utils.MarketingStepPengajuan,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
nil); err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders")
|
||||||
|
}
|
||||||
|
|
||||||
|
marketing, err = s.MarketingRepo.GetByID(c.Context(), marketing.Id, s.withRelations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created sales order")
|
||||||
|
}
|
||||||
|
|
||||||
|
return marketing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists},
|
||||||
|
commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists},
|
||||||
|
commonSvc.RelationCheck{Name: "SalesPerson", ID: &req.SalesPersonId, Exists: s.UserRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
|
}
|
||||||
|
if latestApproval != nil && latestApproval.StepNumber >= 3 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Cannot update sales order after delivery order approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.MarketingProducts) > 0 {
|
||||||
|
for _, item := range req.MarketingProducts {
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||||
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
|
|
||||||
|
updateBody := make(map[string]any)
|
||||||
|
if req.CustomerId != 0 {
|
||||||
|
updateBody["customer_id"] = req.CustomerId
|
||||||
|
}
|
||||||
|
if req.SalesPersonId != 0 {
|
||||||
|
updateBody["sales_person_id"] = req.SalesPersonId
|
||||||
|
}
|
||||||
|
if req.Date != "" {
|
||||||
|
soDate, err := utils.ParseDateString(req.Date)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
|
||||||
|
}
|
||||||
|
updateBody["so_date"] = soDate
|
||||||
|
}
|
||||||
|
if req.Notes != "" {
|
||||||
|
updateBody["notes"] = req.Notes
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateBody) > 0 {
|
||||||
|
if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.MarketingProducts) > 0 {
|
||||||
|
|
||||||
|
oldProducts, err := marketingProductRepoTx.GetByMarketingID(c.Context(), id)
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch existing products")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldByPW := make(map[uint]*entity.MarketingProduct)
|
||||||
|
for i := range oldProducts {
|
||||||
|
p := oldProducts[i]
|
||||||
|
oldByPW[p.ProductWarehouseId] = &p
|
||||||
|
}
|
||||||
|
|
||||||
|
reqByPW := make(map[uint]validation.CreateMarketingProduct)
|
||||||
|
for _, rp := range req.MarketingProducts {
|
||||||
|
reqByPW[rp.ProductWarehouseId] = rp
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rp := range req.MarketingProducts {
|
||||||
|
if old, ok := oldByPW[rp.ProductWarehouseId]; ok {
|
||||||
|
|
||||||
|
updateBody := map[string]any{
|
||||||
|
"product_warehouse_id": rp.ProductWarehouseId,
|
||||||
|
"qty": rp.Qty,
|
||||||
|
"unit_price": rp.UnitPrice,
|
||||||
|
"avg_weight": rp.AvgWeight,
|
||||||
|
"total_weight": rp.TotalWeight,
|
||||||
|
"total_price": rp.TotalPrice,
|
||||||
|
}
|
||||||
|
if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure delivery product exists; if not, create default
|
||||||
|
if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
mdp := &entity.MarketingDeliveryProduct{
|
||||||
|
MarketingProductId: old.Id,
|
||||||
|
Qty: 0,
|
||||||
|
UnitPrice: 0,
|
||||||
|
TotalWeight: 0,
|
||||||
|
AvgWeight: 0,
|
||||||
|
TotalPrice: 0,
|
||||||
|
DeliveryDate: nil,
|
||||||
|
VehicleNumber: rp.VehicleNumber,
|
||||||
|
}
|
||||||
|
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new marketing product (use helper)
|
||||||
|
if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Delete missing old products (prevent deletion if deliveries exist)
|
||||||
|
for _, old := range oldProducts {
|
||||||
|
if _, ok := reqByPW[old.ProductWarehouseId]; !ok {
|
||||||
|
// Check delivery product for this marketing product
|
||||||
|
deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product")
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
// If delivery exists (delivery_date not nil or qty > 0), prevent deletion
|
||||||
|
if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id))
|
||||||
|
}
|
||||||
|
// safe to delete delivery product record
|
||||||
|
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Delete marketing product
|
||||||
|
if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestApproval != nil && latestApproval.StepNumber == 2 {
|
||||||
|
actorID := uint(1) // todo: ambil dari auth context
|
||||||
|
resetNote := ""
|
||||||
|
action := entity.ApprovalActionApproved
|
||||||
|
_, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowMarketing,
|
||||||
|
id,
|
||||||
|
utils.MarketingStepPengajuan,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
&resetNote)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.getOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
|
marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
|
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||||
|
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
for _, product := range marketing.Products {
|
||||||
|
if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("marketing_product_id = ?", product.Id)
|
||||||
|
}); err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("marketing_id = ?", id)
|
||||||
|
}); err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := marketingRepoTx.DeleteOne(c.Context(), id); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return fiberErr
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB()))
|
||||||
|
|
||||||
|
var action entity.ApprovalAction
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
||||||
|
case string(entity.ApprovalActionRejected):
|
||||||
|
action = entity.ApprovalActionRejected
|
||||||
|
case string(entity.ApprovalActionApproved):
|
||||||
|
action = entity.ApprovalActionApproved
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
|
||||||
|
if len(approvableIDs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range approvableIDs {
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
|
}
|
||||||
|
if latestApproval == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d - sales orders must be created first", id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
switch latestApproval.StepNumber {
|
||||||
|
case uint16(utils.MarketingStepPengajuan):
|
||||||
|
case uint16(utils.MarketingStepSalesOrder):
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
|
for _, approvableID := range approvableIDs {
|
||||||
|
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check current approval step")
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestApproval == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", approvableID))
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextStep approvalutils.ApprovalStep
|
||||||
|
currentStep := latestApproval.StepNumber
|
||||||
|
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
|
||||||
|
if currentStep == uint16(utils.MarketingStepPengajuan) {
|
||||||
|
nextStep = utils.MarketingStepSalesOrder
|
||||||
|
} else if currentStep == uint16(utils.MarketingStepSalesOrder) {
|
||||||
|
nextStep = utils.MarketingDeliveryOrder
|
||||||
|
} else {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d already completed all approval steps", approvableID))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
nextStep = approvalutils.ApprovalStep(currentStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := uint(1) // todo ambil dari auth context
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowMarketing,
|
||||||
|
approvableID,
|
||||||
|
nextStep,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := make([]entity.Marketing, 0, len(approvableIDs))
|
||||||
|
for _, id := range approvableIDs {
|
||||||
|
marketing, err := s.getOne(c, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updated = append(updated, *marketing)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo rInvMarketingDeliveryProduct.MarketingDeliveryProductRepository) error {
|
||||||
|
|
||||||
|
marketingProduct := &entity.MarketingProduct{
|
||||||
|
MarketingId: marketingId,
|
||||||
|
ProductWarehouseId: rp.ProductWarehouseId,
|
||||||
|
Qty: rp.Qty,
|
||||||
|
UnitPrice: rp.UnitPrice,
|
||||||
|
AvgWeight: rp.AvgWeight,
|
||||||
|
TotalWeight: rp.TotalWeight,
|
||||||
|
TotalPrice: rp.TotalPrice,
|
||||||
|
}
|
||||||
|
if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
|
||||||
|
MarketingProductId: marketingProduct.Id,
|
||||||
|
Qty: 0,
|
||||||
|
UnitPrice: 0,
|
||||||
|
TotalWeight: 0,
|
||||||
|
AvgWeight: 0,
|
||||||
|
TotalPrice: 0,
|
||||||
|
DeliveryDate: nil,
|
||||||
|
VehicleNumber: rp.VehicleNumber,
|
||||||
|
}
|
||||||
|
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
CustomerId uint `json:"customer_id" validate:"required,gt=0"`
|
||||||
|
SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"`
|
||||||
|
Date string `json:"date" validate:"required,datetime=2006-01-02"`
|
||||||
|
Notes string `json:"notes" validate:"omitempty,max=500"`
|
||||||
|
MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateMarketingProduct struct {
|
||||||
|
VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"`
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"`
|
||||||
|
UnitPrice float64 `json:"unit_price" validate:"required,gt=0"`
|
||||||
|
TotalWeight float64 `json:"total_weight" validate:"required,gt=0"`
|
||||||
|
Qty float64 `json:"qty" validate:"required,gt=0"`
|
||||||
|
AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"`
|
||||||
|
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
CustomerId uint `json:"customer_id" validate:"omitempty,gt=0"`
|
||||||
|
SalesPersonId uint `json:"sales_person_id" validate:"omitempty,gt=0"`
|
||||||
|
Date string `json:"date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
Notes string `json:"notes" validate:"omitempty,max=500"`
|
||||||
|
MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Approve struct {
|
||||||
|
Action string `json:"action" validate:"required_strict"`
|
||||||
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ type CustomerRepository interface {
|
|||||||
repository.BaseRepository[entity.Customer]
|
repository.BaseRepository[entity.Customer]
|
||||||
PicExists(ctx context.Context, areaId uint) (bool, error)
|
PicExists(ctx context.Context, areaId uint) (bool, error)
|
||||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerRepositoryImpl struct {
|
type CustomerRepositoryImpl struct {
|
||||||
@@ -33,3 +34,7 @@ func (r *CustomerRepositoryImpl) PicExists(ctx context.Context, picId uint) (boo
|
|||||||
func (r *CustomerRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
func (r *CustomerRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||||
return repository.ExistsByName[entity.Customer](ctx, r.db, name, excludeID)
|
return repository.ExistsByName[entity.Customer](ctx, r.db, name, excludeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *CustomerRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.Customer](ctx, r.db, id)
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type KandangBaseDTO struct {
|
|||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
Capacity float64 `json:"capacity"`
|
||||||
Location *locationDTO.LocationBaseDTO `json:"location"`
|
Location *locationDTO.LocationBaseDTO `json:"location"`
|
||||||
Pic *userDTO.UserBaseDTO `json:"pic"`
|
Pic *userDTO.UserBaseDTO `json:"pic"`
|
||||||
}
|
}
|
||||||
@@ -48,6 +49,7 @@ func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
|
|||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
Status: e.Status,
|
Status: e.Status,
|
||||||
|
Capacity: e.Capacity,
|
||||||
Location: location,
|
Location: location,
|
||||||
Pic: pic,
|
Pic: pic,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type KandangRepository interface {
|
|||||||
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
|
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
|
||||||
UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error
|
UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error
|
||||||
UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error
|
UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type KandangRepositoryImpl struct {
|
type KandangRepositoryImpl struct {
|
||||||
@@ -59,6 +60,10 @@ func (r *KandangRepositoryImpl) ProjectFlockExists(ctx context.Context, projectF
|
|||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *KandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.Kandang](ctx, r.db, id)
|
||||||
|
}
|
||||||
|
|
||||||
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).
|
||||||
|
|||||||
@@ -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,7 +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.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
createBody := &entity.Kandang{
|
createBody := &entity.Kandang{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
LocationId: req.LocationId,
|
LocationId: req.LocationId,
|
||||||
|
Capacity: req.Capacity,
|
||||||
Status: status,
|
Status: status,
|
||||||
PicId: req.PicId,
|
PicId: req.PicId,
|
||||||
CreatedBy: 1,
|
CreatedBy: 1,
|
||||||
@@ -194,6 +195,10 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
updateBody["pic_id"] = *req.PicId
|
updateBody["pic_id"] = *req.PicId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Capacity != nil {
|
||||||
|
updateBody["capacity"] = *req.Capacity
|
||||||
|
}
|
||||||
|
|
||||||
finalStatus := strings.ToUpper(existing.Status)
|
finalStatus := strings.ToUpper(existing.Status)
|
||||||
if req.Status != nil {
|
if req.Status != nil {
|
||||||
status := strings.ToUpper(*req.Status)
|
status := strings.ToUpper(*req.Status)
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `json:"name" validate:"required_strict,min=3"`
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
Status string `json:"status,omitempty" validate:"omitempty,min=3"`
|
Status string `json:"status,omitempty" validate:"omitempty,min=3"`
|
||||||
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
Capacity float64 `json:"capacity" validate:"required_strict,gt=0"`
|
||||||
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
|
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
||||||
ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"`
|
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
|
||||||
|
ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
|
Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
|
||||||
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
Capacity *float64 `json:"capacity" validate:"omitempty,gt=0"`
|
||||||
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
|
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"`
|
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
|
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ type UomBaseDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UomListDTO struct {
|
type UomListDTO struct {
|
||||||
UomBaseDTO
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -42,7 +43,8 @@ func ToUomListDTO(e entity.Uom) UomListDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return UomListDTO{
|
return UomListDTO{
|
||||||
UomBaseDTO: ToUomBaseDTO(e),
|
Id: e.Id,
|
||||||
|
Name: e.Name,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ type WarehouseRepository interface {
|
|||||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
||||||
|
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
||||||
|
GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarehouseRepositoryImpl struct {
|
type WarehouseRepositoryImpl struct {
|
||||||
@@ -60,3 +62,28 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
|
|||||||
}
|
}
|
||||||
return &warehouse, nil
|
return &warehouse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *WarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error) {
|
||||||
|
var warehouse entity.Warehouse
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Area").
|
||||||
|
Preload("Location").
|
||||||
|
First(&warehouse, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &warehouse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
|
||||||
|
var warehouse entity.Warehouse
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("kandang_id = ?", kandangId).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Order("id DESC").
|
||||||
|
First(&warehouse).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &warehouse, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
|
||||||
@@ -22,30 +21,84 @@ func NewChickinController(chickinService service.ChickinService) *ChickinControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ChickinController) GetAll(c *fiber.Ctx) error {
|
// func (u *ChickinController) GetAll(c *fiber.Ctx) error {
|
||||||
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),
|
||||||
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
|
// ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result, totalResults, err := u.ChickinService.GetAll(c, query)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return c.Status(fiber.StatusOK).
|
||||||
|
// JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
|
||||||
|
// Code: fiber.StatusOK,
|
||||||
|
// Status: "success",
|
||||||
|
// Message: "Get all chickins successfully",
|
||||||
|
// Meta: response.Meta{
|
||||||
|
// Page: query.Page,
|
||||||
|
// Limit: query.Limit,
|
||||||
|
// TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
// TotalResults: totalResults,
|
||||||
|
// },
|
||||||
|
// Data: dto.ToChickinListDTOs(result),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (u *ChickinController) GetOne(c *fiber.Ctx) error {
|
||||||
|
// param := c.Params("id")
|
||||||
|
|
||||||
|
// id, err := strconv.Atoi(param)
|
||||||
|
// if err != nil {
|
||||||
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result, err := u.ChickinService.GetOne(c, uint(id))
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return c.Status(fiber.StatusOK).
|
||||||
|
// JSON(response.Success{
|
||||||
|
// Code: fiber.StatusOK,
|
||||||
|
// Status: "success",
|
||||||
|
// Message: "Get chickin successfully",
|
||||||
|
// Data: dto.ToChickinListDTO(*result),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.ChickinService.GetAll(c, query)
|
results, err := u.ChickinService.CreateOne(c, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
var (
|
||||||
JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
|
data interface{}
|
||||||
Code: fiber.StatusOK,
|
message = "Create chickin successfully"
|
||||||
|
)
|
||||||
|
if len(results) == 1 {
|
||||||
|
data = dto.ToChickinListDTO(results[0])
|
||||||
|
} else {
|
||||||
|
message = "Create chickins successfully"
|
||||||
|
data = dto.ToChickinListDTOs(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get all chickins successfully",
|
Message: message,
|
||||||
Meta: response.Meta{
|
Data: data,
|
||||||
Page: query.Page,
|
|
||||||
Limit: query.Limit,
|
|
||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
|
||||||
TotalResults: totalResults,
|
|
||||||
},
|
|
||||||
Data: dto.ToChickinListDTOs(result),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,95 +120,85 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get chickin successfully",
|
Message: "Get chickin successfully",
|
||||||
Data: dto.ToChickinListDTO(*result),
|
Data: dto.ToChickinDetailDTO(*result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
|
// func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Create)
|
// req := new(validation.Update)
|
||||||
|
// param := c.Params("id")
|
||||||
|
|
||||||
|
// id, err := strconv.Atoi(param)
|
||||||
|
// if err != nil {
|
||||||
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := c.BodyParser(req); err != nil {
|
||||||
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result, err := u.ChickinService.UpdateOne(c, req, uint(id))
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return c.Status(fiber.StatusOK).
|
||||||
|
// JSON(response.Success{
|
||||||
|
// Code: fiber.StatusOK,
|
||||||
|
// Status: "success",
|
||||||
|
// Message: "Update chickin successfully",
|
||||||
|
// Data: dto.ToChickinListDTO(*result),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
// param := c.Params("id")
|
||||||
|
|
||||||
|
// id, err := strconv.Atoi(param)
|
||||||
|
// if err != nil {
|
||||||
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return c.Status(fiber.StatusOK).
|
||||||
|
// JSON(response.Common{
|
||||||
|
// Code: fiber.StatusOK,
|
||||||
|
// Status: "success",
|
||||||
|
// Message: "Delete chickin successfully",
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (u *ChickinController) Approval(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Approve)
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ChickinService.CreateOne(c, req)
|
results, err := u.ChickinService.Approval(c, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).
|
var (
|
||||||
JSON(response.Success{
|
data interface{}
|
||||||
Code: fiber.StatusCreated,
|
message = "Submit chickin approval successfully"
|
||||||
Status: "success",
|
)
|
||||||
Message: "Create chickin successfully",
|
if len(results) == 1 {
|
||||||
Data: dto.ToChickinListDTO(*result),
|
data = dto.ToChickinListDTO(results[0])
|
||||||
})
|
} else {
|
||||||
}
|
message = "Submit chickin approvals successfully"
|
||||||
|
data = dto.ToChickinListDTOs(results)
|
||||||
func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Update)
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ChickinService.UpdateOne(c, req, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Update chickin successfully",
|
Message: message,
|
||||||
Data: dto.ToChickinListDTO(*result),
|
Data: data,
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Common{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Delete chickin successfully",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ChickinController) Approve(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := u.ChickinService.Approve(c, uint(id)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Approve chickin successfully",
|
|
||||||
Data: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,29 @@ 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"
|
||||||
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs (ordered) ===
|
// === DTO Structs (ordered) ===
|
||||||
|
|
||||||
|
type ProductWarehouseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Product *productDTO.ProductBaseDTO `json:"product,omitempty"`
|
||||||
|
Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ChickinBaseDTO struct {
|
type ChickinBaseDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
ChickInDate time.Time `json:"chick_in_date"`
|
ChickInDate time.Time `json:"chick_in_date"`
|
||||||
Quantity float64 `json:"quantity"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
Note string `json:"note"`
|
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||||
|
UsageQty float64 `json:"usage_qty"`
|
||||||
|
PendingUsageQty float64 `json:"pending_usage_qty"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectFlockDTO struct {
|
type ProjectFlockDTO struct {
|
||||||
@@ -45,21 +56,32 @@ type ChickinSimpleDTO struct {
|
|||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
ChickInDate time.Time `json:"chick_in_date"`
|
ChickInDate time.Time `json:"chick_in_date"`
|
||||||
Quantity float64 `json:"quantity"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
Note string `json:"note"`
|
UsageQty float64 `json:"usage_qty"`
|
||||||
|
PendingUsageQty float64 `json:"pending_usage_qty"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
CreatedBy uint `json:"created_by"`
|
CreatedBy uint `json:"created_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChickinListDTO struct {
|
type ChickinListDTO struct {
|
||||||
ChickinBaseDTO
|
ChickinBaseDTO
|
||||||
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
|
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
|
||||||
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChickinDetailDTO struct {
|
type ChickinDetailDTO struct {
|
||||||
ChickinListDTO
|
Id uint `json:"id"`
|
||||||
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
|
ChickInDate time.Time `json:"chick_in_date"`
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
UsageQty float64 `json:"usage_qty"`
|
||||||
|
PendingUsageQty float64 `json:"pending_usage_qty"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
CreatedBy uint `json:"created_by"`
|
||||||
|
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions (ordered) ===
|
// === Mapper Functions (ordered) ===
|
||||||
@@ -138,17 +160,29 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO {
|
func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO {
|
||||||
var pfk *ProjectFlockKandangDTO
|
var projectFlockKandangId uint
|
||||||
if e.ProjectFlockKandang.Id != 0 {
|
// Check if ProjectFlockKandang relation is loaded
|
||||||
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
|
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 {
|
||||||
pfk = &mapped
|
projectFlockKandangId = e.ProjectFlockKandang.Id
|
||||||
|
} else if e.ProjectFlockKandangId != 0 {
|
||||||
|
// If relation is not loaded but ID is available, use the ID
|
||||||
|
projectFlockKandangId = e.ProjectFlockKandangId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var productWarehouse *ProductWarehouseDTO
|
||||||
|
if e.ProductWarehouse != nil && e.ProductWarehouse.Id != 0 {
|
||||||
|
productWarehouse = toProductWarehouseDTO(e.ProductWarehouse)
|
||||||
|
}
|
||||||
|
|
||||||
return ChickinBaseDTO{
|
return ChickinBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlockKandang: pfk,
|
ProjectFlockKandangId: projectFlockKandangId,
|
||||||
ChickInDate: e.ChickInDate,
|
ChickInDate: e.ChickInDate,
|
||||||
Quantity: e.Quantity,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
Note: e.Note,
|
ProductWarehouse: productWarehouse,
|
||||||
|
UsageQty: e.UsageQty,
|
||||||
|
PendingUsageQty: e.PendingUsageQty,
|
||||||
|
Notes: e.Notes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,29 +191,25 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO {
|
|||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||||
ChickInDate: e.ChickInDate,
|
ChickInDate: e.ChickInDate,
|
||||||
Quantity: e.Quantity,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
Note: e.Note,
|
UsageQty: e.UsageQty,
|
||||||
|
PendingUsageQty: e.PendingUsageQty,
|
||||||
|
Notes: e.Notes,
|
||||||
CreatedBy: e.CreatedBy,
|
CreatedBy: e.CreatedBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO {
|
func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO {
|
||||||
var createdUser *userBaseDTO.UserBaseDTO
|
var createdUser *userBaseDTO.UserBaseDTO
|
||||||
if e.CreatedUser.Id != 0 {
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser)
|
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||||
createdUser = &mapped
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
var pfk *ProjectFlockKandangDTO
|
|
||||||
if e.ProjectFlockKandang.Id != 0 {
|
|
||||||
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
|
|
||||||
pfk = &mapped
|
|
||||||
}
|
|
||||||
return ChickinListDTO{
|
return ChickinListDTO{
|
||||||
ChickinBaseDTO: ToChickinBaseDTO(e),
|
ChickinBaseDTO: ToChickinBaseDTO(e),
|
||||||
ProjectFlockKandang: pfk,
|
CreatedUser: createdUser,
|
||||||
CreatedUser: createdUser,
|
CreatedAt: e.CreatedAt,
|
||||||
CreatedAt: e.CreatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +230,53 @@ func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO {
|
func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO {
|
||||||
|
var createdUser *userBaseDTO.UserBaseDTO
|
||||||
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return ChickinDetailDTO{
|
return ChickinDetailDTO{
|
||||||
ChickinListDTO: ToChickinListDTO(e),
|
Id: e.Id,
|
||||||
|
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||||
|
ChickInDate: e.ChickInDate,
|
||||||
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
|
UsageQty: e.UsageQty,
|
||||||
|
PendingUsageQty: e.PendingUsageQty,
|
||||||
|
Notes: e.Notes,
|
||||||
|
CreatedBy: e.CreatedBy,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO {
|
||||||
|
result := make([]ChickinDetailDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToChickinDetailDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helper Functions ===
|
||||||
|
|
||||||
|
// ToProductWarehouseDTO adalah exported helper untuk mapping ProductWarehouse (shared logic)
|
||||||
|
func ToProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO {
|
||||||
|
if pw == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
product := productDTO.ToProductBaseDTO(pw.Product)
|
||||||
|
warehouse := warehouseDTO.ToWarehouseBaseDTO(pw.Warehouse)
|
||||||
|
|
||||||
|
return &ProductWarehouseDTO{
|
||||||
|
Id: pw.Id,
|
||||||
|
Product: &product,
|
||||||
|
Warehouse: &warehouse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO {
|
||||||
|
return ToProductWarehouseDTO(pw)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package chickins
|
package chickins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
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"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
@@ -15,6 +20,8 @@ import (
|
|||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
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"
|
||||||
|
|
||||||
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChickinModule struct{}
|
type ChickinModule struct{}
|
||||||
@@ -32,6 +39,12 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
|||||||
@@ -11,21 +11,26 @@ import (
|
|||||||
type ProjectChickinRepository interface {
|
type ProjectChickinRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectChickin]
|
repository.BaseRepository[entity.ProjectChickin]
|
||||||
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
|
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
|
||||||
|
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||||
|
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||||
|
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChickinRepositoryImpl struct {
|
type ChickinRepositoryImpl struct {
|
||||||
*repository.BaseRepositoryImpl[entity.ProjectChickin]
|
*repository.BaseRepositoryImpl[entity.ProjectChickin]
|
||||||
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChickinRepository(db *gorm.DB) ProjectChickinRepository {
|
func NewChickinRepository(db *gorm.DB) ProjectChickinRepository {
|
||||||
return &ChickinRepositoryImpl{
|
return &ChickinRepositoryImpl{
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db),
|
||||||
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) {
|
func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) {
|
||||||
var chickin entity.ProjectChickin
|
var chickin entity.ProjectChickin
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Where("project_floc_id = ?", projectFlockID).
|
Where("project_floc_id = ?", projectFlockID).
|
||||||
Where("deleted_at IS NULL").
|
Where("deleted_at IS NULL").
|
||||||
First(&chickin).Error
|
First(&chickin).Error
|
||||||
@@ -34,3 +39,43 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
|
|||||||
}
|
}
|
||||||
return &chickin, nil
|
return &chickin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||||
|
var chickins []entity.ProjectChickin
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&chickins).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return chickins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||||
|
var chickins []entity.ProjectChickin
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where("usage_qty = 0").
|
||||||
|
Where("pending_usage_qty > 0").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&chickins).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return chickins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectChickin{}).
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where("pending_usage_qty > 0").
|
||||||
|
Select("COALESCE(SUM(pending_usage_qty), 0)").
|
||||||
|
Row().Scan(&total)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|||||||
+25
@@ -1,6 +1,8 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"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"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -8,6 +10,10 @@ import (
|
|||||||
|
|
||||||
type ProjectChickinDetailRepository interface {
|
type ProjectChickinDetailRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectChickinDetail]
|
repository.BaseRepository[entity.ProjectChickinDetail]
|
||||||
|
CreateOne(ctx context.Context, entity *entity.ProjectChickinDetail, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error)
|
||||||
|
WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChickinDetailRepositoryImpl struct {
|
type ChickinDetailRepositoryImpl struct {
|
||||||
@@ -19,3 +25,22 @@ func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository {
|
|||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ChickinDetailRepositoryImpl) WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository {
|
||||||
|
return &ChickinDetailRepositoryImpl{BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](tx)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChickinDetailRepositoryImpl) DB() *gorm.DB {
|
||||||
|
return r.BaseRepositoryImpl.DB()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChickinDetailRepositoryImpl) GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) {
|
||||||
|
var records []entity.ProjectChickinDetail
|
||||||
|
if err := r.DB().WithContext(ctx).Where("project_chickin_id = ?", projectChickinID).Find(&records).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
|
|||||||
route := v1.Group("/chickins")
|
route := v1.Group("/chickins")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
// route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
// route.Patch("/:id", ctrl.UpdateOne)
|
||||||
route.Delete("/:id", ctrl.DeleteOne)
|
// route.Delete("/:id", ctrl.DeleteOne)
|
||||||
route.Post("/:id/approve", ctrl.Approve)
|
route.Post("/approvals", ctrl.Approval)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
@@ -21,10 +25,10 @@ import (
|
|||||||
type ChickinService interface {
|
type ChickinService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
Approve(ctx *fiber.Ctx, id uint) error
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type chickinService struct {
|
type chickinService struct {
|
||||||
@@ -76,6 +80,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
}
|
}
|
||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.ProjectFlockKandangId != 0 {
|
if params.ProjectFlockKandangId != 0 {
|
||||||
@@ -103,112 +108,159 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e
|
|||||||
return chickin, nil
|
return chickin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) {
|
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
|
projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId)
|
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get warehouse: %+v", err)
|
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found")
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// move complex DB query into repository for cleaner service
|
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
||||||
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to get product warehouses: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(productWarehouses) == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
|
|
||||||
}
|
|
||||||
totalQuantity := 0.0
|
|
||||||
for _, pw := range productWarehouses {
|
|
||||||
totalQuantity += pw.Quantity
|
|
||||||
}
|
|
||||||
|
|
||||||
if totalQuantity < 1 {
|
actorID := uint(1) // todo nanti ambil dari auth context
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
|
newChikins := make([]*entity.ProjectChickin, 0)
|
||||||
}
|
|
||||||
|
|
||||||
chickinDate, err := utils.ParseDateString(req.ChickInDate)
|
for _, chickinReq := range req.ChickinRequests {
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to parse chickin date: %+v", err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format")
|
|
||||||
}
|
|
||||||
newChickin := &entity.ProjectChickin{
|
|
||||||
ProjectFlockKandangId: projectflockkandang.Id,
|
|
||||||
ChickInDate: chickinDate,
|
|
||||||
Quantity: totalQuantity,
|
|
||||||
Note: req.Note,
|
|
||||||
CreatedBy: 1, //todo: ganti dengan user login
|
|
||||||
}
|
|
||||||
err = s.Repository.CreateOne(c.Context(), newChickin, nil)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to create chickin: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update semua product warehouse: set quantity jadi 0
|
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, nil)
|
||||||
for _, pw := range productWarehouses {
|
|
||||||
err = s.ProductWarehouseRepo.PatchOne(c.Context(), pw.Id, map[string]any{
|
|
||||||
"quantity": 0,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickinReq.ProductWarehouseId))
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newChickinDetail := &entity.ProjectChickinDetail{
|
if productWarehouse.WarehouseId != warehouse.Id {
|
||||||
ProjectChickinId: newChickin.Id,
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId))
|
||||||
ProductWarehouseId: pw.Id,
|
|
||||||
Quantity: pw.Quantity,
|
|
||||||
CreatedBy: 1, // todo: ganti dengan user login
|
|
||||||
}
|
}
|
||||||
err = s.ProjectChickinDetailRepo.CreateOne(c.Context(), newChickinDetail, nil)
|
|
||||||
|
chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to create chickin detail: %+v", err)
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
|
availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, productWarehouse, category)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
s.Log.Errorf("Failed to get project flock population: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if existingPopulation != nil {
|
|
||||||
|
|
||||||
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), existingPopulation.Id, map[string]any{
|
|
||||||
"reserved_quantity": newChickin.Quantity + existingPopulation.ReservedQuantity,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to update project flock population: %+v", err)
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
newPopulation := &entity.ProjectFlockPopulation{
|
if availableQty <= 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available quantity for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||||
|
}
|
||||||
|
|
||||||
|
newChickin := &entity.ProjectChickin{
|
||||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||||
InitialQuantity: 0,
|
ChickInDate: chickinDate,
|
||||||
CurrentQuantity: 0,
|
UsageQty: 0,
|
||||||
ReservedQuantity: newChickin.Quantity,
|
PendingUsageQty: availableQty,
|
||||||
CreatedBy: 1, // todo: ganti dengan user login
|
ProductWarehouseId: chickinReq.ProductWarehouseId,
|
||||||
}
|
Notes: chickinReq.Note,
|
||||||
err = s.ProjectflockPopulationRepo.CreateOne(c.Context(), newPopulation, nil)
|
CreatedBy: actorID,
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to create project flock population: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newChikins = append(newChikins, newChickin)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetOne(c, newChickin.Id)
|
if len(newChikins) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create")
|
||||||
|
}
|
||||||
|
|
||||||
|
existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
isFirstTime := len(existingChikins) == 0
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
for _, chickin := range newChikins {
|
||||||
|
updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)}
|
||||||
|
|
||||||
|
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var approvalAction entity.ApprovalAction
|
||||||
|
if isFirstTime {
|
||||||
|
approvalAction = entity.ApprovalActionCreated
|
||||||
|
} else {
|
||||||
|
approvalAction = entity.ApprovalActionUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest == nil {
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
projectFlockKandang.Id,
|
||||||
|
utils.ProjectFlockKandangStepPengajuan,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
nil); err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
projectFlockKandang.Id,
|
||||||
|
utils.ProjectFlockKandangStepPengajuan,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
nil); err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]entity.ProjectChickin, 0, len(newChikins))
|
||||||
|
for _, chickin := range newChikins {
|
||||||
|
loaded, err := s.GetOne(c, chickin.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reload chickin %d with relations: %v", chickin.Id, err))
|
||||||
|
}
|
||||||
|
result = append(result, *loaded)
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) {
|
func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) {
|
||||||
@@ -222,7 +274,7 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
updateBody["chick_in_date"] = req.ChickInDate
|
updateBody["chick_in_date"] = req.ChickInDate
|
||||||
}
|
}
|
||||||
if req.Note != "" {
|
if req.Note != "" {
|
||||||
updateBody["note"] = req.Note
|
updateBody["notes"] = req.Note
|
||||||
}
|
}
|
||||||
if len(updateBody) == 0 {
|
if len(updateBody) == 0 {
|
||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
@@ -240,174 +292,335 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
db := s.Repository.DB()
|
|
||||||
|
|
||||||
tx := db.WithContext(c.Context()).Begin()
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
if tx.Error != nil {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
s.Log.Errorf("Failed to begin transaction: %+v", tx.Error)
|
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||||
return tx.Error
|
|
||||||
}
|
|
||||||
rollback := func(err error) error {
|
|
||||||
if rerr := tx.Rollback().Error; rerr != nil {
|
|
||||||
s.Log.Errorf("Rollback failed: %+v", rerr)
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
chickinRepoTx := s.Repository.WithTx(tx)
|
return nil
|
||||||
pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx)
|
}
|
||||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx)
|
|
||||||
|
|
||||||
chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil)
|
func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
availableQty := productWarehouse.Quantity
|
||||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed get chickin by id: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var population entity.ProjectFlockPopulation
|
if category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil {
|
var totalPendingQty float64
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found"))
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to get project flock population: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newReserved := population.ReservedQuantity - chickin.Quantity
|
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||||
if newReserved < 0 {
|
if err == nil {
|
||||||
newReserved = 0
|
for _, chickin := range chickins {
|
||||||
}
|
|
||||||
if err := tx.WithContext(c.Context()).Model(&entity.ProjectFlockPopulation{}).Where("id = ?", population.Id).Updates(map[string]any{"reserved_quantity": newReserved}).Error; err != nil {
|
|
||||||
s.Log.Errorf("Failed to update project flock population: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreFromDetails := func() (bool, error) {
|
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
||||||
var details []entity.ProjectChickinDetail
|
totalPendingQty += chickin.PendingUsageQty
|
||||||
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
|
}
|
||||||
return false, err
|
}
|
||||||
}
|
|
||||||
if len(details) == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range details {
|
availableQty = productWarehouse.Quantity - totalPendingQty
|
||||||
var pw entity.ProductWarehouse
|
if availableQty < 0 {
|
||||||
if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil {
|
availableQty = 0
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
}
|
||||||
|
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
var totalPopulation float64
|
||||||
|
var totalPendingQty float64
|
||||||
|
|
||||||
|
populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id)
|
||||||
|
if err == nil {
|
||||||
|
for _, pop := range populations {
|
||||||
|
totalPopulation += pop.TotalQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||||
|
if err == nil {
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
|
||||||
|
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
||||||
|
totalPendingQty += chickin.PendingUsageQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
|
||||||
|
if availableQty < 0 {
|
||||||
|
availableQty = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableQty, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
|
||||||
|
|
||||||
|
var action entity.ApprovalAction
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
||||||
|
case string(entity.ApprovalActionRejected):
|
||||||
|
action = entity.ApprovalActionRejected
|
||||||
|
case string(entity.ApprovalActionApproved):
|
||||||
|
action = entity.ApprovalActionApproved
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
|
||||||
|
if len(approvableIDs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range approvableIDs {
|
||||||
|
idCopy := id
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
|
}
|
||||||
|
if latestApproval == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
|
||||||
|
}
|
||||||
|
if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step := utils.ProjectFlockKandangStepPengajuan
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
step = utils.ProjectFlockKandangStepDisetujui
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||||
|
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
for _, approvableID := range approvableIDs {
|
||||||
|
actorID := uint(1) // todo nanti ambil dari auth context
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
approvableID,
|
||||||
|
step,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID))
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
|
||||||
|
|
||||||
|
if category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
|
||||||
|
}
|
||||||
|
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
||||||
|
}
|
||||||
|
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
|
||||||
|
}
|
||||||
|
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if action == entity.ApprovalActionRejected {
|
||||||
|
chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chickins) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedQuantity := pw.Quantity + d.Quantity
|
kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
|
||||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
|
if err != nil {
|
||||||
return false, err
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category))
|
||||||
|
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
|
||||||
|
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
|
updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)}
|
||||||
|
|
||||||
|
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
restored, err := restoreFromDetails()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to restore from chickin details: %+v", err)
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
return rollback(err)
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !restored {
|
updated := make([]entity.ProjectChickin, 0)
|
||||||
|
for _, kandangID := range approvableIDs {
|
||||||
|
var chickins []entity.ProjectChickin
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load approved chickins")
|
||||||
|
}
|
||||||
|
updated = append(updated, chickins...)
|
||||||
|
}
|
||||||
|
|
||||||
projectflockkandang, err := pfkRepoTx.GetByID(c.Context(), population.ProjectFlockKandangId)
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) {
|
||||||
|
|
||||||
|
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
|
||||||
|
if err == nil && len(products) > 0 {
|
||||||
|
return &products[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
|
||||||
|
}
|
||||||
|
if product == nil {
|
||||||
|
return nil, fmt.Errorf("no %s product found in system", categoryCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPW := &entity.ProductWarehouse{
|
||||||
|
ProductId: product.Id,
|
||||||
|
WarehouseId: warehouseId,
|
||||||
|
Quantity: 0,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPW, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
|
||||||
|
|
||||||
|
if targetPW == nil || targetPW.Id == 0 {
|
||||||
|
return fmt.Errorf("invalid target product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||||
|
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
|
||||||
|
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
|
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
|
||||||
return rollback(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var warehouse entity.Warehouse
|
if populationExists {
|
||||||
if err := tx.WithContext(c.Context()).Where("kandang_id = ?", projectflockkandang.KandangId).First(&warehouse).Error; err != nil {
|
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
continue
|
||||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang"))
|
}
|
||||||
|
|
||||||
|
quantityToConvert := chickin.PendingUsageQty
|
||||||
|
|
||||||
|
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
|
||||||
|
"usage_qty": quantityToConvert,
|
||||||
|
"pending_usage_qty": 0,
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chickin.ProductWarehouseId != targetPW.Id {
|
||||||
|
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
|
||||||
|
"quantity": gorm.Expr("quantity - ?", quantityToConvert),
|
||||||
|
}, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err)
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to get warehouse: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
productWarehouse, err := s.ProductWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(
|
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
|
||||||
c.Context(),
|
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
|
||||||
"DOC",
|
}, nil); err != nil {
|
||||||
warehouse.Id,
|
|
||||||
tx,
|
|
||||||
)
|
|
||||||
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 fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to get product warehouse: %+v", err)
|
return fmt.Errorf("failed to update target warehouse quantity: %w", err)
|
||||||
return rollback(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedQuantity := productWarehouse.Quantity + chickin.Quantity
|
population := &entity.ProjectFlockPopulation{
|
||||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
|
ProjectChickinId: chickin.Id,
|
||||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
ProductWarehouseId: targetPW.Id,
|
||||||
return rollback(err)
|
TotalQty: quantityToConvert,
|
||||||
|
TotalUsedQty: 0,
|
||||||
|
Notes: chickin.Notes,
|
||||||
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
}
|
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
|
||||||
|
return err
|
||||||
// delete chickin (single place)
|
|
||||||
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
|
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to delete chickin: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
s.Log.Errorf("Failed to commit transaction: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *chickinService) Approve(c *fiber.Ctx, id uint) error {
|
|
||||||
|
|
||||||
// todo: ini contoh akhir jika sudah approved
|
|
||||||
|
|
||||||
chickin, err := s.Repository.GetByID(
|
|
||||||
c.Context(),
|
|
||||||
id,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed get chickin by id: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to get project flock population: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{
|
|
||||||
"reserved_quantity": population.ReservedQuantity - chickin.Quantity,
|
|
||||||
"initial_quantity": population.InitialQuantity + chickin.Quantity,
|
|
||||||
"current_quantity": population.CurrentQuantity + chickin.Quantity,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to update project flock population: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user