Compare commits

..

39 Commits

Author SHA1 Message Date
ragilap 770adbd3ff fix(BE-69,70,71,72,73): add & implement middleware auth 2025-11-03 17:21:58 +07:00
ragilap 50119ac538 Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into refactor-to-serve/with-middleware 2025-11-03 16:57:10 +07:00
ragilap d4a0d5c68b feat/BE/US-76/TASK-122,133,121,120 Recording add create delete edit 2025-10-28 09:57:44 +07:00
ragilap 054ad2ad20 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into refactor-to-serve/with-middleware 2025-10-27 10:21:15 +07:00
Adnan Zahir 171191c97d chore: ignore vendor, delete pipeline 2025-10-23 11:51:59 +07:00
Adnan Zahir 587cfabb4a Merge branch 'development-after-rebase' into 'development'
chore(REBASE): Development with SSO

See merge request mbugroup/lti-api!40
2025-10-23 11:50:06 +07:00
Adnan Zahir 3ede6461cf Merge branch 'development' into 'development-after-rebase'
# Conflicts:
#   Makefile
#   internal/middleware/auth.go
#   internal/route/route.go
2025-10-23 11:48:52 +07:00
ragilap 1dfd1f747e fix(BE): ignore makefile 2025-10-23 11:41:51 +07:00
ragilap 40665b0d8f Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token 2025-10-23 11:41:46 +07:00
kris 94f4929749 Update .gitlab-ci.yml file 2025-10-23 11:41:00 +07:00
GitLab Deploy Bot ad815b3412 . 2025-10-23 11:40:58 +07:00
Hafizh A. Y d41e16cab9 chore(BE): delete gitlab-ci.yml 2025-10-23 11:37:35 +07:00
ragilap 22e4728738 Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token 2025-10-23 11:37:32 +07:00
ragilap 501b6f8440 feat/login crud in users sync with sso 2025-10-23 11:36:29 +07:00
Adnan Zahir 3ea5bf6787 chore(CI): added [LTI API] label to webhook content 2025-10-23 11:30:14 +07:00
Adnan Zahir 0a4e06614b chore(CI): added gitlab ci yaml file for notify MR and MR-merged events 2025-10-23 11:30:13 +07:00
Adnan Zahir 45f41f87ff Merge branch 'fix/development' into 'development'
fix(BE): ignore makefile

See merge request mbugroup/lti-api!36
2025-10-22 21:29:00 +07:00
ragilap 02defcb86a fix(BE): ignore makefile 2025-10-22 21:25:16 +07:00
Mitra Berlian Unggas 0c791898ff Merge branch 'feat/sso-integration' into 'development'
Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token

See merge request mbugroup/lti-api!32
2025-10-22 12:12:23 +00:00
kris ddcda59239 Update .gitlab-ci.yml file 2025-10-21 17:07:46 +00:00
kris 85dfc33191 Merge branch 'fix-pipeline' into 'development'
fix: perbaikan file .gitlab-ci.yml dan docker-compose untuk LTI API

See merge request mbugroup/lti-api!33
2025-10-21 16:56:56 +00:00
GitLab Deploy Bot bb60e987e5 . 2025-10-21 23:45:13 +07:00
ragilap d5e8487f44 Merge branch 'feat/sso-integration' of https://gitlab.com/mbugroup/lti-api into feat/sso-integration 2025-10-21 20:32:52 +07:00
ragilap ab8c5d2ec4 Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token 2025-10-21 20:31:10 +07:00
Adnan Zahir 452403d71e Merge branch 'feat/sso-integration' into 'development'
Feat/sso integration

See merge request mbugroup/lti-api!31
2025-10-21 16:32:17 +07:00
Hafizh A. Y 5d676d5993 chore(BE): delete gitlab-ci.yml 2025-10-21 16:28:51 +07:00
ragilap e239246d02 Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token 2025-10-08 15:25:17 +07:00
Adnan Zahir 6c387b420c Merge branch 'feat/sso-integration' into 'development'
[FEAT/BE][Storyless-task/TASK-69-verify-authentication-flows-work-correctly] Sync login and crud in lti with sso

See merge request mbugroup/lti-api!9
2025-10-06 21:09:50 +07:00
Adnan Zahir ae84c9d8cc Merge branch 'chore/CI/merge-request-notify-workflow' into 'development'
chore(CI): added [LTI API] label to webhook content

See merge request mbugroup/lti-api!8
2025-10-06 16:50:17 +07:00
ragilap 6bddbbf9d9 feat/login crud in users sync with sso 2025-10-06 12:31:54 +07:00
Adnan Zahir d04b9278d2 chore(CI): added [LTI API] label to webhook content 2025-10-03 21:59:45 +07:00
Adnan Zahir 1684d69fae Merge branch 'chore/CI/merge-request-notify-workflow' into 'development'
chore(CI): added gitlab ci yaml file for notify MR and MR-merged events

See merge request mbugroup/lti-api!7
2025-10-03 21:51:14 +07:00
Adnan Zahir 3376f538fe chore(CI): added gitlab ci yaml file for notify MR and MR-merged events 2025-10-03 21:41:54 +07:00
Adnan Zahir d6c9747c54 Merge branch 'feat/BE/US-33/master-data-management' into 'development'
[FEAT/BE][US#33/TASK#36,37,38,39] Finish Master Data Management Api

See merge request mbugroup/lti-api!6
2025-10-03 21:20:04 +07:00
Hafizh A. Y. e4646faf30 Merge branch 'dev/hafizh' into 'feat/BE/US-33/master-data-management'
[FEAT/BE][US#33/TASK#36,37,38,39] Finish Master Data Management Api

See merge request mbugroup/lti-api!5
2025-10-03 14:08:08 +00:00
Adnan Zahir fac5f382ec Merge branch 'dev/hafizh' into 'development'
chore: update port so it doesn't conflict with docker sso

See merge request mbugroup/lti-api!4
2025-09-30 16:56:01 +07:00
Adnan Zahir 1053b779e4 Merge branch 'dev/hafizh' into 'development'
chore: adjust docker and any file for starting project

See merge request mbugroup/lti-api!3
2025-09-30 14:54:01 +07:00
Adnan Zahir 9444ad56dc Merge branch 'dev/hafizh' into 'development'
fix: adjust from boilerplate to lti project

See merge request mbugroup/lti-api!2
2025-09-25 11:28:53 +07:00
Adnan Zahir 4d26e6b7b4 Merge branch 'dev/hafizh' into 'development'
initial commit

See merge request mbugroup/lti-api!1
2025-09-25 10:50:10 +07:00
126 changed files with 3794 additions and 4060 deletions
+7 -3
View File
@@ -3,9 +3,13 @@ root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/main ./cmd/api"
bin = "tmp/main"
full_bin = "APP_ENV=dev ./tmp/main"
# Build binary utama
cmd = "go build -o /lti-api/tmp/main ./cmd/api"
# Lokasi binary hasil build
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"]
exclude_dir = ["vendor", "tmp"]
+22
View File
@@ -32,3 +32,25 @@ CORS_MAX_AGE=600
# Redis
REDIS_URL=redis://redis:6379/0
REDIS_PORT_HOST=6381
# SSO Integration
SSO_ISSUER=http://localhost:8080/api
# SSO_JWKS_URL=http://localhost:8080/api/.well-known/jwks.json
SSO_JWKS_URL=http://host.docker.internal:8080/api/.well-known/jwks.json
SSO_ALLOWED_AUDIENCES=client:lti-api
SSO_AUTHORIZE_URL=http://localhost:8080/sso/authorize
SSO_TOKEN_URL=http://localhost:8080/sso/token
SSO_GETME_URL=http://localhost:8080/api/auth/get-me
SSO_ACCESS_COOKIE_NAME=sso_access
SSO_REFRESH_COOKIE_NAME=sso_refresh
SSO_COOKIE_DOMAIN=
SSO_COOKIE_SECURE=false
SSO_COOKIE_SAMESITE=Lax
SSO_TOKEN_BLACKLIST_PREFIX=sso:blacklist
SSO_PKCE_TTL_SECONDS=300
# Security window and payload limits for SSO user sync webhook
SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS=120
SSO_USER_SYNC_NONCE_TTL_SECONDS=600
SSO_USER_SYNC_MAX_BODY_BYTES=32768
# Example JSON (single-line) of client configs (each client requires a unique sync_secret)
SSO_CLIENTS={"LTI":{"public_id":"Lumbung-Telur-Indonesia","redirect_uri":"http://localhost:8081/api/sso/callback","scope":"openid profile","default_return_uri":"http://localhost:3000","allowed_return_origins":["http://localhost:3000"],"sync_secret":"onUyfODIMHOh4TgGLgyWLmsNeVNxFRHqoLJFLPjr"}}
+58
View File
@@ -0,0 +1,58 @@
# .env.lti-api (Development Server with Domain)
# =============================================
# Server configuration
VERSION=0.0.1
APP_ENV=dev
APP_HOST=0.0.0.0
APP_PORT=8081
APP_URL=https://dev-api-lti.mbugroup.id
# Database configuration (pakai PostgreSQL milik SSO)
DB_HOST=sso-postgres
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=db_lti_erp
DB_PORT=5432
# JWT configuration
JWT_SECRET=changeme
JWT_ACCESS_EXP_MINUTES=30
JWT_REFRESH_EXP_DAYS=30
JWT_RESET_PASSWORD_EXP_MINUTES=10
JWT_VERIFY_EMAIL_EXP_MINUTES=10
# Redis (pakai Redis milik SSO)
REDIS_URL=redis://sso-redis:6379/0
# CORS configuration
CORS_ALLOW_ORIGINS=https://dev-api-sso.mbugroup.id,https://dev-lti.mbugroup.id,https://dev-api-lti.mbugroup.id,http://localhost:3000
CORS_ALLOW_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
CORS_ALLOW_HEADERS=Authorization,Content-Type,X-Requested-With
CORS_EXPOSE_HEADERS=Link,Location
CORS_ALLOW_CREDENTIALS=true
CORS_MAX_AGE=600
# SSO Integration (Gunakan domain backend SSO)
SSO_ISSUER=https://dev-api-sso.mbugroup.id
SSO_JWKS_URL=https://dev-api-sso.mbugroup.id/api/.well-known/jwks.json
SSO_ALLOWED_AUDIENCES=
SSO_AUTHORIZE_URL=https://dev-api-sso.mbugroup.id/api/sso/authorize
SSO_TOKEN_URL=https://dev-api-sso.mbugroup.id/api/sso/token
SSO_GETME_URL=https://dev-api-sso.mbugroup.id/api/auth/get-me
# Cookie & session configuration
SSO_ACCESS_COOKIE_NAME=sso_access
SSO_REFRESH_COOKIE_NAME=sso_refresh
SSO_COOKIE_DOMAIN=.mbugroup.id
SSO_COOKIE_SECURE=true
SSO_COOKIE_SAMESITE=Lax
SSO_PKCE_TTL_SECONDS=300
# SSO webhook / user sync settings
SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS=120
SSO_USER_SYNC_NONCE_TTL_SECONDS=600
SSO_USER_SYNC_MAX_BODY_BYTES=32768
# Client registration for SSO
SSO_CLIENTS={"Lumbung-Telur-Indonesia":{"public_id":"Lumbung-Telur-Indonesia","redirect_uri":"https://dev-api-lti.mbugroup.id/api/sso/callback","scope":"openid profile","default_return_uri":"https://dev-lti.mbugroup.id","allowed_return_origins":["https://dev-lti.mbugroup.id","http://localhost:3000"],"sync_secret":"onUyfODIMHOh4TgGLgyWLmsNeVNxFRHqoLJFLPjr"}}
+5
View File
@@ -10,8 +10,13 @@ bin/
*.exe
*.out
Makefile
docker-compose.local.yml
docker-compose.yaml
Dockerfile.local
# Go build cache
.gocache/
vendor/
# Logs & reports
*.log
+78
View File
@@ -0,0 +1,78 @@
stages:
- build
- deploy
- cleanup
# ==============================
# 🏗️ BUILD IMAGE (Overwrite :dev)
# ==============================
build_image:
stage: build
image: docker:latest
services:
- docker:dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- echo "🔧 Building Docker image for :dev..."
- docker login -u gitlab-ci-token -p "$CI_JOB_TOKEN" "$CI_REGISTRY"
- docker build -f Dockerfile.local -t registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev .
- docker push registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev
only:
- development
# ==============================
# 🚀 DEPLOY TO DEV SERVER
# ==============================
deploy_lti:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client bash curl
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
script:
- echo "🚀 Deploy ke ${SERVER_USER}@${SERVER_IP} menggunakan image :dev"
- |
ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} bash -s <<REMOTE
set -e
APP_NAME="lti-api"
DOCKER_IMAGE="registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev"
NETWORK_NAME="lti-network"
ENV_PATH="/home/devops/code/api/lti-api/.env.lti-api"
PORT=8081
echo "🔑 Login ke GitLab Registry..."
echo "${GITLAB_TOKEN}" | docker login -u "${GITLAB_USER}" --password-stdin registry.gitlab.com
echo "🛑 Stop & remove old container..."
docker stop "\${APP_NAME}" >/dev/null 2>&1 || true
docker rm -f "\${APP_NAME}" >/dev/null 2>&1 || true
echo "🧹 Membersihkan container zombie di port \${PORT}..."
OLD_ID=\$(docker ps -aq --filter "publish=\${PORT}")
if [ -n "\${OLD_ID}" ]; then
echo "⚠️ Container lain masih pakai port \${PORT}, hapus..."
docker stop \${OLD_ID} >/dev/null 2>&1 || true
docker rm -f \${OLD_ID} >/dev/null 2>&1 || true
fi
echo "🐳 Pull image baru..."
docker pull "\${DOCKER_IMAGE}"
echo "🚀 Run container baru..."
docker run -d --name "\${APP_NAME}" --restart always \
--env-file "\${ENV_PATH}" \
-p \${PORT}:8081 \
--network "\${NETWORK_NAME}" \
"\${DOCKER_IMAGE}"
echo "✅ Deployment selesai di port \${PORT}"
REMOTE
only:
- development
+43 -104
View File
@@ -1,120 +1,59 @@
# --- Load .env kalau ada, dan export ke shell child ---
ifneq (,$(wildcard .env))
include .env
export
endif
# ===============================
# LTI-API Makefile (Docker Setup)
# ===============================
# --- Konfigurasi umum ---
COMPOSE ?= docker compose -f docker-compose.local.yml
NETWORK ?= lti-api_go-network
MIGRATE_IMAGE ?= migrate/migrate
MIGRATIONS_DIR := $(PWD)/internal/database/migrations
APP_NAME := lti-api
COMPOSE := docker compose -f docker-compose.yaml
NETWORK := lti-network
ENV_FILE := .env.lti-api
# Fallback agar tetap jalan meski .env kosong
DB_HOST ?= postgresdb
DB_PORT ?= 5432
DB_USER ?= postgres
DB_PASSWORD ?= postgres
DB_NAME ?= db_lti_erp
include $(ENV_FILE)
export $(shell sed 's/=.*//' $(ENV_FILE))
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
MIGRATIONS_DIR := ./migrations
MIGRATE_IMAGE := migrate/migrate:v4.15.2
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@lti-postgres:5432/$(DB_NAME)?sslmode=disable
# Tunggu DB ready memakai pg_isready dari image postgres
WAIT_DB := docker run --rm --network $(NETWORK) postgres:alpine \
sh -c 'until pg_isready -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -d $(DB_NAME); do echo "waiting for postgres..."; sleep 1; done'
# Default target
.DEFAULT_GOAL := start
# --- Daftar phony targets ---
.PHONY: start build test lint gen \
db-up wait-db \
migration-% migrate-up migrate-down migrate-fresh \
seed \
docker-local docker-down docker-nuke docker-cache psql
# --- Go workflow ---
start:
@go run cmd/api/main.go
build:
@go build -o tmp/app ./cmd/api
test:
@go test ./test/...
lint:
@golangci-lint run
# --- Compose / DB helpers ---
db-up:
@$(COMPOSE) up -d postgresdb
wait-db:
@$(WAIT_DB)
# --- Migration (pembuatan file) ---
# Contoh: make migration-create_users_table
# ":" akan diubah ke "_" (biar aman untuk nama file)
migration-%:
@migrate create -ext sql -dir $(MIGRATIONS_DIR) $(subst :,_,$*)
# --- Migration (apply via docker image 'migrate') ---
migrate-up: db-up wait-db
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
# Contoh:
# make migrate-down step=2 → rollback 2 step
# make migrate-down → rollback semua
migrate-down: db-up wait-db
@if [ -n "$(step)" ]; then \
echo "⬇️ Migrating down $(step) step(s)..."; \
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down $(step); \
else \
echo "⬇️ Migrating down ALL steps..."; \
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all; \
fi
migrate-fresh: migrate-down migrate-up
@true
# Pakai: make migrate-force v=20250917120000
migrate-force:
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" force $(v)
# --- Seeder ---
seed: db-up wait-db
@$(COMPOSE) run --rm app go run cmd/seed/main.go
# --- Docker orchestration convenience ---
# --- Docker ---
docker-local:
@echo "🚀 Starting $(APP_NAME) with local PostgreSQL & Redis..."
@$(COMPOSE) up --build -d
docker-down:
@$(COMPOSE) down --remove-orphans
# ⚠️ Akan menghapus container, images dan volumes.
docker-nuke:
@echo "💣 Removing all containers, images, and volumes..."
@$(COMPOSE) down --rmi all --volumes --remove-orphans
docker-cache:
@docker builder prune -f
# --- Database / Migration ---
# --- PSQL shell ke DB di container ---
psql: db-up
@$(COMPOSE) exec -it postgresdb psql -U $(DB_USER) -d $(DB_NAME)
wait-db:
@echo "⏳ Waiting for database lti-postgres to be ready (inside Docker network)..."
@$(COMPOSE) run --rm app sh -c 'until nc -z lti-postgres 5432; do echo "Waiting for DB..."; sleep 2; done; echo "✅ Database is ready!"'
# Single feature
# example: make gen feat=product-category
migrate-up: wait-db
@echo "⬆️ Running migrations..."
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
# Sub feature
# make gen feat=master/area
gen:
@go run tools/gen.go $(feat)
# @goimports -w internal
migrate-down: wait-db
@echo "⬇️ Rolling back all migrations..."
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all
seed:
@echo "🌱 Running seed script..."
@$(COMPOSE) run --rm app go run cmd/seed/main.go
psql:
@docker exec -it lti-postgres psql -U $(DB_USER) -d $(DB_NAME)
logs:
@$(COMPOSE) logs -f app
restart:
@$(COMPOSE) restart
status:
@$(COMPOSE) ps
+41
View File
@@ -9,10 +9,13 @@ import (
"syscall"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/cache"
"gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/database"
"gitlab.com/mbugroup/lti-api.git/internal/middleware"
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
"gitlab.com/mbugroup/lti-api.git/internal/route"
"gitlab.com/mbugroup/lti-api.git/internal/sso"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
@@ -33,6 +36,7 @@ func main() {
defer closeDatabase(db)
rdb := setupRedis()
defer rdb.Close()
setupSSO(ctx, rdb)
setupRoutes(app, db, rdb)
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
@@ -52,10 +56,47 @@ func setupRedis() *redis.Client {
if err := rdb.Ping(context.Background()).Err(); err != nil {
utils.Log.Fatalf("Redis ping failed: %v", err)
}
cache.SetRedis(rdb)
utils.Log.Infof("Redis connected: %s", config.RedisURL)
return rdb
}
func setupSSO(ctx context.Context, rdb *redis.Client) {
const (
maxAttempts = 12
retryDelay = 5 * time.Second
)
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil {
lastErr = err
utils.Log.WithError(err).Warnf("SSO initialization attempt %d/%d failed", attempt, maxAttempts)
select {
case <-ctx.Done():
utils.Log.Fatalf("SSO initialization aborted: %v", ctx.Err())
case <-time.After(retryDelay):
}
continue
}
lastErr = nil
if attempt > 1 {
utils.Log.Infof("SSO initialization succeeded after %d attempts", attempt)
}
break
}
if lastErr != nil {
utils.Log.Fatalf("SSO initialization failed: %v", lastErr)
}
if rdb != nil {
session.SetRevocationStore(session.NewRevocationStore(rdb, config.SSOTokenBlacklistPrefix))
} else {
session.SetRevocationStore(nil)
}
}
func setupFiberApp() *fiber.App {
app := fiber.New(config.FiberConfig())
-2
View File
@@ -41,8 +41,6 @@ services:
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
+77
View File
@@ -0,0 +1,77 @@
version: "3.9"
services:
dev-lti-api:
container_name: dev-lti-api
build:
context: .
dockerfile: Dockerfile.local
image: dev-lti-api:latest
working_dir: /lti-api
command: air -c .air.toml
ports:
- "8081:8081"
env_file:
- .env.lti-api
environment:
# override agar koneksi ke container internal
DB_HOST: dev-lti-postgres
DB_PORT: 5432
REDIS_URL: redis://dev-lti-redis:6379/0
volumes:
- .:/lti-api
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
depends_on:
- dev-lti-postgres
- dev-lti-redis
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
dev-lti-postgres:
image: postgres:15-alpine
container_name: dev-lti-postgres
restart: always
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
ports:
- "5433:5432"
volumes:
- dev-lti-postgres-data:/var/lib/postgresql/data
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
dev-lti-redis:
image: redis:7-alpine
container_name: dev-lti-redis
restart: always
ports:
- "6380:6379"
networks:
- lti-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
networks:
lti-network:
driver: bridge
volumes:
dev-lti-postgres-data:
+5 -1
View File
@@ -3,12 +3,14 @@ module gitlab.com/mbugroup/lti-api.git
go 1.23
require (
github.com/MicahParks/keyfunc/v2 v2.1.0
github.com/bytedance/sonic v1.12.1
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.27.0
github.com/gofiber/contrib/jwt v1.0.10
github.com/gofiber/fiber/v2 v2.52.5
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/jackc/pgconn v1.14.1
github.com/redis/go-redis/v9 v9.14.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0
@@ -18,7 +20,6 @@ require (
)
require (
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -34,7 +35,10 @@ require (
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/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
+104
View File
@@ -17,6 +17,10 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -43,6 +47,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofiber/contrib/jwt v1.0.10 h1:/ilGepl6i0Bntl0Zcd+lAzagY8BiS1+fEiAj32HMApk=
github.com/gofiber/contrib/jwt v1.0.10/go.mod h1:1qBENE6sZ6PPT4xIpBzx1VxeyROQO7sj48OlM1I9qdU=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
@@ -57,12 +62,47 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4=
github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -75,16 +115,28 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -96,6 +148,7 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -109,10 +162,17 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -126,10 +186,15 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -150,24 +215,41 @@ github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRV
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -175,7 +257,14 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -184,28 +273,43 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+38
View File
@@ -0,0 +1,38 @@
package cache
import (
"errors"
"sync"
"github.com/redis/go-redis/v9"
)
var (
redisClient *redis.Client
mu sync.RWMutex
)
// SetRedis assigns the global redis client used across the application.
func SetRedis(client *redis.Client) {
mu.Lock()
defer mu.Unlock()
redisClient = client
}
// Redis returns the configured redis client. It may be nil if not yet initialised.
func Redis() *redis.Client {
mu.RLock()
defer mu.RUnlock()
return redisClient
}
// MustRedis returns the redis client or panics if it has not been set.
func MustRedis() *redis.Client {
mu.RLock()
client := redisClient
mu.RUnlock()
if client == nil {
panic(errors.New("redis client not initialised"))
}
return client
}
+44
View File
@@ -0,0 +1,44 @@
package capabilities
import (
"strings"
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
)
// FromPermissions returns a filtered map of capabilities that the frontend can use
// to toggle features. Only permissions recognized by the application are exposed.
func FromPermissions(perms []string) map[string]bool {
if len(perms) == 0 {
return nil
}
out := make(map[string]bool)
for _, perm := range perms {
if key, ok := normalizeAndAllow(perm); ok {
out[key] = true
}
}
if len(out) == 0 {
return nil
}
return out
}
func normalizeAndAllow(perm string) (string, bool) {
perm = strings.ToLower(strings.TrimSpace(perm))
if perm == "" {
return "", false
}
if _, ok := allowed[perm]; !ok {
return "", false
}
return perm, true
}
var allowed = map[string]struct{}{
recordings.PermissionRecordingRead: {},
recordings.PermissionRecordingCreate: {},
recordings.PermissionRecordingUpdate: {},
recordings.PermissionRecordingDelete: {},
}
@@ -11,6 +11,7 @@ type BaseRepository[T any] interface {
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error)
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*T, error)
GetByIDs(ctx context.Context, ids []uint, modifier func(*gorm.DB) *gorm.DB) ([]T, error)
First(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) (*T, error)
CreateOne(ctx context.Context, entity *T, modifier func(*gorm.DB) *gorm.DB) error
CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
@@ -96,6 +97,21 @@ func (r *BaseRepositoryImpl[T]) GetByIDs(
return entities, nil
}
func (r *BaseRepositoryImpl[T]) First(
ctx context.Context,
modifier func(*gorm.DB) *gorm.DB,
) (*T, error) {
entity := new(T)
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
if err := q.First(entity).Error; err != nil {
return nil, err
}
return entity, nil
}
// ---- CREATE ----
func (r *BaseRepositoryImpl[T]) CreateOne(
ctx context.Context,
+142
View File
@@ -2,13 +2,25 @@ package config
import (
"encoding/json"
"fmt"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/spf13/viper"
)
type SSOClientConfig struct {
PublicID string `json:"public_id"`
RedirectURI string `json:"redirect_uri"`
Scope string `json:"scope"`
// Prompt string `json:"prompt"`
DefaultReturnURI string `json:"default_return_uri"`
AllowedReturnOrigins []string `json:"allowed_return_origins"`
SyncSecret string `json:"sync_secret"`
}
var (
IsProd bool
AppHost string
@@ -20,6 +32,10 @@ var (
DBPassword string
DBName string
DBPort int
DBSSLMode string
DBSSLRootCert string
DBSSLCert string
DBSSLKey string
JWTSecret string
JWTAccessExp int
JWTRefreshExp int
@@ -32,6 +48,23 @@ var (
CORSExposeHeaders []string
CORSAllowCredentials bool
CORSMaxAge int
SSOIssuer string
SSOJWKSURL string
SSOAllowedAudiences []string
SSOAuthorizeURL string
SSOTokenURL string
SSOGetMeURL string
SSOClients map[string]SSOClientConfig
SSOAccessCookieName string
SSORefreshCookieName string
SSOCookieDomain string
SSOCookieSecure bool
SSOCookieSameSite string
SSOTokenBlacklistPrefix string
SSOPKCETTL time.Duration
SSOUserSyncDrift time.Duration
SSOUserSyncNonceTTL time.Duration
SSOUserSyncMaxBodyBytes int
)
func init() {
@@ -50,6 +83,10 @@ func init() {
DBPassword = viper.GetString("DB_PASSWORD")
DBName = viper.GetString("DB_NAME")
DBPort = viper.GetInt("DB_PORT")
DBSSLMode = defaultString(viper.GetString("DB_SSLMODE"), "disable")
DBSSLRootCert = strings.TrimSpace(viper.GetString("DB_SSLROOTCERT"))
DBSSLCert = strings.TrimSpace(viper.GetString("DB_SSLCERT"))
DBSSLKey = strings.TrimSpace(viper.GetString("DB_SSLKEY"))
// jwt configuration
JWTSecret = viper.GetString("JWT_SECRET")
@@ -68,6 +105,44 @@ func init() {
// Redis
RedisURL = viper.GetString("REDIS_URL")
// SSO integration
SSOIssuer = viper.GetString("SSO_ISSUER")
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
SSOAllowedAudiences = parseList("SSO_ALLOWED_AUDIENCES")
SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL")
SSOTokenURL = viper.GetString("SSO_TOKEN_URL")
SSOGetMeURL = viper.GetString("SSO_GETME_URL")
SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access")
SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh")
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax")
SSOTokenBlacklistPrefix = defaultString(viper.GetString("SSO_TOKEN_BLACKLIST_PREFIX"), "sso:blacklist")
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
SSOPKCETTL = time.Duration(ttl) * time.Second
} else {
SSOPKCETTL = 5 * time.Minute
}
SSOClients = loadSSOClients("SSO_CLIENTS")
if drift := viper.GetInt("SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS"); drift > 0 {
SSOUserSyncDrift = time.Duration(drift) * time.Second
} else {
SSOUserSyncDrift = 2 * time.Minute
}
if ttl := viper.GetInt("SSO_USER_SYNC_NONCE_TTL_SECONDS"); ttl > 0 {
SSOUserSyncNonceTTL = time.Duration(ttl) * time.Second
} else {
SSOUserSyncNonceTTL = 10 * time.Minute
}
SSOUserSyncMaxBodyBytes = viper.GetInt("SSO_USER_SYNC_MAX_BODY_BYTES")
if SSOUserSyncMaxBodyBytes <= 0 {
SSOUserSyncMaxBodyBytes = 32 * 1024
}
if IsProd {
ensureProdConfig()
}
}
func loadConfig() {
@@ -117,3 +192,70 @@ func parseListWithDefault(key, def string) []string {
}
return parts
}
func loadSSOClients(key string) map[string]SSOClientConfig {
clients := make(map[string]SSOClientConfig)
raw := strings.TrimSpace(viper.GetString(key))
if raw == "" {
return clients
}
if err := json.Unmarshal([]byte(raw), &clients); err != nil {
utils.Log.Errorf("Failed to parse %s: %v", key, err)
return make(map[string]SSOClientConfig)
}
result := make(map[string]SSOClientConfig, len(clients))
for alias, cfg := range clients {
alias = strings.ToLower(strings.TrimSpace(alias))
for i, origin := range cfg.AllowedReturnOrigins {
cfg.AllowedReturnOrigins[i] = strings.TrimSpace(origin)
}
cfg.SyncSecret = strings.TrimSpace(cfg.SyncSecret)
result[alias] = cfg
}
return result
}
func defaultString(v, def string) string {
if strings.TrimSpace(v) == "" {
return def
}
return v
}
func ensureProdConfig() {
if SSOAuthorizeURL == "" || !strings.HasPrefix(SSOAuthorizeURL, "https://") {
panic("SSO_AUTHORIZE_URL must be https in production")
}
if SSOTokenURL == "" || !strings.HasPrefix(SSOTokenURL, "https://") {
panic("SSO_TOKEN_URL must be https in production")
}
if SSOGetMeURL == "" || !strings.HasPrefix(SSOGetMeURL, "https://") {
panic("SSO_GETME_URL must be https in production")
}
if !SSOCookieSecure {
panic("SSO_COOKIE_SECURE must be true in production")
}
if SSOCookieDomain == "" {
panic("SSO_COOKIE_DOMAIN must be configured in production")
}
if len(SSOAllowedAudiences) == 0 {
panic("SSO_ALLOWED_AUDIENCES must contain at least one audience in production")
}
for alias, cfg := range SSOClients {
if strings.TrimSpace(cfg.SyncSecret) == "" {
panic(fmt.Sprintf("SSO_CLIENTS[%s].sync_secret must be configured in production", alias))
}
if len(cfg.SyncSecret) < 16 {
panic(fmt.Sprintf("SSO_CLIENTS[%s].sync_secret must be at least 16 characters", alias))
}
}
if SSOUserSyncDrift <= 0 {
panic("SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS must be greater than zero in production")
}
if SSOUserSyncNonceTTL <= 0 {
panic("SSO_USER_SYNC_NONCE_TTL_SECONDS must be greater than zero in production")
}
if SSOUserSyncMaxBodyBytes <= 0 {
panic("SSO_USER_SYNC_MAX_BODY_BYTES must be greater than zero in production")
}
}
+20 -4
View File
@@ -2,6 +2,7 @@ package database
import (
"fmt"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/config"
@@ -13,10 +14,25 @@ import (
)
func Connect(dbHost, dbName string) *gorm.DB {
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
dbHost, config.DBUser, config.DBPassword, dbName, config.DBPort,
)
parts := []string{
fmt.Sprintf("host=%s", dbHost),
fmt.Sprintf("user=%s", config.DBUser),
fmt.Sprintf("password=%s", config.DBPassword),
fmt.Sprintf("dbname=%s", dbName),
fmt.Sprintf("port=%d", config.DBPort),
fmt.Sprintf("sslmode=%s", config.DBSSLMode),
"TimeZone=Asia/Shanghai",
}
if config.DBSSLRootCert != "" {
parts = append(parts, fmt.Sprintf("sslrootcert=%s", config.DBSSLRootCert))
}
if config.DBSSLCert != "" {
parts = append(parts, fmt.Sprintf("sslcert=%s", config.DBSSLCert))
}
if config.DBSSLKey != "" {
parts = append(parts, fmt.Sprintf("sslkey=%s", config.DBSSLKey))
}
dsn := strings.Join(parts, " ")
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
@@ -9,13 +9,9 @@ CREATE TABLE users (
deleted_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user)
WHERE
deleted_at IS NULL;
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX users_email_unique ON users (email)
WHERE
deleted_at IS NULL;
CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL;
-- FLAGS
CREATE TABLE flags (
@@ -0,0 +1,2 @@
ALTER TABLE users
DROP CONSTRAINT IF EXISTS users_id_user_key;
@@ -0,0 +1,2 @@
ALTER TABLE users
ADD CONSTRAINT users_id_user_key UNIQUE (id_user);
@@ -0,0 +1,36 @@
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);
@@ -0,0 +1,36 @@
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
initial_quantity NUMERIC(15, 3) NOT NULL,
current_quantity NUMERIC(15, 3) NOT NULL,
reserved_quantity NUMERIC(15, 3),
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_chickin_details (
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
@@ -1,30 +1,22 @@
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE kandangs
DROP COLUMN IF EXISTS project_flock_id;
-- Only alter if tables exist
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
ALTER TABLE project_chickins
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_chickins
ALTER TABLE project_chickins
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
ALTER TABLE project_flock_populations
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_flock_populations
ALTER TABLE project_flock_populations
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
END $$;
@@ -1 +0,0 @@
DROP TABLE IF EXISTS laying_transfers CASCADE;
@@ -1,52 +0,0 @@
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);
@@ -1,58 +0,0 @@
-- ============================================
-- 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);
@@ -1,62 +0,0 @@
-- ============================================
-- 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;
@@ -1,5 +0,0 @@
-- 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;
@@ -1,93 +0,0 @@
-- 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);
+8 -10
View File
@@ -570,16 +570,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagDOC},
},
{
Name: "Ayam Pullet",
Brand: "MBU Pullet",
Sku: "PLT0001",
Uom: "Ekor",
Category: "Pullet",
Price: 15000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPullet},
},
{
Name: "Ayam Afkir",
Brand: "-",
@@ -587,6 +577,8 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Mati",
@@ -595,6 +587,8 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Culling",
@@ -603,6 +597,8 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Telur Konsumsi Baik",
@@ -611,6 +607,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "Telur Pecah",
@@ -619,6 +616,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "281 SPECIAL STARTER",
@@ -1,22 +0,0 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingKandangTransfer struct {
Id uint `gorm:"primaryKey"`
KandangId uint
ProductWarehouseId uint
Qty float64 `gorm:"type:numeric(15,3)"`
LayingTransferId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
}
-29
View File
@@ -1,29 +0,0 @@
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:"-"`
}
@@ -1,23 +0,0 @@
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"`
}
@@ -1,24 +0,0 @@
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"`
}
+4 -7
View File
@@ -12,16 +12,13 @@ type ProjectChickin struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
ChickInDate time.Time `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"`
UsageQty float64 `gorm:"type:numeric(15,3);not null"`
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"`
Notes string `gorm:"type:text"`
Quantity float64 `gorm:"not null"`
Note string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -8,17 +8,16 @@ import (
type ProjectFlockPopulation struct {
Id uint `gorm:"primaryKey"`
ProjectChickinId uint `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"`
TotalQty float64 `gorm:"type:numeric(15,3);not null"`
TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
-1
View File
@@ -28,4 +28,3 @@ type ProjectFlock struct {
LatestApproval *Approval `gorm:"-" json:"-"`
}
+2 -2
View File
@@ -8,8 +8,8 @@ type ProjectFlockKandang struct {
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+177 -85
View File
@@ -1,101 +1,193 @@
package middleware
// import (
// "strings"
import (
"strings"
// "gitlab.com/mbugroup/lti-api.git/internal/config"
// service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
// "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/config"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/sso"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
// "github.com/gofiber/fiber/v2"
// )
"github.com/gofiber/fiber/v2"
)
// func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
// return func(c *fiber.Ctx) error {
// authHeader := c.Get("Authorization")
// token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
const (
authContextLocalsKey = "auth.context"
authUserLocalsKey = "auth.user"
)
// if token == "" {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// AuthContext keeps authentication details captured by the middleware.
type AuthContext struct {
Token string
Verification *sso.VerificationResult
User *entity.User
Roles []sso.Role
Permissions map[string]struct{}
}
// userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)
// if err != nil {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// Auth validates the incoming request against the central SSO access token and
// loads the corresponding local user. Optional scopes can be provided to enforce
// fine-grained authorization using the SSO access token scopes.
func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
token := bearerToken(c)
if token == "" {
token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName))
}
if token == "" {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// // Only end-user subjects are allowed by this middleware. Service tokens
// if verification.UserID == 0 {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
verification, err := sso.VerifyAccessToken(token)
if err != nil {
utils.Log.WithError(err).Warn("auth: token verification failed")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// // Fail-closed on revocation check errors for stricter security posture.
// if revoker := session.GetRevocationStore(); revoker != nil {
// if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
// revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
// if err != nil {
// utils.Log.WithError(err).Warn("failed to check token revocation")
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// if revoked {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// }
// }
if verification.UserID == 0 {
return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint")
}
// user, err := userService.GetBySSOUserID(c, verification.UserID)
// if err != nil || user == nil {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
if err := ensureNotRevoked(c, token, verification); err != nil {
return err
}
// if len(requiredRights) > 0 && verification.Claims != nil {
// if !hasAllScopes(verification.Claims.Scopes(), requiredRights) {
// return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
// }
// }
user, err := userService.GetBySSOUserID(c, verification.UserID)
if err != nil || user == nil {
utils.Log.WithError(err).Warn("auth: failed to resolve user from repository")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// c.Locals("user", user)
if len(requiredScopes) > 0 {
if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) {
return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
}
}
// // if len(requiredRights) > 0 {
// // userRights, hasRights := config.RoleRights[user.Role]
// // if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID {
// // return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource")
// // }
// // }
var roles []sso.Role
permissions := make(map[string]struct{})
if verification.UserID != 0 {
if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil {
utils.Log.WithError(err).Warn("auth: failed to fetch sso profile")
} else if profile != nil {
roles = profile.Roles
for _, perm := range profile.PermissionNames() {
if perm != "" {
permissions[perm] = struct{}{}
}
}
}
}
// return c.Next()
// }
// }
ctx := &AuthContext{
Token: token,
Verification: verification,
User: user,
Roles: roles,
Permissions: permissions,
}
// // bearerToken extracts a Bearer token from the Authorization header using
// // case-insensitive scheme matching and tolerant whitespace handling.
// func bearerToken(c *fiber.Ctx) string {
// parts := strings.Fields(c.Get("Authorization"))
// if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
// return strings.TrimSpace(parts[1])
// }
// return ""
// }
c.Locals(authContextLocalsKey, ctx)
c.Locals(authUserLocalsKey, user)
// func hasAllScopes(have, required []string) bool {
// if len(required) == 0 {
// return true
// }
// set := make(map[string]struct{}, len(have))
// for _, s := range have {
// s = strings.ToLower(strings.TrimSpace(s))
// if s != "" {
// set[s] = struct{}{}
// }
// }
// for _, r := range required {
// r = strings.ToLower(strings.TrimSpace(r))
// if r == "" {
// continue
// }
// if _, ok := set[r]; !ok {
// return false
// }
// }
// return true
// }
return c.Next()
}
}
// AuthenticatedUser returns the authenticated user populated by Auth.
func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
value := c.Locals(authUserLocalsKey)
if user, ok := value.(*entity.User); ok && user != nil {
return user, true
}
return nil, false
}
// AuthDetails returns the full authentication context (token, claims, user).
func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) {
value := c.Locals(authContextLocalsKey)
if ctx, ok := value.(*AuthContext); ok && ctx != nil {
return ctx, true
}
return nil, false
}
// ensureNotRevoked ensures the token is not revoked or superseded by a forced logout.
func ensureNotRevoked(c *fiber.Ctx, token string, verification *sso.VerificationResult) error {
revoker := session.GetRevocationStore()
if revoker == nil {
return nil
}
if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
if err != nil {
utils.Log.WithError(err).Warn("auth: token revocation check failed")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
if revoked {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
}
if verification.UserID == 0 {
return nil
}
logoutAt, err := revoker.UserLogoutTime(c.Context(), verification.UserID)
if err != nil {
utils.Log.WithError(err).Warn("auth: failed to load user logout marker")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
if logoutAt.IsZero() {
return nil
}
claims := verification.Claims
if claims == nil || claims.IssuedAt == nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
issuedAt := claims.IssuedAt.Time
// Treat tokens issued at or before the forced logout timestamp as invalid.
if !issuedAt.After(logoutAt) {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
return nil
}
// bearerToken extracts a Bearer token from the Authorization header using
// case-insensitive scheme matching and tolerant whitespace handling.
func bearerToken(c *fiber.Ctx) string {
parts := strings.Fields(c.Get("Authorization"))
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
return strings.TrimSpace(parts[1])
}
return ""
}
func hasAllScopes(have, required []string) bool {
if len(required) == 0 {
return true
}
set := make(map[string]struct{}, len(have))
for _, s := range have {
s = strings.ToLower(strings.TrimSpace(s))
if s != "" {
set[s] = struct{}{}
}
}
for _, r := range required {
r = strings.ToLower(strings.TrimSpace(r))
if r == "" {
continue
}
if _, ok := set[r]; !ok {
return false
}
}
return true
}
+21
View File
@@ -24,3 +24,24 @@ func LimiterConfig() fiber.Handler {
SkipSuccessfulRequests: true,
})
}
func NewLimiter(max int, expiration time.Duration) fiber.Handler {
if max <= 0 {
max = 10
}
if expiration <= 0 {
expiration = time.Minute
}
return limiter.New(limiter.Config{
Max: max,
Expiration: expiration,
LimitReached: func(c *fiber.Ctx) error {
return c.Status(fiber.StatusTooManyRequests).
JSON(response.Common{
Code: fiber.StatusTooManyRequests,
Status: "error",
Message: "Too many requests, please try again later",
})
},
})
}
+75
View File
@@ -0,0 +1,75 @@
package middleware
import (
"strings"
"github.com/gofiber/fiber/v2"
)
// RequirePermissions ensures the authenticated user possesses all specified permissions.
func RequirePermissions(perms ...string) fiber.Handler {
required := canonicalPermissions(perms)
return func(c *fiber.Ctx) error {
if len(required) == 0 {
return c.Next()
}
ctx, ok := AuthDetails(c)
if !ok || ctx == nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
userPerms := ctx.permissionSet()
if len(userPerms) == 0 {
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
}
for _, perm := range required {
if _, has := userPerms[perm]; !has {
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
}
}
return c.Next()
}
}
// HasPermission reports whether the current request context includes the given permission.
func HasPermission(c *fiber.Ctx, perm string) bool {
ctx, ok := AuthDetails(c)
if !ok || ctx == nil {
return false
}
perm = canonicalPermission(perm)
if perm == "" {
return false
}
_, has := ctx.permissionSet()[perm]
return has
}
func (a *AuthContext) permissionSet() map[string]struct{} {
if a == nil || a.Permissions == nil {
return nil
}
return a.Permissions
}
func canonicalPermissions(perms []string) []string {
out := make([]string, 0, len(perms))
seen := make(map[string]struct{}, len(perms))
for _, perm := range perms {
if canonical := canonicalPermission(perm); canonical != "" {
if _, ok := seen[canonical]; ok {
continue
}
seen[canonical] = struct{}{}
out = append(out, canonical)
}
}
return out
}
func canonicalPermission(perm string) string {
return strings.ToLower(strings.TrimSpace(perm))
}
+4
View File
@@ -16,6 +16,10 @@ func JSONBody() fiber.Handler {
return c.Next()
}
if strings.EqualFold(c.Path(), "/api/sso/users/sync") {
return c.Next()
}
body := c.Body()
if len(body) == 0 {
return c.Next()
@@ -82,10 +82,6 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"LOKASI",
"KANDANG",
},
"stock_log": map[string][]string{
"log_types": []string{"TRANSFER", "ADJUSTMENT"},
"transaction_types": []string{"INCREASE", "DECREASE"},
},
"supplier_categories": []string{
"BOP",
"SAPRONAK",
@@ -23,4 +23,3 @@ func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, v
ProductWarehouseRoutes(router, userService, productWarehouseService)
}
@@ -19,8 +19,6 @@ type ProductWarehouseRepository interface {
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
}
@@ -80,14 +78,14 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
err := r.DB().WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC")
// preload relations so nested Product and Warehouse are populated
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
if err != nil {
return nil, err
}
@@ -102,12 +100,12 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c
}
fmt.Println(warehouseId)
err := query.WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Table("product_warehouses").
Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC").
Preload("Product").Preload("Warehouse").
First(&productWarehouse).Error
if err != nil {
return nil, err
@@ -148,42 +146,3 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
}
return 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
}
@@ -1,7 +1,7 @@
package productWarehouses
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/controllers"
productWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func ProductWarehouseRoutes(v1 fiber.Router, u user.UserService, s productWareho
ctrl := controller.NewProductWarehouseController(s)
route := v1.Group("/product-warehouses")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
+1 -1
View File
@@ -7,8 +7,8 @@ import (
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
// MODULE IMPORTS
)
@@ -1,7 +1,7 @@
package transfers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/controllers"
transfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
ctrl := controller.NewTransferController(s)
route := v1.Group("/transfers")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
-1
View File
@@ -23,4 +23,3 @@ func (AreaModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *val
AreaRoutes(router, userService, areaService)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package areas
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/controllers"
area "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func AreaRoutes(v1 fiber.Router, u user.UserService, s area.AreaService) {
ctrl := controller.NewAreaController(s)
route := v1.Group("/areas")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
-1
View File
@@ -23,4 +23,3 @@ func (BankModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *val
BankRoutes(router, userService, bankService)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package banks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/controllers"
bank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func BankRoutes(v1 fiber.Router, u user.UserService, s bank.BankService) {
ctrl := controller.NewBankController(s)
route := v1.Group("/banks")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -23,4 +23,3 @@ func (CustomerModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
CustomerRoutes(router, userService, customerService)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package customers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/controllers"
customer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func CustomerRoutes(v1 fiber.Router, u user.UserService, s customer.CustomerServ
ctrl := controller.NewCustomerController(s)
route := v1.Group("/customers")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
+2 -7
View File
@@ -1,7 +1,7 @@
package fcrs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/controllers"
fcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func FcrRoutes(v1 fiber.Router, u user.UserService, s fcr.FcrService) {
ctrl := controller.NewFcrController(s)
route := v1.Group("/fcrs")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
+2 -7
View File
@@ -1,7 +1,7 @@
package flocks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/controllers"
flock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func FlockRoutes(v1 fiber.Router, u user.UserService, s flock.FlockService) {
ctrl := controller.NewFlockController(s)
route := v1.Group("/flocks")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -23,4 +23,3 @@ func (KandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
KandangRoutes(router, userService, kandangService)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package kandangs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService
ctrl := controller.NewKandangController(s)
route := v1.Group("/kandangs")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -23,4 +23,3 @@ func (LocationModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
LocationRoutes(router, userService, locationService)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package locations
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/controllers"
location "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func LocationRoutes(v1 fiber.Router, u user.UserService, s location.LocationServ
ctrl := controller.NewLocationController(s)
route := v1.Group("/locations")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -23,4 +23,3 @@ func (NonstockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
NonstockRoutes(router, userService, nonstockService)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package nonstocks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/controllers"
nonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func NonstockRoutes(v1 fiber.Router, u user.UserService, s nonstock.NonstockServ
ctrl := controller.NewNonstockController(s)
route := v1.Group("/nonstocks")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -1,7 +1,7 @@
package productcategories
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/controllers"
productCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func ProductCategoryRoutes(v1 fiber.Router, u user.UserService, s productCategor
ctrl := controller.NewProductCategoryController(s)
route := v1.Group("/product-categories")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -23,4 +23,3 @@ func (ProductModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
ProductRoutes(router, userService, productService)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package products
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/controllers"
product "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func ProductRoutes(v1 fiber.Router, u user.UserService, s product.ProductService
ctrl := controller.NewProductController(s)
route := v1.Group("/products")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
+1 -1
View File
@@ -11,6 +11,7 @@ import (
banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks"
customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers"
fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs"
flocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks"
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks"
@@ -19,7 +20,6 @@ import (
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
flocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks"
// MODULE IMPORTS
)
@@ -23,4 +23,3 @@ func (SupplierModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
SupplierRoutes(router, userService, supplierService)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package suppliers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/controllers"
supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func SupplierRoutes(v1 fiber.Router, u user.UserService, s supplier.SupplierServ
ctrl := controller.NewSupplierController(s)
route := v1.Group("/suppliers")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
-1
View File
@@ -23,4 +23,3 @@ func (UomModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *vali
UomRoutes(router, userService, uomService)
}
+2 -7
View File
@@ -1,7 +1,7 @@
package uoms
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/controllers"
uom "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func UomRoutes(v1 fiber.Router, u user.UserService, s uom.UomService) {
ctrl := controller.NewUomController(s)
route := v1.Group("/uoms")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -23,4 +23,3 @@ func (WarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
WarehouseRoutes(router, userService, warehouseService)
}
@@ -16,7 +16,6 @@ type WarehouseRepository interface {
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error)
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
}
type WarehouseRepositoryImpl struct {
@@ -61,16 +60,3 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
}
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
}
+2 -7
View File
@@ -1,7 +1,7 @@
package warehouses
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/controllers"
warehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func WarehouseRoutes(v1 fiber.Router, u user.UserService, s warehouse.WarehouseS
ctrl := controller.NewWarehouseController(s)
route := v1.Group("/warehouses")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -1,6 +1,7 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
@@ -21,84 +22,30 @@ func NewChickinController(chickinService service.ChickinService) *ChickinControl
}
}
// func (u *ChickinController) GetAll(c *fiber.Ctx) error {
// query := &validation.Query{
// Page: c.QueryInt("page", 1),
// Limit: c.QueryInt("limit", 10),
// ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
// }
// 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")
func (u *ChickinController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
}
results, err := u.ChickinService.CreateOne(c, req)
result, totalResults, err := u.ChickinService.GetAll(c, query)
if err != nil {
return err
}
var (
data interface{}
message = "Create chickin successfully"
)
if len(results) == 1 {
data = dto.ToChickinListDTO(results[0])
} else {
message = "Create chickins successfully"
data = dto.ToChickinListDTOs(results)
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
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),
})
}
@@ -120,85 +67,95 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error {
Code: fiber.StatusOK,
Status: "success",
Message: "Get chickin successfully",
Data: dto.ToChickinDetailDTO(*result),
Data: dto.ToChickinListDTO(*result),
})
}
// func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
// req := new(validation.Update)
// param := c.Params("id")
// id, err := strconv.Atoi(param)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
// }
// if err := c.BodyParser(req); err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
// }
// result, err := u.ChickinService.UpdateOne(c, req, uint(id))
// if err != nil {
// return err
// }
// 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)
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")
}
results, err := u.ChickinService.Approval(c, req)
result, err := u.ChickinService.CreateOne(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit chickin approval successfully"
)
if len(results) == 1 {
data = dto.ToChickinListDTO(results[0])
} else {
message = "Submit chickin approvals successfully"
data = dto.ToChickinListDTOs(results)
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create chickin successfully",
Data: dto.ToChickinListDTO(*result),
})
}
func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ChickinService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
Message: "Update chickin successfully",
Data: dto.ToChickinListDTO(*result),
})
}
func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete chickin successfully",
})
}
func (u *ChickinController) Approve(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ChickinService.Approve(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Approve chickin successfully",
Data: nil,
})
}
@@ -17,12 +17,10 @@ import (
type ChickinBaseDTO struct {
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
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"`
Quantity float64 `json:"quantity"`
Note string `json:"note"`
}
type ProjectFlockDTO struct {
@@ -47,32 +45,21 @@ type ChickinSimpleDTO struct {
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"`
Quantity float64 `json:"quantity"`
Note string `json:"note"`
CreatedBy uint `json:"created_by"`
}
type ChickinListDTO struct {
ChickinBaseDTO
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ChickinDetailDTO struct {
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"`
ChickinListDTO
}
// === Mapper Functions (ordered) ===
@@ -151,22 +138,17 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
}
func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO {
var projectFlockKandangId uint
// Check if ProjectFlockKandang relation is loaded
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 {
projectFlockKandangId = e.ProjectFlockKandang.Id
} else if e.ProjectFlockKandangId != 0 {
// If relation is not loaded but ID is available, use the ID
projectFlockKandangId = e.ProjectFlockKandangId
var pfk *ProjectFlockKandangDTO
if e.ProjectFlockKandang.Id != 0 {
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
pfk = &mapped
}
return ChickinBaseDTO{
Id: e.Id,
ProjectFlockKandangId: projectFlockKandangId,
ProjectFlockKandang: pfk,
ChickInDate: e.ChickInDate,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
Quantity: e.Quantity,
Note: e.Note,
}
}
@@ -175,22 +157,26 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO {
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
ChickInDate: e.ChickInDate,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
Quantity: e.Quantity,
Note: e.Note,
CreatedBy: e.CreatedBy,
}
}
func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO {
var createdUser *userBaseDTO.UserBaseDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
if e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
var pfk *ProjectFlockKandangDTO
if e.ProjectFlockKandang.Id != 0 {
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
pfk = &mapped
}
return ChickinListDTO{
ChickinBaseDTO: ToChickinBaseDTO(e),
ProjectFlockKandang: pfk,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
@@ -214,31 +200,7 @@ func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO {
}
func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO {
var createdUser *userBaseDTO.UserBaseDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
return ChickinDetailDTO{
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,
ChickinListDTO: ToChickinListDTO(e),
}
}
func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO {
result := make([]ChickinDetailDTO, len(e))
for i, r := range e {
result[i] = ToChickinDetailDTO(r)
}
return result
}
@@ -1,15 +1,10 @@
package chickins
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
@@ -20,8 +15,6 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type ChickinModule struct{}
@@ -39,12 +32,6 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
}
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
@@ -11,26 +11,21 @@ import (
type ProjectChickinRepository interface {
repository.BaseRepository[entity.ProjectChickin]
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
}
type ChickinRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectChickin]
db *gorm.DB
}
func NewChickinRepository(db *gorm.DB) ProjectChickinRepository {
return &ChickinRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db),
db: db,
}
}
func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) {
var chickin entity.ProjectChickin
err := r.db.WithContext(ctx).
err := r.DB().WithContext(ctx).
Where("project_floc_id = ?", projectFlockID).
Where("deleted_at IS NULL").
First(&chickin).Error
@@ -39,43 +34,3 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
}
return &chickin, nil
}
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Order("created_at DESC").
Find(&chickins).Error
if err != nil {
return nil, err
}
return chickins, nil
}
func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where("usage_qty = 0").
Where("pending_usage_qty > 0").
Order("created_at DESC").
Find(&chickins).Error
if err != nil {
return nil, err
}
return chickins, nil
}
func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where("pending_usage_qty > 0").
Select("COALESCE(SUM(pending_usage_qty), 0)").
Row().Scan(&total)
if err != nil {
return 0, err
}
return total, nil
}
@@ -1,8 +1,6 @@
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"
@@ -10,10 +8,6 @@ import (
type ProjectChickinDetailRepository interface {
repository.BaseRepository[entity.ProjectChickinDetail]
CreateOne(ctx context.Context, entity *entity.ProjectChickinDetail, modifier func(*gorm.DB) *gorm.DB) error
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error)
WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository
}
type ChickinDetailRepositoryImpl struct {
@@ -25,22 +19,3 @@ func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository {
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
}
}
func (r *ChickinDetailRepositoryImpl) WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository {
return &ChickinDetailRepositoryImpl{BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](tx)}
}
func (r *ChickinDetailRepositoryImpl) DB() *gorm.DB {
return r.BaseRepositoryImpl.DB()
}
func (r *ChickinDetailRepositoryImpl) GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) {
var records []entity.ProjectChickinDetail
if err := r.DB().WithContext(ctx).Where("project_chickin_id = ?", projectChickinID).Find(&records).Error; err != nil {
return nil, err
}
if len(records) == 0 {
return nil, gorm.ErrRecordNotFound
}
return records, nil
}
+6 -11
View File
@@ -1,7 +1,7 @@
package chickins
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/controllers"
chickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,17 +13,12 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
ctrl := controller.NewChickinController(s)
route := v1.Group("/chickins")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
// route.Get("/", ctrl.GetAll)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
// route.Patch("/:id", ctrl.UpdateOne)
// route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Post("/:id/approve", ctrl.Approve)
}
@@ -2,11 +2,7 @@ package service
import (
"errors"
"fmt"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -25,10 +21,10 @@ import (
type ChickinService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
Approve(ctx *fiber.Ctx, id uint) error
}
type chickinService struct {
@@ -80,7 +76,6 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
}
offset := (params.Page - 1) * params.Limit
chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.ProjectFlockKandangId != 0 {
@@ -108,164 +103,112 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e
return chickin, nil
}
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) {
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return nil, err
}
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId)
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found")
s.Log.Errorf("Failed to get warehouse: %+v", err)
return nil, err
}
var productWarehouses []entity.ProductWarehouse
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
var productCategoryCode string
switch category {
case string(utils.ProjectFlockCategoryGrowing):
productCategoryCode = "DOC"
case string(utils.ProjectFlockCategoryLaying):
productCategoryCode = "PULLET"
default:
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category))
// move complex DB query into repository for cleaner service
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
if err != nil {
s.Log.Errorf("Failed to get product warehouses: %+v", err)
return nil, err
}
if len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
}
totalQuantity := 0.0
for _, pw := range productWarehouses {
totalQuantity += pw.Quantity
}
productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id)
if err != nil || len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category)))
if totalQuantity < 1 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
}
chickinDate, err := utils.ParseDateString(req.ChickInDate)
if err != nil {
s.Log.Errorf("Failed to parse chickin date: %+v", err)
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format")
}
actorID := uint(1) // todo nanti ambil dari auth context
newChikins := make([]*entity.ProjectChickin, 0)
for _, productWarehouse := range productWarehouses {
availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", productWarehouse.Id))
}
if availableQty <= 0 {
continue
}
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: req.ProjectFlockKandangId,
ProjectFlockKandangId: projectflockkandang.Id,
ChickInDate: chickinDate,
UsageQty: 0,
PendingUsageQty: availableQty,
ProductWarehouseId: productWarehouse.Id,
Notes: req.Note,
CreatedBy: actorID,
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
}
newChikins = append(newChikins, newChickin)
// Update semua product warehouse: set quantity jadi 0
for _, pw := range productWarehouses {
err = s.ProductWarehouseRepo.PatchOne(c.Context(), pw.Id, map[string]any{
"quantity": 0,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return nil, err
}
if len(newChikins) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create")
newChickinDetail := &entity.ProjectChickinDetail{
ProjectChickinId: newChickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: 1, // todo: ganti dengan user login
}
err = s.ProjectChickinDetailRepo.CreateOne(c.Context(), newChickinDetail, nil)
if err != nil {
s.Log.Errorf("Failed to create chickin detail: %+v", err)
return nil, err
}
}
existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins")
s.Log.Errorf("Failed to get project flock population: %+v", err)
return nil, err
}
if existingPopulation != nil {
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)
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), existingPopulation.Id, map[string]any{
"reserved_quantity": newChickin.Quantity + existingPopulation.ReservedQuantity,
}, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
s.Log.Errorf("Failed to update project flock population: %+v", err)
return nil, err
}
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
newPopulation := &entity.ProjectFlockPopulation{
ProjectFlockKandangId: req.ProjectFlockKandangId,
InitialQuantity: 0,
CurrentQuantity: 0,
ReservedQuantity: newChickin.Quantity,
CreatedBy: 1, // todo: ganti dengan user login
}
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
})
err = s.ProjectflockPopulationRepo.CreateOne(c.Context(), newPopulation, nil)
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
s.Log.Errorf("Failed to create project flock population: %+v", err)
return nil, err
}
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
return s.GetOne(c, newChickin.Id)
}
func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) {
@@ -279,7 +222,7 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["chick_in_date"] = req.ChickInDate
}
if req.Note != "" {
updateBody["notes"] = req.Note
updateBody["note"] = req.Note
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
@@ -297,336 +240,175 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
}
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
db := s.Repository.DB()
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
tx := db.WithContext(c.Context()).Begin()
if tx.Error != nil {
s.Log.Errorf("Failed to begin transaction: %+v", tx.Error)
return tx.Error
}
rollback := func(err error) error {
if rerr := tx.Rollback().Error; rerr != nil {
s.Log.Errorf("Rollback failed: %+v", rerr)
}
return err
}
chickinRepoTx := s.Repository.WithTx(tx)
pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx)
chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
}
if err != nil {
s.Log.Errorf("Failed get chickin by id: %+v", err)
return rollback(err)
}
var population entity.ProjectFlockPopulation
if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found"))
}
s.Log.Errorf("Failed to get project flock population: %+v", err)
return rollback(err)
}
newReserved := population.ReservedQuantity - chickin.Quantity
if newReserved < 0 {
newReserved = 0
}
if err := tx.WithContext(c.Context()).Model(&entity.ProjectFlockPopulation{}).Where("id = ?", population.Id).Updates(map[string]any{"reserved_quantity": newReserved}).Error; err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return rollback(err)
}
restoreFromDetails := func() (bool, error) {
var details []entity.ProjectChickinDetail
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
return false, err
}
if len(details) == 0 {
return false, nil
}
for _, d := range details {
var pw entity.ProductWarehouse
if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return false, err
}
updatedQuantity := pw.Quantity + d.Quantity
if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
return false, err
}
}
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil {
return false, err
}
return true, nil
}
restored, err := restoreFromDetails()
if err != nil {
s.Log.Errorf("Failed to restore from chickin details: %+v", err)
return rollback(err)
}
if !restored {
projectflockkandang, err := pfkRepoTx.GetByID(c.Context(), population.ProjectFlockKandangId)
if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return rollback(err)
}
var warehouse entity.Warehouse
if err := tx.WithContext(c.Context()).Where("kandang_id = ?", projectflockkandang.KandangId).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang"))
}
s.Log.Errorf("Failed to get warehouse: %+v", err)
return rollback(err)
}
productWarehouse, err := s.ProductWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(
c.Context(),
"DOC",
warehouse.Id,
tx,
)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse"))
}
s.Log.Errorf("Failed to get product warehouse: %+v", err)
return rollback(err)
}
updatedQuantity := productWarehouse.Quantity + chickin.Quantity
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return rollback(err)
}
}
// 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
}
func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) {
availableQty := productWarehouse.Quantity
if category == string(utils.ProjectFlockCategoryGrowing) {
var totalPendingQty float64
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 - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
} else if category == string(utils.ProjectFlockCategoryLaying) {
var totalPopulation float64
var totalPendingQty float64
populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id)
if err == nil {
for _, pop := range populations {
totalPopulation += pop.TotalQty
}
}
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err == nil {
for _, chickin := range chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
}
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
}
return availableQty, nil
}
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected):
action = entity.ApprovalActionRejected
case string(entity.ApprovalActionApproved):
action = entity.ApprovalActionApproved
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
if len(approvableIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
for _, id := range approvableIDs {
idCopy := id
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
return nil, err
}
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
}
if latestApproval == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
}
if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
}
}
step := utils.ProjectFlockKandangStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.ProjectFlockKandangStepDisetujui
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs {
actorID := uint(1) // todo nanti ambil dari auth context
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
}
if action == entity.ApprovalActionApproved {
chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID))
}
kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
}
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
if category == string(utils.ProjectFlockCategoryGrowing) {
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
}
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
}
} else if category == string(utils.ProjectFlockCategoryLaying) {
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
}
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
}
}
}
if action == entity.ApprovalActionRejected {
chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID))
}
if len(chickins) == 0 {
continue
}
kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
}
categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category))
for _, chickin := range chickins {
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId))
}
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id))
}
}
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id))
}
}
}
}
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
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...)
}
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 {
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
}
if populationExists {
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
continue
}
quantityToConvert := chickin.PendingUsageQty
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
"usage_qty": quantityToConvert,
"pending_usage_qty": 0,
}, nil); err != nil {
return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err)
}
if chickin.ProductWarehouseId != targetPW.Id {
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
"quantity": gorm.Expr("quantity - ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
}
return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err)
}
}
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
}
return fmt.Errorf("failed to update target warehouse quantity: %w", err)
}
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: targetPW.Id,
TotalQty: quantityToConvert,
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
}
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
return err
}
}
return nil
}
@@ -3,7 +3,7 @@ package validation
type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
Note string `json:"note" validate:"omitempty"`
Note string `json:"note" validate:"omitempty`
}
type Update struct {
@@ -16,9 +16,3 @@ type Query struct {
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
}
type Approve struct {
Action string `json:"action" validate:"required_strict"`
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
@@ -1,76 +0,0 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ProjectFlockKandangController struct {
ProjectFlockKandangService service.ProjectFlockKandangService
}
func NewProjectFlockKandangController(projectFlockKandangService service.ProjectFlockKandangService) *ProjectFlockKandangController {
return &ProjectFlockKandangController{
ProjectFlockKandangService: projectFlockKandangService,
}
}
func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all projectFlockKandangs successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProjectFlockKandangListDTOs(result),
})
}
func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, availableQtys, err := u.ProjectFlockKandangService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get projectFlockKandang successfully",
Data: dto.ToProjectFlockKandangListDTOWithAvailableQty(*result, availableQtys),
})
}
@@ -1,330 +0,0 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs (ordered) ===
type ProjectFlockKandangBaseDTO struct {
Id uint `json:"id"`
}
type ProjectFlockDTO struct {
Id uint `json:"id"`
Period int `json:"period"`
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type KandangDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
type ProductWarehouseDTO struct {
Id uint `json:"id"`
Product *productDTO.ProductBaseDTO `json:"product,omitempty"`
Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"`
}
type AvailableQtyDTO struct {
AvailableQty float64 `json:"available_qty"`
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
}
type ProjectFlockKandangListDTO struct {
ProjectFlockKandangBaseDTO
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
Kandang *KandangDTO `json:"kandang,omitempty"`
Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"`
AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
type ProjectFlockKandangDetailDTO struct {
ProjectFlockKandangListDTO
}
// === Mapper Functions (ordered) ===
func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO {
return ProjectFlockKandangBaseDTO{
Id: e.Id,
}
}
func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO {
if pf == nil {
return nil
}
return &ProjectFlockDTO{
Id: pf.Id,
Period: pf.Period,
Area: pf.Area,
Category: pf.Category,
Fcr: pf.Fcr,
Location: pf.Location,
CreatedUser: pf.CreatedUser,
CreatedAt: pf.CreatedAt,
UpdatedAt: pf.UpdatedAt,
}
}
func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO {
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
if e.ProjectFlock.Id != 0 {
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
projectFlockSummary = &mapped
}
return ProjectFlockKandangListDTO{
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangDTO(e.Kandang),
Chickins: toChickinDTOs(e.Chickins),
AvailableQtys: toAvailableQtyDTOsFromRaw(availableQtysRaw),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTO(e),
}
}
func toKandangDTO(kandang entity.Kandang) *KandangDTO {
if kandang.Id == 0 {
return nil
}
return &KandangDTO{
Id: kandang.Id,
Name: kandang.Name,
Status: kandang.Status,
}
}
func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO {
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
return &mapped
}
return nil
}
func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKandangListDTO {
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
if e.ProjectFlock.Id != 0 {
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
projectFlockSummary = &mapped
}
return ProjectFlockKandangListDTO{
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangDTO(e.Kandang),
Chickins: toChickinDTOs(e.Chickins),
AvailableQtys: toAvailableQtyDTOs(e.Chickins),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTO(e),
}
}
func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFlockKandangListDTO {
result := make([]ProjectFlockKandangListDTO, len(e))
for i, r := range e {
result[i] = ToProjectFlockKandangListDTO(r)
}
return result
}
func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDetailDTO {
return ProjectFlockKandangDetailDTO{
ProjectFlockKandangListDTO: ToProjectFlockKandangListDTO(e),
}
}
// === Helper Functions (ordered) ===
func toProductWarehouseDTO(pwData map[string]interface{}) *ProductWarehouseDTO {
if pwData == nil {
return nil
}
dto := &ProductWarehouseDTO{}
if id, ok := pwData["id"].(float64); ok {
dto.Id = uint(id)
} else if id, ok := pwData["id"].(uint); ok {
dto.Id = id
}
if pData, ok := pwData["product"].(map[string]interface{}); ok {
dto.Product = toProductDTO(pData)
}
if wData, ok := pwData["warehouse"].(map[string]interface{}); ok {
dto.Warehouse = toWarehouseDTO(wData)
}
return dto
}
func toProductDTO(pData map[string]interface{}) *productDTO.ProductBaseDTO {
if pData == nil {
return nil
}
product := &productDTO.ProductBaseDTO{}
if id, ok := pData["id"].(float64); ok {
product.Id = uint(id)
} else if id, ok := pData["id"].(uint); ok {
product.Id = id
}
if name, ok := pData["name"].(string); ok {
product.Name = name
}
return product
}
func toWarehouseDTO(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO {
if wData == nil {
return nil
}
warehouse := &warehouseDTO.WarehouseBaseDTO{}
if id, ok := wData["id"].(float64); ok {
warehouse.Id = uint(id)
} else if id, ok := wData["id"].(uint); ok {
warehouse.Id = id
}
if name, ok := wData["name"].(string); ok {
warehouse.Name = name
}
if wType, ok := wData["type"].(string); ok {
warehouse.Type = wType
}
return warehouse
}
func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO {
if pf.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(pf.CreatedUser)
return &mapped
} else if pf.CreatedBy != 0 {
return &userDTO.UserBaseDTO{
Id: pf.CreatedBy,
IdUser: int64(pf.CreatedBy),
}
}
return nil
}
func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO {
if len(chickins) == 0 {
return nil
}
result := make([]chickinDTO.ChickinBaseDTO, len(chickins))
for i, ch := range chickins {
result[i] = chickinDTO.ToChickinBaseDTO(ch)
}
return result
}
func toAvailableQtyDTOs(chickins []entity.ProjectChickin) []AvailableQtyDTO {
if len(chickins) == 0 {
return nil
}
availableQtyMap := make(map[uint]AvailableQtyDTO)
for _, ch := range chickins {
if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 {
continue
}
if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists {
continue
}
pwDTO := &ProductWarehouseDTO{
Id: ch.ProductWarehouse.Id,
}
if ch.ProductWarehouse.Product.Id != 0 {
pwDTO.Product = &productDTO.ProductBaseDTO{
Id: ch.ProductWarehouse.Product.Id,
Name: ch.ProductWarehouse.Product.Name,
}
}
if ch.ProductWarehouse.Warehouse.Id != 0 {
pwDTO.Warehouse = &warehouseDTO.WarehouseBaseDTO{
Id: ch.ProductWarehouse.Warehouse.Id,
Name: ch.ProductWarehouse.Warehouse.Name,
Type: ch.ProductWarehouse.Warehouse.Type,
}
}
availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{
ProductWarehouse: pwDTO,
}
}
if len(availableQtyMap) == 0 {
return nil
}
result := make([]AvailableQtyDTO, 0, len(availableQtyMap))
for _, v := range availableQtyMap {
result = append(result, v)
}
return result
}
func toAvailableQtyDTOsFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO {
if len(availableQtysRaw) == 0 {
return nil
}
result := make([]AvailableQtyDTO, len(availableQtysRaw))
for i, v := range availableQtysRaw {
pwData, ok := v["product_warehouse"].(map[string]interface{})
if !ok {
continue
}
pwDTO := toProductWarehouseDTO(pwData)
availableQty := 0.0
if qty, ok := v["available_qty"].(float64); ok {
availableQty = qty
}
result[i] = AvailableQtyDTO{
AvailableQty: availableQty,
ProductWarehouse: pwDTO,
}
}
return result
}
@@ -1,43 +0,0 @@
package project_flock_kandangs
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProjectFlockKandangModule struct{}
func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlockKandang.NewProjectFlockPopulationRepository(db)
userRepo := rUser.NewUserRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
// register workflow steps for project flock kandang approvals
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
}
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
}
@@ -1,26 +0,0 @@
package project_flock_kandangs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/controllers"
projectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlockKandang.ProjectFlockKandangService) {
ctrl := controller.NewProjectFlockKandangController(s)
route := v1.Group("/project-flock-kandangs")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
}
@@ -1,195 +0,0 @@
package service
import (
"errors"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ProjectFlockKandangService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error)
}
type projectFlockKandangService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
PopulationRepo repository.ProjectFlockPopulationRepository
}
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
return &projectFlockKandangService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
PopulationRepo: populationRepo,
}
}
func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
projectFlockKandangs, err := s.Repository.GetAll(c.Context())
if err != nil {
s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err)
return nil, 0, err
}
total := int64(len(projectFlockKandangs))
return projectFlockKandangs, total, nil
}
func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) {
projectFlockKandang, err := s.Repository.GetByID(c.Context(), id)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
if err != nil {
s.Log.Errorf("Failed get projectFlockKandang by id: %+v", err)
return nil, nil, err
}
if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil {
latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
if err != nil {
s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
}
if latest != nil {
projectFlockKandang.LatestApproval = latest
}
}
availableQtys, err := s.getAvailableQuantities(c, projectFlockKandang)
if err != nil {
s.Log.Errorf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandang.Kandang.Id, err)
availableQtys = nil
}
return projectFlockKandang, availableQtys, nil
}
func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) ([]map[string]interface{}, error) {
if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil {
return nil, nil
}
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.Kandang.Id)
if err != nil || warehouse == nil {
return nil, nil
}
var productCategoryCode string
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
productCategoryCode = "DOC"
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
productCategoryCode = "PULLET"
} else {
return nil, nil
}
products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id)
if err != nil || len(products) == 0 {
return nil, nil
}
var result []map[string]interface{}
for _, pw := range products {
availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
if err != nil {
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
}
// Only include product warehouse if available_qty > 0
if availableQty <= 0 {
continue
}
productData := map[string]interface{}{
"id": pw.Product.Id,
"name": pw.Product.Name,
}
warehouseData := map[string]interface{}{
"id": pw.Warehouse.Id,
"name": pw.Warehouse.Name,
"type": pw.Warehouse.Type,
}
productWarehouseData := map[string]interface{}{
"id": pw.Id,
"product": productData,
"warehouse": warehouseData,
}
result = append(result, map[string]interface{}{
"available_qty": availableQty,
"product_warehouse": productWarehouseData,
})
}
return result, nil
}
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
availableQty := productWarehouse.Quantity
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
var totalPendingQty float64
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
availableQty = productWarehouse.Quantity - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
var totalPopulation float64
var totalPendingQty float64
populations, err := s.PopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(c.Context(), projectFlockKandang.Id, productWarehouse.Id)
if err == nil {
for _, pop := range populations {
totalPopulation += pop.TotalQty
}
}
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
}
return availableQty, nil
}
@@ -1,17 +0,0 @@
package validation
type Create struct {
ProjectFlockId uint `json:"project_flock_id" validate:"required"`
KandangId uint `json:"kandang_id" validate:"required"`
}
type Update struct {
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -10,7 +10,6 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -23,13 +22,6 @@ type ProjectFlockBaseDTO struct {
FlockName string `json:"flock_name"`
}
type KandangWithProjectFlockIdDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
}
type ProjectFlockListDTO struct {
ProjectFlockBaseDTO
// Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
@@ -9,19 +9,8 @@ import (
)
type ProjectFlockPopulationRepository interface {
// domain-specific
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error)
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
// subset of base repository methods used by services
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
// transaction helpers
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
DB() *gorm.DB
repository.BaseRepository[entity.ProjectFlockPopulation]
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error)
}
type projectFlockPopulationRepositoryImpl struct {
@@ -34,60 +23,13 @@ func NewProjectFlockPopulationRepository(db *gorm.DB) ProjectFlockPopulationRepo
}
}
func (r *projectFlockPopulationRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockPopulationRepository {
return &projectFlockPopulationRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockPopulation](tx),
}
}
func (r *projectFlockPopulationRepositoryImpl) DB() *gorm.DB {
return r.BaseRepositoryImpl.DB()
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) {
var records []entity.ProjectFlockPopulation
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) {
var record entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx).
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Preload("ProjectChickin").
Find(&records).Error
Where("project_flock_kandang_id = ?", projectFlockKandangID).
First(&record).Error
if err != nil {
return nil, err
}
return records, nil
}
func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) {
var count int64
err := r.DB().WithContext(ctx).
Where("project_chickin_id = ?", projectChickinID).
Model(&entity.ProjectFlockPopulation{}).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) {
var records []entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx).
Where("project_chickin_id = ? AND product_warehouse_id = ?", projectChickinID, productWarehouseID).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) {
var records []entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx).
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ? AND project_flock_populations.product_warehouse_id = ?", projectFlockKandangID, productWarehouseID).
Find(&records).Error
if err != nil {
return nil, err
}
return records, nil
return &record, nil
}
@@ -4,7 +4,6 @@ import (
"context"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
@@ -20,7 +19,6 @@ type ProjectFlockKandangRepository interface {
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
IdExists(ctx context.Context, id uint) (bool, error)
DB() *gorm.DB
}
@@ -61,9 +59,6 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse").
Order("project_flock_id ASC, created_at ASC").
Find(&records).Error; err != nil {
return nil, err
@@ -78,9 +73,6 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand
func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB {
return r.db
}
func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id)
}
func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
record := new(entity.ProjectFlockKandang)
@@ -93,9 +85,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse").
First(record, id).Error; err != nil {
return nil, err
}
@@ -114,9 +103,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Preload("Chickins").
Preload("Chickins.CreatedUser").
Preload("Chickins.ProductWarehouse").
First(record).Error; err != nil {
return nil, err
}
@@ -1,7 +1,7 @@
package project_flocks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers"
projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
ctrl := controller.NewProjectflockController(s)
route := v1.Group("/project_flocks")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
@@ -10,6 +10,7 @@ import (
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"
authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -80,18 +81,6 @@ func NewProjectflockService(
}
}
func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Flock").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs").
Preload("KandangHistory").
Preload("KandangHistory.Kandang")
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
@@ -170,6 +159,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err
}
actorID, err := actorIDFromContext(c)
if err != nil {
return nil, err
}
cat := strings.ToUpper(req.Category)
if !utils.IsValidProjectFlockCategory(cat) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
@@ -194,7 +188,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
canonicalBase := baseName
if s.FlockRepo != nil {
baseFlock, err := s.ensureFlockByName(c.Context(), baseName)
baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
if err != nil {
return nil, err
}
@@ -224,7 +218,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
Category: cat,
FcrId: req.FcrId,
LocationId: req.LocationId,
CreatedBy: 1,
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -249,7 +243,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return err
}
actorID := uint(1) //TODO: Change From Auth
action := entity.ApprovalActionCreated
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
_, err = approvalSvcTx.CreateApproval(
@@ -283,6 +276,11 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, err
}
actorID, err := actorIDFromContext(c)
if err != nil {
return nil, err
}
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
@@ -305,7 +303,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
canonicalBase := trimmed
if s.FlockRepo != nil {
flockEntity, err := s.ensureFlockByName(c.Context(), trimmed)
flockEntity, err := s.ensureFlockByName(c.Context(), actorID, trimmed)
if err != nil {
return nil, err
}
@@ -464,7 +462,6 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
if hasChanges {
actorID := uint(1) //TODO: Change From Auth
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
if approvalSvc != nil {
latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
@@ -518,7 +515,11 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
return nil, err
}
actorID := uint(1) // TODO: change from auth context
actorID, err := actorIDFromContext(c)
if err != nil {
return nil, err
}
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected):
@@ -539,7 +540,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
step = utils.ProjectFlockStepAktif
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
@@ -826,7 +827,7 @@ func (s projectflockService) generateSequentialFlockName(ctx context.Context, re
}
}
func (s projectflockService) ensureFlockByName(ctx context.Context, name string) (*entity.Flock, error) {
func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint, name string) (*entity.Flock, error) {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
@@ -843,7 +844,7 @@ func (s projectflockService) ensureFlockByName(ctx context.Context, name string)
newFlock := &entity.Flock{
Name: trimmed,
CreatedBy: 1, // TODO: replace with authenticated user
CreatedBy: actorID,
}
if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
@@ -962,3 +963,11 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
}
return kandangRepository.NewKandangRepository(s.Repository.DB())
}
func actorIDFromContext(c *fiber.Ctx) (uint, error) {
user, ok := authmiddleware.AuthenticatedUser(c)
if !ok || user == nil || user.Id == 0 {
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
return user.Id, nil
}
@@ -0,0 +1,8 @@
package recordings
const (
PermissionRecordingRead = "recording.read"
PermissionRecordingCreate = "recording.write"
PermissionRecordingUpdate = "recording.update"
PermissionRecordingDelete = "recording.delete"
)
@@ -265,7 +265,7 @@ func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandang
if err != nil {
return 0, err
}
return int64(math.Round(population.TotalQty)), nil
return int64(math.Round(population.InitialQuantity)), nil
}
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
@@ -1,7 +1,7 @@
package recordings
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/controllers"
recording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,12 +13,7 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
ctrl := controller.NewRecordingController(s)
route := v1.Group("/recordings")
// 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.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Get("/next-day", ctrl.GetNextDay)
+1 -5
View File
@@ -10,8 +10,6 @@ import (
chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins"
projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks"
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings"
projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs"
// MODULE IMPORTS
)
@@ -22,10 +20,8 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
projectflocks.ProjectflockModule{},
recordings.RecordingModule{},
chickins.ChickinModule{},
transferLayings.TransferLayingModule{},
projectFlockKandangs.ProjectFlockKandangModule{},
// MODULE REGISTRY
}
}
for _, m := range allModules {
m.RegisterRoutes(group, db, validate)
@@ -1,182 +0,0 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransferLayingController struct {
TransferLayingService service.TransferLayingService
}
func NewTransferLayingController(transferLayingService service.TransferLayingService) *TransferLayingController {
return &TransferLayingController{
TransferLayingService: transferLayingService,
}
}
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)),
TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)),
TransferDateFrom: c.Query("transfer_date_from", ""),
TransferDateTo: c.Query("transfer_date_to", ""),
ApprovalStatus: c.Query("approval_status", ""),
TransferNumber: c.Query("transfer_number", ""),
Sort: c.Query("sort", "created_at"),
Order: c.Query("order", "desc"),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.TransferLayingService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transferLayings successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransferLayingListDTOs(result),
})
}
func (u *TransferLayingController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, approval, err := u.TransferLayingService.GetOneWithApproval(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transferLaying successfully",
Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, approval),
})
}
func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.TransferLayingService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.TransferLayingService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
func (u *TransferLayingController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.TransferLayingService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete transferLaying successfully",
})
}
func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
results, err := u.TransferLayingService.Approval(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit transfer laying approval successfully"
)
if len(results) == 1 {
data = dto.ToTransferLayingListDTO(results[0])
} else {
message = "Submit transfer laying approvals successfully"
data = dto.ToTransferLayingListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
})
}
@@ -1,255 +0,0 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type TransferLayingBaseDTO struct {
Id uint `json:"id"`
TransferNumber string `json:"transfer_number"`
TransferDate time.Time `json:"transfer_date"`
Notes string `json:"notes"`
}
type ProjectFlockSummaryDTO struct {
Id uint `json:"id"`
Period int `json:"period"`
Category string `json:"category"`
}
type ProductSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type WarehouseSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
type ProductWarehouseSummaryDTO struct {
Product *ProductSummaryDTO `json:"product,omitempty"`
Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"`
}
type ProjectFlockKandangSummaryDTO struct {
Id uint `json:"id"`
KandangId uint `json:"kandang_id"`
}
type LayingTransferSourceDTO struct {
SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"`
Qty float64 `json:"qty"`
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
Note string `json:"note,omitempty"`
}
type LayingTransferTargetDTO struct {
TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"`
Qty float64 `json:"qty"`
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
Note string `json:"note,omitempty"`
}
type TransferLayingListDTO struct {
TransferLayingBaseDTO
FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"`
ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"`
PendingUsageQty *float64 `json:"pending_usage_qty"`
UsageQty *float64 `json:"usage_qty"`
CreatedBy uint `json:"created_by"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
type TransferLayingDetailDTO struct {
TransferLayingListDTO
Sources []LayingTransferSourceDTO `json:"sources,omitempty"`
Targets []LayingTransferTargetDTO `json:"targets,omitempty"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
// === Mapper Functions ===
func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO {
if pf == nil || pf.Id == 0 {
return nil
}
return &ProjectFlockSummaryDTO{
Id: pf.Id,
Period: pf.Period,
Category: pf.Category,
}
}
func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO {
if pfk == nil || pfk.Id == 0 {
return nil
}
return &ProjectFlockKandangSummaryDTO{
Id: pfk.Id,
KandangId: pfk.KandangId,
}
}
func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO {
if product == nil || product.Id == 0 {
return nil
}
return &ProductSummaryDTO{
Id: product.Id,
Name: product.Name,
}
}
func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO {
if warehouse == nil || warehouse.Id == 0 {
return nil
}
return &WarehouseSummaryDTO{
Id: warehouse.Id,
Name: warehouse.Name,
Type: warehouse.Type,
}
}
func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO {
if pw == nil || pw.Id == 0 {
return nil
}
return &ProductWarehouseSummaryDTO{
Product: ToProductSummaryDTO(&pw.Product),
Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse),
}
}
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
return LayingTransferSourceDTO{
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
Qty: source.Qty,
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
Note: source.Note,
}
}
func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingTransferSourceDTO {
if len(sources) == 0 {
return []LayingTransferSourceDTO{}
}
result := make([]LayingTransferSourceDTO, len(sources))
for i, s := range sources {
result[i] = ToLayingTransferSourceDTO(s)
}
return result
}
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
return LayingTransferTargetDTO{
TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang),
Qty: target.Qty,
ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse),
Note: target.Note,
}
}
func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingTransferTargetDTO {
if len(targets) == 0 {
return []LayingTransferTargetDTO{}
}
result := make([]LayingTransferTargetDTO, len(targets))
for i, t := range targets {
result[i] = ToLayingTransferTargetDTO(t)
}
return result
}
func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO {
return TransferLayingBaseDTO{
Id: e.Id,
TransferNumber: e.TransferNumber,
TransferDate: e.TransferDate,
Notes: e.Notes,
}
}
func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
return TransferLayingListDTO{
TransferLayingBaseDTO: ToTransferLayingBaseDTO(e),
FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock),
ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock),
PendingUsageQty: e.PendingUsageQty,
UsageQty: e.UsageQty,
CreatedBy: e.CreatedBy,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
}
}
func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
var latestApproval *approvalDTO.ApprovalBaseDTO
// Use LatestApproval from entity if available
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = &mapped
} else if len(approvals) > 0 {
// Fallback to approvals slice
latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1])
latestApproval = &latest
}
return TransferLayingDetailDTO{
TransferLayingListDTO: ToTransferLayingListDTO(e),
Sources: ToLayingTransferSourceDTOs(e.Sources),
Targets: ToLayingTransferTargetDTOs(e.Targets),
Approval: latestApproval,
}
}
func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO {
var mappedApproval *approvalDTO.ApprovalBaseDTO
// Prefer LatestApproval from entity
if e.LatestApproval != nil && e.LatestApproval.Id != 0 {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
mappedApproval = &mapped
} else if approval != nil && approval.Id != 0 {
// Fallback to passed approval parameter
mapped := approvalDTO.ToApprovalDTO(*approval)
mappedApproval = &mapped
}
return TransferLayingDetailDTO{
TransferLayingListDTO: ToTransferLayingListDTO(e),
Sources: ToLayingTransferSourceDTOs(e.Sources),
Targets: ToLayingTransferTargetDTOs(e.Targets),
Approval: mappedApproval,
}
}
func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingListDTO {
result := make([]TransferLayingListDTO, len(items))
for i, item := range items {
result[i] = ToTransferLayingListDTO(item)
}
return result
}

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