mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f11caac3ff |
@@ -3,13 +3,9 @@ root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
# 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
|
||||
cmd = "go build -o ./tmp/main ./cmd/api"
|
||||
bin = "tmp/main"
|
||||
full_bin = "APP_ENV=dev ./tmp/main"
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
exclude_dir = ["vendor", "tmp"]
|
||||
|
||||
|
||||
+2
-4
@@ -35,8 +35,7 @@ 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_JWKS_URL=http://localhost: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
|
||||
@@ -46,11 +45,10 @@ 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"}}
|
||||
SSO_CLIENTS={"lti":{"public_id":"client:lti","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":"changeme"}}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# .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"}}
|
||||
@@ -10,13 +10,8 @@ bin/
|
||||
*.exe
|
||||
*.out
|
||||
|
||||
Makefile
|
||||
docker-compose.local.yml
|
||||
docker-compose.yaml
|
||||
Dockerfile.local
|
||||
# Go build cache
|
||||
.gocache/
|
||||
vendor/
|
||||
|
||||
# Logs & reports
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
stages: [notify]
|
||||
|
||||
# --- Notify when MR is opened/updated ---
|
||||
notify_discord_mr:
|
||||
stage: notify
|
||||
image: alpine:3.20
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
variables:
|
||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
||||
before_script:
|
||||
- apk add --no-cache curl jq
|
||||
script: |
|
||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
||||
|
||||
jq -n \
|
||||
--arg repo "$CI_PROJECT_PATH" \
|
||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
||||
--arg url "$MR_URL" \
|
||||
--arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
|
||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
||||
'{
|
||||
username: "CI Bot - BE",
|
||||
embeds: [{
|
||||
title: "📣 [LTI API] Merge Request Opened/Updated",
|
||||
description: ($mr + " in " + $repo),
|
||||
url: $url,
|
||||
color: 3447003,
|
||||
fields: [
|
||||
{name: "Author", value: $requestor, inline: true},
|
||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
||||
{name: "Title", value: $title}
|
||||
]
|
||||
}]
|
||||
}' \
|
||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
||||
|
||||
# --- Notify when MR is merged ---
|
||||
notify_discord_merge:
|
||||
stage: notify
|
||||
image: alpine:3.20
|
||||
rules:
|
||||
# Only run for merge request pipelines that are in merged state
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
|
||||
variables:
|
||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
||||
before_script:
|
||||
- apk add --no-cache curl jq
|
||||
script: |
|
||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
||||
|
||||
jq -n \
|
||||
--arg repo "$CI_PROJECT_PATH" \
|
||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
||||
--arg url "$MR_URL" \
|
||||
--arg requestor "${CI_MERGE_REQUEST_AUTHOR}" \
|
||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
||||
'{
|
||||
username: "CI Bot - BE",
|
||||
embeds: [{
|
||||
title: "✅ [LTI API] Merge Request Merged",
|
||||
description: ($mr + " has been merged into " + $repo),
|
||||
url: $url,
|
||||
color: 3066993,
|
||||
fields: [
|
||||
{name: "Author", value: $requestor, inline: true},
|
||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
||||
{name: "Title", value: $title}
|
||||
]
|
||||
}]
|
||||
}' \
|
||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
||||
@@ -1,78 +0,0 @@
|
||||
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
|
||||
@@ -1,59 +1,120 @@
|
||||
# ===============================
|
||||
# LTI-API Makefile (Docker Setup)
|
||||
# ===============================
|
||||
# --- Load .env kalau ada, dan export ke shell child ---
|
||||
ifneq (,$(wildcard .env))
|
||||
include .env
|
||||
export
|
||||
endif
|
||||
|
||||
APP_NAME := lti-api
|
||||
COMPOSE := docker compose -f docker-compose.yaml
|
||||
NETWORK := lti-network
|
||||
ENV_FILE := .env.lti-api
|
||||
# --- 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
|
||||
|
||||
include $(ENV_FILE)
|
||||
export $(shell sed 's/=.*//' $(ENV_FILE))
|
||||
# Fallback agar tetap jalan meski .env kosong
|
||||
DB_HOST ?= postgresdb
|
||||
DB_PORT ?= 5432
|
||||
DB_USER ?= postgres
|
||||
DB_PASSWORD ?= postgres
|
||||
DB_NAME ?= db_lti_erp
|
||||
|
||||
MIGRATIONS_DIR := ./migrations
|
||||
MIGRATE_IMAGE := migrate/migrate:v4.15.2
|
||||
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@lti-postgres:5432/$(DB_NAME)?sslmode=disable
|
||||
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
||||
|
||||
# --- Docker ---
|
||||
# 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 internal/database/migrations $(subst :,_,$*)
|
||||
|
||||
# --- Migration (apply via docker image 'migrate') ---
|
||||
migrate-up: db-up wait-db
|
||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
|
||||
|
||||
# Contoh:
|
||||
# make migrate-down step=2 → rollback 2 step
|
||||
# make migrate-down → rollback semua
|
||||
|
||||
migrate-down: db-up wait-db
|
||||
@if [ -n "$(step)" ]; then \
|
||||
echo "⬇️ Migrating down $(step) step(s)..."; \
|
||||
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down $(step); \
|
||||
else \
|
||||
echo "⬇️ Migrating down ALL steps..."; \
|
||||
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all; \
|
||||
fi
|
||||
|
||||
migrate-fresh: migrate-down migrate-up
|
||||
@true
|
||||
|
||||
# Pakai: make migrate-force v=20250917120000
|
||||
migrate-force:
|
||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" force $(v)
|
||||
|
||||
|
||||
# --- Seeder ---
|
||||
seed: db-up wait-db
|
||||
@$(COMPOSE) run --rm app go run cmd/seed/main.go
|
||||
|
||||
# --- Docker orchestration convenience ---
|
||||
docker-local:
|
||||
@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
|
||||
|
||||
# --- Database / Migration ---
|
||||
docker-cache:
|
||||
@docker builder prune -f
|
||||
|
||||
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!"'
|
||||
# --- PSQL shell ke DB di container ---
|
||||
psql: db-up
|
||||
@$(COMPOSE) exec -it postgresdb psql -U $(DB_USER) -d $(DB_NAME)
|
||||
|
||||
migrate-up: wait-db
|
||||
@echo "⬆️ Running migrations..."
|
||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
|
||||
# Single feature
|
||||
# example: make gen feat=product-category
|
||||
|
||||
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
|
||||
# Sub feature
|
||||
# make gen feat=master/area
|
||||
gen:
|
||||
@go run tools/gen.go $(feat)
|
||||
# @goimports -w internal
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "VERCEL_GIT_COMMIT_REF: $VERCEL_GIT_COMMIT_REF"
|
||||
|
||||
if [[ "$VERCEL_GIT_COMMIT_REF" == "master" || "$VERCEL_GIT_COMMIT_REF" == "development" ]]; then
|
||||
echo "✅ - Build can proceed"
|
||||
exit 1
|
||||
else
|
||||
echo "🛑 - Build cancelled"
|
||||
exit 0
|
||||
fi
|
||||
+4
-35
@@ -13,7 +13,6 @@ import (
|
||||
"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"
|
||||
@@ -36,7 +35,7 @@ func main() {
|
||||
defer closeDatabase(db)
|
||||
rdb := setupRedis()
|
||||
defer rdb.Close()
|
||||
setupSSO(ctx, rdb)
|
||||
setupSSO(ctx)
|
||||
setupRoutes(app, db, rdb)
|
||||
|
||||
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
|
||||
@@ -61,39 +60,9 @@ func setupRedis() *redis.Client {
|
||||
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 setupSSO(ctx context.Context) {
|
||||
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil {
|
||||
utils.Log.Fatalf("SSO initialization failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ 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
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
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:
|
||||
@@ -3,14 +3,11 @@ 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
|
||||
@@ -20,6 +17,7 @@ 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
|
||||
@@ -30,15 +28,13 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/glebarez/sqlite v1.11.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/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
|
||||
@@ -51,6 +47,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
@@ -79,6 +76,7 @@ require (
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/sqlite v1.5.5 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
|
||||
@@ -17,10 +17,6 @@ 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=
|
||||
@@ -47,7 +43,6 @@ 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=
|
||||
@@ -56,53 +51,16 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
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=
|
||||
@@ -111,44 +69,33 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
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=
|
||||
@@ -162,17 +109,10 @@ 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=
|
||||
@@ -186,15 +126,10 @@ 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=
|
||||
@@ -215,41 +150,24 @@ 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=
|
||||
@@ -257,14 +175,7 @@ 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=
|
||||
@@ -273,48 +184,35 @@ 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=
|
||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
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: {},
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ApprovalRepository interface {
|
||||
BaseRepository[entity.Approval]
|
||||
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
|
||||
LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
|
||||
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
|
||||
}
|
||||
|
||||
type approvalRepositoryImpl struct {
|
||||
*BaseRepositoryImpl[entity.Approval]
|
||||
}
|
||||
|
||||
func NewApprovalRepository(db *gorm.DB) ApprovalRepository {
|
||||
return &approvalRepositoryImpl{
|
||||
BaseRepositoryImpl: NewBaseRepository[entity.Approval](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *approvalRepositoryImpl) FindByTarget(
|
||||
ctx context.Context,
|
||||
workflow string,
|
||||
approvableID uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) ([]entity.Approval, error) {
|
||||
var approvals []entity.Approval
|
||||
|
||||
q := r.DB().WithContext(ctx).Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
if err := q.Order("action_at ASC").Find(&approvals).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return approvals, nil
|
||||
}
|
||||
|
||||
func (r *approvalRepositoryImpl) LatestByTarget(
|
||||
ctx context.Context,
|
||||
workflow string,
|
||||
approvableID uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (*entity.Approval, error) {
|
||||
var approval entity.Approval
|
||||
|
||||
q := r.DB().WithContext(ctx).
|
||||
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
|
||||
Order("action_at DESC")
|
||||
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
if err := q.Limit(1).First(&approval).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &approval, nil
|
||||
}
|
||||
|
||||
func (r *approvalRepositoryImpl) LatestByTargets(
|
||||
ctx context.Context,
|
||||
workflow string,
|
||||
approvableIDs []uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (map[uint]entity.Approval, error) {
|
||||
if len(approvableIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := make(map[uint]entity.Approval, len(approvableIDs))
|
||||
|
||||
q := r.DB().WithContext(ctx).
|
||||
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
|
||||
Order("action_at DESC")
|
||||
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
var approvals []entity.Approval
|
||||
if err := q.Find(&approvals).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, approval := range approvals {
|
||||
if _, exists := result[approval.ApprovableId]; exists {
|
||||
continue
|
||||
}
|
||||
result[approval.ApprovableId] = approval
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
-19
@@ -2,7 +2,6 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -33,21 +32,3 @@ func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeI
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
|
||||
if field == "" {
|
||||
return false, fmt.Errorf("field is required")
|
||||
}
|
||||
var count int64
|
||||
q := db.WithContext(ctx).
|
||||
Model(new(T)).
|
||||
Where(fmt.Sprintf("%s = ?", field), value).
|
||||
Where("deleted_at IS NULL")
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ApprovalService interface {
|
||||
RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error
|
||||
WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string
|
||||
WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool)
|
||||
CreateApproval(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, step approvalutils.ApprovalStep, action *entity.ApprovalAction, actorID uint, note *string) (*entity.Approval, error)
|
||||
List(ctx context.Context, module string, approvableID *uint, page, limit int, search string) ([]entity.Approval, int64, error)
|
||||
ListByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
|
||||
LatestByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
|
||||
LatestByTargets(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]*entity.Approval, error)
|
||||
}
|
||||
|
||||
type approvalService struct {
|
||||
repo commonRepo.ApprovalRepository
|
||||
}
|
||||
|
||||
func NewApprovalService(repo commonRepo.ApprovalRepository) ApprovalService {
|
||||
return &approvalService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *approvalService) RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error {
|
||||
return approvalutils.RegisterWorkflowSteps(workflow, steps)
|
||||
}
|
||||
|
||||
func (s *approvalService) WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string {
|
||||
return approvalutils.WorkflowSteps(workflow)
|
||||
}
|
||||
|
||||
func (s *approvalService) WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool) {
|
||||
return approvalutils.ApprovalStepName(workflow, step)
|
||||
}
|
||||
|
||||
func (s *approvalService) CreateApproval(
|
||||
ctx context.Context,
|
||||
workflow approvalutils.ApprovalWorkflowKey,
|
||||
approvableID uint,
|
||||
step approvalutils.ApprovalStep,
|
||||
action *entity.ApprovalAction,
|
||||
actorID uint,
|
||||
note *string,
|
||||
) (*entity.Approval, error) {
|
||||
record, err := approvalutils.NewApproval(workflow, approvableID, step, action, actorID, note)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.repo.CreateOne(ctx, record, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.decorateApproval(workflow, record)
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) List(
|
||||
ctx context.Context,
|
||||
module string,
|
||||
approvableID *uint,
|
||||
page, limit int,
|
||||
search string,
|
||||
) ([]entity.Approval, int64, error) {
|
||||
module = strings.TrimSpace(strings.ToUpper(module))
|
||||
search = strings.TrimSpace(search)
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
records, total, err := s.repo.GetAll(
|
||||
ctx,
|
||||
offset,
|
||||
limit,
|
||||
func(db *gorm.DB) *gorm.DB {
|
||||
query := db.
|
||||
Where("approvable_type = ?", module).
|
||||
Order("action_at DESC").
|
||||
Preload("ActionUser")
|
||||
|
||||
if approvableID != nil {
|
||||
query = query.Where("approvable_id = ?", *approvableID)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
like := "%" + strings.ToLower(search) + "%"
|
||||
query = query.Where("(LOWER(step_name) LIKE ? OR LOWER(action) LIKE ? OR LOWER(notes) LIKE ?)", like, like, like)
|
||||
}
|
||||
|
||||
return query
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if s.isApprovalTableMissing(err) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, total, nil
|
||||
}
|
||||
|
||||
workflow := approvalutils.ApprovalWorkflowKey(module)
|
||||
for i := range records {
|
||||
s.decorateApproval(workflow, &records[i])
|
||||
}
|
||||
|
||||
return records, total, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) ListByTarget(
|
||||
ctx context.Context,
|
||||
workflow approvalutils.ApprovalWorkflowKey,
|
||||
approvableID uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) ([]entity.Approval, error) {
|
||||
records, err := s.repo.FindByTarget(ctx, workflow.String(), approvableID, modifier)
|
||||
if err != nil {
|
||||
if s.isApprovalTableMissing(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range records {
|
||||
s.decorateApproval(workflow, &records[i])
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) LatestByTarget(
|
||||
ctx context.Context,
|
||||
workflow approvalutils.ApprovalWorkflowKey,
|
||||
approvableID uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (*entity.Approval, error) {
|
||||
record, err := s.repo.LatestByTarget(ctx, workflow.String(), approvableID, modifier)
|
||||
if err != nil {
|
||||
if s.isApprovalTableMissing(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, nil
|
||||
}
|
||||
s.decorateApproval(workflow, record)
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) LatestByTargets(
|
||||
ctx context.Context,
|
||||
workflow approvalutils.ApprovalWorkflowKey,
|
||||
approvableIDs []uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (map[uint]*entity.Approval, error) {
|
||||
records, err := s.repo.LatestByTargets(ctx, workflow.String(), approvableIDs, modifier)
|
||||
if err != nil {
|
||||
if s.isApprovalTableMissing(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := make(map[uint]*entity.Approval, len(records))
|
||||
for approvableID, approval := range records {
|
||||
approvalCopy := approval
|
||||
s.decorateApproval(workflow, &approvalCopy)
|
||||
result[approvableID] = &approvalCopy
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) decorateApproval(workflow approvalutils.ApprovalWorkflowKey, approval *entity.Approval) {
|
||||
if approval == nil {
|
||||
return
|
||||
}
|
||||
currentName := strings.TrimSpace(approval.StepName)
|
||||
if currentName == "" {
|
||||
if name, ok := approvalutils.ApprovalStepName(workflow, approvalutils.ApprovalStep(approval.StepNumber)); ok {
|
||||
approval.StepName = name
|
||||
}
|
||||
} else {
|
||||
approval.StepName = currentName
|
||||
}
|
||||
}
|
||||
|
||||
func (s *approvalService) isApprovalTableMissing(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
|
||||
if strings.Contains(errMsg, "no such table: approvals") {
|
||||
return true
|
||||
}
|
||||
|
||||
schemaIssues := []string{
|
||||
`relation "approvals" does not exist`,
|
||||
`column "step_name" does not exist`,
|
||||
`column "step_number" does not exist`,
|
||||
`column "action" does not exist`,
|
||||
`column "status" does not exist`,
|
||||
`column "step" does not exist`,
|
||||
}
|
||||
for _, issue := range schemaIssues {
|
||||
if strings.Contains(errMsg, issue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
)
|
||||
|
||||
type SSOClientConfig struct {
|
||||
PublicID string `json:"public_id"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
Scope string `json:"scope"`
|
||||
// Prompt string `json:"prompt"`
|
||||
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"`
|
||||
@@ -32,10 +32,6 @@ var (
|
||||
DBPassword string
|
||||
DBName string
|
||||
DBPort int
|
||||
DBSSLMode string
|
||||
DBSSLRootCert string
|
||||
DBSSLCert string
|
||||
DBSSLKey string
|
||||
JWTSecret string
|
||||
JWTAccessExp int
|
||||
JWTRefreshExp int
|
||||
@@ -60,7 +56,6 @@ var (
|
||||
SSOCookieDomain string
|
||||
SSOCookieSecure bool
|
||||
SSOCookieSameSite string
|
||||
SSOTokenBlacklistPrefix string
|
||||
SSOPKCETTL time.Duration
|
||||
SSOUserSyncDrift time.Duration
|
||||
SSOUserSyncNonceTTL time.Duration
|
||||
@@ -83,10 +78,6 @@ 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")
|
||||
@@ -118,7 +109,6 @@ func init() {
|
||||
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 {
|
||||
|
||||
@@ -2,7 +2,6 @@ package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
@@ -14,25 +13,10 @@ import (
|
||||
)
|
||||
|
||||
func Connect(dbHost, dbName string) *gorm.DB {
|
||||
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, " ")
|
||||
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,
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
DROP TABLE IF EXISTS stock_logs;
|
||||
DROP INDEX IF EXISTS idx_product_warehouses_unique;
|
||||
DROP INDEX IF EXISTS idx_product_warehouses_deleted_at;
|
||||
DROP INDEX IF EXISTS idx_product_warehouses_warehouse_id;
|
||||
DROP INDEX IF EXISTS idx_product_warehouses_product_id;
|
||||
DROP TABLE IF EXISTS product_warehouses;
|
||||
|
||||
DROP TABLE IF EXISTS fcr_standards;
|
||||
DROP INDEX IF EXISTS suppliers_name_unique;
|
||||
DROP TABLE IF EXISTS product_suppliers;
|
||||
@@ -40,4 +35,4 @@ DROP TABLE IF EXISTS fcrs;
|
||||
DROP TABLE IF EXISTS projects;
|
||||
DROP INDEX IF EXISTS users_id_user_unique;
|
||||
DROP INDEX IF EXISTS users_email_unique;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
-- USERS
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id_user BIGINT NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id_user BIGINT NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
|
||||
@@ -15,319 +15,221 @@ CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL
|
||||
|
||||
-- FLAGS
|
||||
CREATE TABLE flags (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
flagable_id BIGINT NOT NULL,
|
||||
flagable_type VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX flags_unique_flagable ON flags (
|
||||
name,
|
||||
flagable_id,
|
||||
flagable_type
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
flagable_id BIGINT NOT NULL,
|
||||
flagable_type VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
|
||||
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
||||
|
||||
-- PRODUCT CATEGORIES
|
||||
CREATE TABLE product_categories (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
code VARCHAR(10) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
code VARCHAR(10) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code) WHERE deleted_at IS NULL;
|
||||
|
||||
-- UOM
|
||||
CREATE TABLE uoms (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- BANKS
|
||||
CREATE TABLE banks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
alias VARCHAR(5) NOT NULL,
|
||||
owner VARCHAR,
|
||||
account_number VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
alias VARCHAR(5) NOT NULL,
|
||||
owner VARCHAR,
|
||||
account_number VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX banks_name_unique ON banks (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX banks_name_unique ON banks (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- AREAS
|
||||
CREATE TABLE areas (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX areas_name_unique ON areas (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX areas_name_unique ON areas (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- LOCATIONS
|
||||
CREATE TABLE locations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX locations_name_unique ON locations (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX locations_name_unique ON locations (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- KANDANG
|
||||
CREATE TABLE kandangs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
location_id BIGINT NOT NULL REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- WAREHOUSES
|
||||
CREATE TABLE warehouses (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
location_id BIGINT REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
location_id BIGINT REFERENCES locations(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
kandang_id BIGINT REFERENCES kandangs(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- CUSTOMERS
|
||||
CREATE TABLE customers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
account_number VARCHAR(50) NOT NULL,
|
||||
balance NUMERIC(15, 3) DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
account_number VARCHAR(50) NOT NULL,
|
||||
balance NUMERIC(15,3) DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX customers_name_unique ON customers (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX customers_name_unique ON customers (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- NONSTOCK
|
||||
CREATE TABLE nonstocks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- FCR
|
||||
CREATE TABLE fcrs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE fcr_standards (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
weight NUMERIC(15, 3) NOT NULL,
|
||||
fcr_number NUMERIC(15, 3) NOT NULL,
|
||||
mortality NUMERIC(15, 3) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fcr_id BIGINT NOT NULL REFERENCES fcrs(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
weight NUMERIC(15,3) NOT NULL,
|
||||
fcr_number NUMERIC(15,3) NOT NULL,
|
||||
mortality NUMERIC(15,3) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- SUPPLIERS
|
||||
CREATE TABLE suppliers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
alias VARCHAR(5) NOT NULL,
|
||||
pic VARCHAR NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
category VARCHAR(20) NOT NULL,
|
||||
hatchery VARCHAR,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
npwp VARCHAR(50),
|
||||
account_number VARCHAR(50),
|
||||
balance NUMERIC(15, 3) DEFAULT 0,
|
||||
due_date INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
alias VARCHAR(5) NOT NULL,
|
||||
pic VARCHAR NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
category VARCHAR(20) NOT NULL,
|
||||
hatchery VARCHAR,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
npwp VARCHAR(50),
|
||||
account_number VARCHAR(50),
|
||||
balance NUMERIC(15,3) DEFAULT 0,
|
||||
due_date INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE nonstock_suppliers (
|
||||
nonstock_id BIGINT NOT NULL REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (nonstock_id, supplier_id)
|
||||
nonstock_id BIGINT NOT NULL REFERENCES nonstocks(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (nonstock_id, supplier_id)
|
||||
);
|
||||
|
||||
-- PRODUCTS
|
||||
CREATE TABLE products (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
brand VARCHAR NOT NULL,
|
||||
sku VARCHAR(100),
|
||||
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
product_price NUMERIC(15, 3) NOT NULL,
|
||||
selling_price NUMERIC(15, 3),
|
||||
tax NUMERIC(15, 3),
|
||||
expiry_period INT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
brand VARCHAR NOT NULL,
|
||||
sku VARCHAR(100),
|
||||
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
product_category_id BIGINT NOT NULL REFERENCES product_categories(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
product_price NUMERIC(15,3) NOT NULL,
|
||||
selling_price NUMERIC(15,3),
|
||||
tax NUMERIC(15,3),
|
||||
expiry_period INT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX products_name_unique ON products (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX products_sku_unique ON products (sku)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE product_suppliers (
|
||||
product_id BIGINT NOT NULL REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (product_id, supplier_id)
|
||||
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (product_id, supplier_id)
|
||||
);
|
||||
|
||||
-- PROJECTS
|
||||
CREATE TABLE projects (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- PRODUCT WAREHOUSES TABLE
|
||||
CREATE TABLE product_warehouses (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
product_id BIGINT NOT NULL REFERENCES products (id),
|
||||
warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
|
||||
quantity INTEGER NOT NULL DEFAULT 0,
|
||||
created_by BIGINT NOT NULL REFERENCES users (id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- INDEXES
|
||||
CREATE INDEX idx_product_warehouses_product_id ON product_warehouses (product_id);
|
||||
|
||||
CREATE INDEX idx_product_warehouses_warehouse_id ON product_warehouses (warehouse_id);
|
||||
|
||||
CREATE INDEX idx_product_warehouses_deleted_at ON product_warehouses (deleted_at);
|
||||
|
||||
CREATE UNIQUE INDEX idx_product_warehouses_unique ON product_warehouses (product_id, warehouse_id)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
|
||||
-- STOCK LOGS
|
||||
CREATE TABLE stock_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
transaction_type VARCHAR(20) NOT NULL,
|
||||
quantity NUMERIC(15, 3) NOT NULL,
|
||||
before_quantity NUMERIC(15, 3) NOT NULL,
|
||||
after_quantity NUMERIC(15, 3) NOT NULL,
|
||||
log_type VARCHAR(50) NOT NULL,
|
||||
log_id BIGINT,
|
||||
note TEXT,
|
||||
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
|
||||
|
||||
CREATE INDEX stock_logs_log_type_log_id_idx ON stock_logs (log_type, log_id);
|
||||
|
||||
CREATE INDEX stock_logs_created_by_idx ON stock_logs (created_by);
|
||||
|
||||
CREATE INDEX stock_logs_created_at_idx ON stock_logs (created_at);
|
||||
|
||||
CREATE INDEX stock_logs_deleted_at_idx ON stock_logs (deleted_at);
|
||||
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE users
|
||||
DROP CONSTRAINT IF EXISTS users_id_user_key;
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE users
|
||||
ADD CONSTRAINT users_id_user_key UNIQUE (id_user);
|
||||
@@ -1,4 +0,0 @@
|
||||
-- DROP TABLE: STOCK_TRANSFERS DAN SEQUENCE-NYA
|
||||
DROP TABLE IF EXISTS stock_transfers CASCADE;
|
||||
|
||||
DROP SEQUENCE IF EXISTS stock_transfer_seq CASCADE;
|
||||
@@ -1,57 +0,0 @@
|
||||
-- ===============================================================
|
||||
-- STOCK TRANSFERS (HEADER)
|
||||
-- ===============================================================
|
||||
|
||||
CREATE SEQUENCE IF NOT EXISTS stock_transfer_seq START 1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_transfers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
movement_number VARCHAR(50) UNIQUE NOT NULL,
|
||||
from_warehouse_id BIGINT NOT NULL,
|
||||
to_warehouse_id BIGINT NOT NULL,
|
||||
area_id BIGINT,
|
||||
reason TEXT,
|
||||
transfer_date DATE NOT NULL,
|
||||
created_by BIGINT,
|
||||
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 = 'warehouses') THEN
|
||||
ALTER TABLE stock_transfers
|
||||
ADD CONSTRAINT fk_stock_transfers_from_warehouse
|
||||
FOREIGN KEY (from_warehouse_id)
|
||||
REFERENCES warehouses(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE stock_transfers
|
||||
ADD CONSTRAINT fk_stock_transfers_to_warehouse
|
||||
FOREIGN KEY (to_warehouse_id)
|
||||
REFERENCES warehouses(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'areas') THEN
|
||||
ALTER TABLE stock_transfers
|
||||
ADD CONSTRAINT fk_stock_transfers_area
|
||||
FOREIGN KEY (area_id)
|
||||
REFERENCES areas(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||
ALTER TABLE stock_transfers
|
||||
ADD CONSTRAINT fk_stock_transfers_created_by
|
||||
FOREIGN KEY (created_by)
|
||||
REFERENCES users(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- INDEXES
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfers_from_warehouse_id ON stock_transfers(from_warehouse_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfers_to_warehouse_id ON stock_transfers(to_warehouse_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfers_transfer_date ON stock_transfers(transfer_date);
|
||||
@@ -1,2 +0,0 @@
|
||||
-- DROP TABLE: STOCK_TRANSFER_DETAILS
|
||||
DROP TABLE IF EXISTS stock_transfer_details CASCADE;
|
||||
@@ -1,48 +0,0 @@
|
||||
-- ===============================================================
|
||||
-- STOCK TRANSFER DETAILS (PRODUK)
|
||||
-- ===============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_transfer_details (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
stock_transfer_id BIGINT NOT NULL,
|
||||
product_id BIGINT NOT NULL,
|
||||
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0),
|
||||
before_quantity NUMERIC(15, 3),
|
||||
after_quantity NUMERIC(15, 3),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- ===============================================================
|
||||
-- FOREIGN KEYS (dengan pengecekan tabel agar anti gagal)
|
||||
-- ===============================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE stock_transfer_details
|
||||
ADD CONSTRAINT fk_stock_transfer_details_transfer
|
||||
FOREIGN KEY (stock_transfer_id)
|
||||
REFERENCES stock_transfers(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE stock_transfer_details
|
||||
ADD CONSTRAINT fk_stock_transfer_details_product
|
||||
FOREIGN KEY (product_id)
|
||||
REFERENCES products(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ===============================================================
|
||||
-- INDEXES
|
||||
-- ===============================================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_transfer_id ON stock_transfer_details (stock_transfer_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_product_id ON stock_transfer_details (product_id);
|
||||
@@ -1,2 +0,0 @@
|
||||
-- DROP TABLE: STOCK_TRANSFER_DELIVERIES
|
||||
DROP TABLE IF EXISTS stock_transfer_deliveries CASCADE;
|
||||
@@ -1,42 +0,0 @@
|
||||
-- ===============================================================
|
||||
-- STOCK TRANSFER DELIVERIES (EKSPEDISI)
|
||||
-- ===============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_transfer_deliveries (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
stock_transfer_id BIGINT NOT NULL,
|
||||
supplier_id BIGINT,
|
||||
vehicle_plate VARCHAR(20),
|
||||
driver_name VARCHAR(100),
|
||||
document_number VARCHAR(50),
|
||||
document_path TEXT,
|
||||
shipping_cost_item NUMERIC(15,3),
|
||||
shipping_cost_total NUMERIC(15,3),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- FOREIGN KEYS
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
|
||||
ALTER TABLE stock_transfer_deliveries
|
||||
ADD CONSTRAINT fk_stock_transfer_deliveries_transfer
|
||||
FOREIGN KEY (stock_transfer_id)
|
||||
REFERENCES stock_transfers(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
|
||||
ALTER TABLE stock_transfer_deliveries
|
||||
ADD CONSTRAINT fk_stock_transfer_deliveries_supplier
|
||||
FOREIGN KEY (supplier_id)
|
||||
REFERENCES suppliers(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- INDEXES
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_transfer_id ON stock_transfer_deliveries(stock_transfer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_supplier_id ON stock_transfer_deliveries(supplier_id);
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
-- DROP PIVOT TABLE: STOCK_TRANSFER_DELIVERY_ITEMS
|
||||
DROP TABLE IF EXISTS stock_transfer_delivery_items CASCADE;
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
-- ===============================================================
|
||||
-- STOCK TRANSFER DELIVERY ITEMS (PIVOT)
|
||||
-- ===============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_transfer_delivery_items (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
stock_transfer_delivery_id BIGINT NOT NULL,
|
||||
stock_transfer_detail_id BIGINT NOT NULL,
|
||||
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0)
|
||||
);
|
||||
|
||||
-- FOREIGN KEYS
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_deliveries') THEN
|
||||
ALTER TABLE stock_transfer_delivery_items
|
||||
ADD CONSTRAINT fk_delivery_items_delivery
|
||||
FOREIGN KEY (stock_transfer_delivery_id)
|
||||
REFERENCES stock_transfer_deliveries(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_details') THEN
|
||||
ALTER TABLE stock_transfer_delivery_items
|
||||
ADD CONSTRAINT fk_delivery_items_detail
|
||||
FOREIGN KEY (stock_transfer_detail_id)
|
||||
REFERENCES stock_transfer_details(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- INDEXES
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_delivery_id ON stock_transfer_delivery_items (stock_transfer_delivery_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_detail_id ON stock_transfer_delivery_items (stock_transfer_detail_id);
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE kandangs
|
||||
DROP COLUMN IF EXISTS status;
|
||||
@@ -1,9 +0,0 @@
|
||||
ALTER TABLE kandangs
|
||||
ADD COLUMN status VARCHAR(20);
|
||||
|
||||
UPDATE kandangs
|
||||
SET status = 'NON_ACTIVE'
|
||||
WHERE status IS NULL;
|
||||
|
||||
ALTER TABLE kandangs
|
||||
ALTER COLUMN status SET NOT NULL;
|
||||
@@ -1,6 +0,0 @@
|
||||
ALTER TABLE kandangs
|
||||
DROP COLUMN IF EXISTS project_flock_id;
|
||||
|
||||
DROP TABLE IF EXISTS project_flocks;
|
||||
|
||||
DROP TABLE IF EXISTS flocks;
|
||||
@@ -1,29 +0,0 @@
|
||||
CREATE TABLE flocks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX flocks_name_unique ON flocks (name)
|
||||
WHERE
|
||||
deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE project_flocks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
flock_id BIGINT NOT NULL REFERENCES flocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
period INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
ALTER TABLE kandangs
|
||||
ADD COLUMN project_flock_id BIGINT REFERENCES project_flocks (id) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP INDEX IF EXISTS approvals_approvable_lookup;
|
||||
DROP TABLE IF EXISTS approvals;
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE approvals (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
approvable_type VARCHAR(50) NOT NULL,
|
||||
approvable_id BIGINT NOT NULL,
|
||||
step SMALLINT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
action_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX approvals_approvable_lookup ON approvals (approvable_type, approvable_id);
|
||||
@@ -1,18 +0,0 @@
|
||||
ALTER TABLE approvals
|
||||
RENAME COLUMN action TO status;
|
||||
|
||||
UPDATE approvals
|
||||
SET status = 'PENDING'
|
||||
WHERE status IS NULL;
|
||||
|
||||
ALTER TABLE approvals
|
||||
ALTER COLUMN status SET NOT NULL;
|
||||
|
||||
ALTER TABLE approvals
|
||||
RENAME COLUMN step_number TO step;
|
||||
|
||||
ALTER TABLE approvals
|
||||
DROP COLUMN step_name;
|
||||
|
||||
ALTER TABLE approvals
|
||||
RENAME COLUMN action_at TO created_at;
|
||||
@@ -1,14 +0,0 @@
|
||||
ALTER TABLE approvals
|
||||
RENAME COLUMN status TO action;
|
||||
|
||||
ALTER TABLE approvals
|
||||
ALTER COLUMN action DROP NOT NULL;
|
||||
|
||||
ALTER TABLE approvals
|
||||
RENAME COLUMN step TO step_number;
|
||||
|
||||
ALTER TABLE approvals
|
||||
ADD COLUMN step_name VARCHAR NOT NULL;
|
||||
|
||||
ALTER TABLE approvals
|
||||
RENAME COLUMN created_at TO action_at;
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS project_flock_kandangs;
|
||||
@@ -1,17 +0,0 @@
|
||||
CREATE TABLE project_flock_kandangs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_flock_id BIGINT NOT NULL REFERENCES project_flocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
kandang_id BIGINT NOT NULL REFERENCES kandangs (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
detached_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_project_flock_kandangs_project ON project_flock_kandangs (project_flock_id);
|
||||
CREATE INDEX idx_project_flock_kandangs_kandang ON project_flock_kandangs (kandang_id);
|
||||
|
||||
CREATE UNIQUE INDEX idx_project_flock_kandangs_active ON project_flock_kandangs (project_flock_id, kandang_id)
|
||||
WHERE
|
||||
detached_at IS NULL;
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS project_chickins;
|
||||
@@ -1,36 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS project_chickins (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_flock_kandang_id BIGINT NOT NULL,
|
||||
chick_in_date DATE NOT NULL,
|
||||
quantity NUMERIC(15, 3) NOT NULL,
|
||||
note TEXT,
|
||||
created_by BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||
ALTER TABLE project_chickins
|
||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||
ALTER TABLE project_chickins
|
||||
ADD CONSTRAINT fk_created_by
|
||||
FOREIGN KEY (created_by)
|
||||
REFERENCES users(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- INDEXES
|
||||
CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by);
|
||||
-1
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS project_flock_populations;
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS project_flock_populations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_flock_kandang_id BIGINT NOT NULL,
|
||||
initial_quantity NUMERIC(15, 3) NOT NULL,
|
||||
current_quantity NUMERIC(15, 3) NOT NULL,
|
||||
reserved_quantity NUMERIC(15, 3),
|
||||
created_by BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||
ALTER TABLE project_flock_populations
|
||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||
ALTER TABLE project_flock_populations
|
||||
ADD CONSTRAINT fk_created_by
|
||||
FOREIGN KEY (created_by)
|
||||
REFERENCES users(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- INDEXES
|
||||
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
-- Recreate legacy columns on project_flock_kandangs
|
||||
DROP INDEX IF EXISTS idx_project_flock_kandangs_unique;
|
||||
|
||||
ALTER TABLE project_flock_kandangs
|
||||
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
ADD COLUMN IF NOT EXISTS assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
ADD COLUMN IF NOT EXISTS detached_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_active
|
||||
ON project_flock_kandangs (project_flock_id, kandang_id)
|
||||
WHERE detached_at IS NULL;
|
||||
|
||||
-- Restore product_category_id reference and drop category column
|
||||
ALTER TABLE project_flocks
|
||||
ADD COLUMN IF NOT EXISTS product_category_id BIGINT REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
DROP COLUMN IF EXISTS category;
|
||||
|
||||
COMMIT;
|
||||
|
||||
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
-- Add category column to project_flocks and backfill existing rows
|
||||
ALTER TABLE project_flocks
|
||||
ADD COLUMN IF NOT EXISTS category VARCHAR(20);
|
||||
|
||||
UPDATE project_flocks
|
||||
SET category = 'GROWING'
|
||||
WHERE category IS NULL;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ALTER COLUMN category SET NOT NULL;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ALTER COLUMN category SET DEFAULT 'GROWING';
|
||||
|
||||
-- Drop legacy foreign key reference and column
|
||||
ALTER TABLE project_flocks
|
||||
DROP CONSTRAINT IF EXISTS project_flocks_product_category_id_fkey;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
DROP COLUMN IF EXISTS product_category_id;
|
||||
|
||||
-- Simplify project_flock_kandangs structure
|
||||
DROP INDEX IF EXISTS idx_project_flock_kandangs_active;
|
||||
|
||||
ALTER TABLE project_flock_kandangs
|
||||
DROP COLUMN IF EXISTS created_by,
|
||||
DROP COLUMN IF EXISTS assigned_at,
|
||||
DROP COLUMN IF EXISTS detached_at,
|
||||
DROP COLUMN IF EXISTS updated_at;
|
||||
|
||||
ALTER TABLE project_flock_kandangs
|
||||
ALTER COLUMN created_at SET DEFAULT NOW();
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_unique
|
||||
ON project_flock_kandangs (project_flock_id, kandang_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
CREATE UNIQUE INDEX project_flocks_flock_period_unique
|
||||
ON project_flocks (flock_id, period)
|
||||
WHERE deleted_at IS NULL;
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS project_chickin_details;
|
||||
@@ -1,45 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS project_chickin_details (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_chickin_id BIGINT NOT NULL,
|
||||
product_warehouse_id BIGINT NOT NULL,
|
||||
quantity NUMERIC(15, 3) NOT NULL,
|
||||
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_chickins') THEN
|
||||
ALTER TABLE project_chickin_details
|
||||
ADD CONSTRAINT fk_project_chickin_id
|
||||
FOREIGN KEY (project_chickin_id)
|
||||
REFERENCES project_chickins(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||
ALTER TABLE project_chickin_details
|
||||
ADD CONSTRAINT fk_product_warehouse_id
|
||||
FOREIGN KEY (product_warehouse_id)
|
||||
REFERENCES product_warehouses(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||
ALTER TABLE project_chickin_details
|
||||
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_chickin_details_project_chickin_id ON project_chickin_details (project_chickin_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_product_warehouse_id ON project_chickin_details (product_warehouse_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_created_by ON project_chickin_details (created_by);
|
||||
@@ -1,24 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
--? Child Indexes(optional, biar rapi tapi klo gada juga ilang pas di drop)
|
||||
DROP INDEX IF EXISTS idx_recording_stocks_product;
|
||||
DROP INDEX IF EXISTS idx_recording_stocks_recording;
|
||||
|
||||
|
||||
DROP INDEX IF EXISTS idx_recording_depl_recording;
|
||||
|
||||
DROP INDEX IF EXISTS idx_recording_bws_recording;
|
||||
|
||||
--? Child Tables
|
||||
DROP TABLE IF EXISTS recording_stocks;
|
||||
DROP TABLE IF EXISTS recording_depletions;
|
||||
DROP TABLE IF EXISTS recording_bws;
|
||||
|
||||
--? Parent Indexes ON recordings
|
||||
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
|
||||
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
|
||||
|
||||
--? Parent table
|
||||
DROP TABLE IF EXISTS recordings;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,150 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
--? RECORDINGS (tabel induk recording harian)
|
||||
CREATE TABLE IF NOT EXISTS recordings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_flock_id BIGINT NOT NULL,
|
||||
record_datetime TIMESTAMPTZ NOT NULL,
|
||||
record_date DATE,
|
||||
status INT NOT NULL DEFAULT 0, --? 0=draft,1=submitted,2=approved,3=rejected
|
||||
ontime INT NOT NULL DEFAULT 0, --? 1=ontime,0=late (pakai INT/BOOLEAN sesuai preferensi)
|
||||
day INT,
|
||||
total_depletion INT,
|
||||
cum_depletion_rate NUMERIC(7,3),
|
||||
daily_gain NUMERIC(7,3),
|
||||
avg_daily_gain NUMERIC(7,3),
|
||||
cum_intake INT,
|
||||
fcr_value NUMERIC(7,3),
|
||||
total_chick BIGINT,
|
||||
daily_depletion_rate NUMERIC(7,3),
|
||||
cum_depletion INT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT,
|
||||
|
||||
CONSTRAINT fk_recordings_project_flock
|
||||
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id),
|
||||
|
||||
CONSTRAINT fk_recordings_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||
|
||||
|
||||
CONSTRAINT chk_recordings_status
|
||||
CHECK (status IN (0,1,2,3)),
|
||||
|
||||
CONSTRAINT chk_recordings_ontime
|
||||
CHECK (ontime IN (0,1)),
|
||||
|
||||
CONSTRAINT chk_recordings_day
|
||||
CHECK (day IS NULL OR day >= 1),
|
||||
|
||||
CONSTRAINT chk_recordings_nonnegatives
|
||||
CHECK (
|
||||
(total_depletion IS NULL OR total_depletion >= 0) AND
|
||||
(cum_depletion IS NULL OR cum_depletion >= 0) AND
|
||||
(total_chick IS NULL OR total_chick >= 0) AND
|
||||
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||
(fcr_value IS NULL OR fcr_value > 0) AND
|
||||
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
|
||||
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
|
||||
)
|
||||
);
|
||||
|
||||
--? Set record_date otomatis berdasarkan record_datetime (pakai zona Asia/Jakarta)
|
||||
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
|
||||
CREATE TRIGGER recordings_set_record_date_trg
|
||||
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
|
||||
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recordings_flock_datetime
|
||||
ON recordings (project_flock_id, record_datetime);
|
||||
|
||||
--? Unique harian (1 recording per hari dan per flock)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_recordings_flock_record_date
|
||||
ON recordings (project_flock_id, record_date)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
|
||||
--? RECORDING_BWS (BW per recording)
|
||||
CREATE TABLE IF NOT EXISTS recording_bws (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_id BIGINT NOT NULL,
|
||||
weight NUMERIC(8,2) NOT NULL, --? bobot per ekor/kelompok
|
||||
qty INT NOT NULL DEFAULT 1, --? jumlah ekor pada bobot ini
|
||||
notes VARCHAR,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT fk_recording_bws_recording
|
||||
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT chk_recording_bws_nonneg
|
||||
CHECK (weight >= 0 AND qty >= 1)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_bws_recording
|
||||
ON recording_bws (recording_id);
|
||||
|
||||
--? RECORDING_DEPLETIONS
|
||||
CREATE TABLE IF NOT EXISTS recording_depletions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_id BIGINT NOT NULL,
|
||||
product_warehouse_id BIGINT NOT NULL,
|
||||
total BIGINT NOT NULL,
|
||||
notes VARCHAR,
|
||||
|
||||
CONSTRAINT fk_recording_depl_recording
|
||||
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_recording_depl_prodwh
|
||||
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
|
||||
|
||||
CONSTRAINT chk_recording_depl_total
|
||||
CHECK (total >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_depl_recording
|
||||
ON recording_depletions (recording_id);
|
||||
|
||||
--? RECORDING_STOCKS
|
||||
CREATE TABLE IF NOT EXISTS recording_stocks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_id BIGINT NOT NULL,
|
||||
product_warehouse_id BIGINT NOT NULL,
|
||||
increase NUMERIC(10,3), --? penambahan (boleh NULL)
|
||||
decrease NUMERIC(10,3), --? pengurangan (boleh NULL)
|
||||
usage_amount BIGINT, --? pemakaian (opsional, jika konsep dipisah dari decrease)
|
||||
notes VARCHAR,
|
||||
|
||||
CONSTRAINT fk_recording_stocks_recording
|
||||
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_recording_stocks_prodwh
|
||||
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
|
||||
|
||||
CONSTRAINT chk_recording_stocks_nonneg
|
||||
CHECK (
|
||||
(increase IS NULL OR increase >= 0) AND
|
||||
(decrease IS NULL OR decrease >= 0) AND
|
||||
(usage_amount IS NULL OR usage_amount >= 0)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_stocks_recording
|
||||
ON recording_stocks (recording_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_stocks_product
|
||||
ON recording_stocks (product_warehouse_id);
|
||||
|
||||
COMMIT;
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
|
||||
ALTER TABLE kandangs
|
||||
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
||||
|
||||
ALTER TABLE kandangs
|
||||
DROP COLUMN IF EXISTS project_flock_id;
|
||||
|
||||
ALTER TABLE project_chickins
|
||||
DROP CONSTRAINT fk_project_flock_kandang_id,
|
||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE project_flock_populations
|
||||
DROP CONSTRAINT fk_project_flock_kandang_id,
|
||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE;
|
||||
@@ -1,98 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS project_flocks_base_period_unique;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ADD COLUMN IF NOT EXISTS flock_id BIGINT;
|
||||
|
||||
WITH normalized AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||
CONCAT('Project Flock ', pf.id)
|
||||
) AS normalized_name,
|
||||
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
|
||||
FROM project_flocks pf
|
||||
),
|
||||
seed_flocks AS (
|
||||
SELECT DISTINCT
|
||||
n.normalized_name,
|
||||
MIN(n.created_by) AS created_by
|
||||
FROM normalized n
|
||||
GROUP BY n.normalized_name
|
||||
)
|
||||
INSERT INTO flocks (name, created_by, created_at, updated_at)
|
||||
SELECT sf.normalized_name, sf.created_by, NOW(), NOW()
|
||||
FROM seed_flocks sf
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH normalized AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||
CONCAT('Project Flock ', pf.id)
|
||||
) AS normalized_name
|
||||
FROM project_flocks pf
|
||||
),
|
||||
resolved AS (
|
||||
SELECT
|
||||
n.id,
|
||||
f.id AS flock_id
|
||||
FROM normalized n
|
||||
JOIN flocks f ON LOWER(f.name) = LOWER(n.normalized_name)
|
||||
)
|
||||
UPDATE project_flocks pf
|
||||
SET flock_id = resolved.flock_id
|
||||
FROM resolved
|
||||
WHERE pf.id = resolved.id;
|
||||
|
||||
WITH missing AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||
CONCAT('Project Flock ', pf.id)
|
||||
) AS normalized_name,
|
||||
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
|
||||
FROM project_flocks pf
|
||||
WHERE pf.flock_id IS NULL
|
||||
),
|
||||
seed_missing AS (
|
||||
SELECT DISTINCT normalized_name, created_by FROM missing
|
||||
)
|
||||
INSERT INTO flocks (name, created_by, created_at, updated_at)
|
||||
SELECT sm.normalized_name, sm.created_by, NOW(), NOW()
|
||||
FROM seed_missing sm
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH missing AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||
CONCAT('Project Flock ', pf.id)
|
||||
) AS normalized_name
|
||||
FROM project_flocks pf
|
||||
WHERE pf.flock_id IS NULL
|
||||
)
|
||||
UPDATE project_flocks pf
|
||||
SET flock_id = f.id
|
||||
FROM missing m
|
||||
JOIN flocks f ON LOWER(f.name) = LOWER(m.normalized_name)
|
||||
WHERE pf.id = m.id;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ALTER COLUMN flock_id SET NOT NULL;
|
||||
|
||||
DROP INDEX IF EXISTS project_flocks_flock_name_unique;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
DROP COLUMN IF EXISTS flock_name;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_period_unique
|
||||
ON project_flocks (flock_id, period)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,55 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ADD COLUMN IF NOT EXISTS flock_name VARCHAR(255);
|
||||
|
||||
WITH generated_names AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(f.name, CONCAT('Project Flock ', pf.id)) AS base_name,
|
||||
pf.period,
|
||||
ROW_NUMBER() OVER (PARTITION BY COALESCE(f.name, CONCAT('Project Flock ', pf.id)) ORDER BY pf.id) AS rn
|
||||
FROM project_flocks pf
|
||||
LEFT JOIN flocks f ON f.id = pf.flock_id
|
||||
)
|
||||
UPDATE project_flocks pf
|
||||
SET flock_name = CASE
|
||||
WHEN gn.period IS NOT NULL THEN
|
||||
CASE
|
||||
WHEN gn.rn = 1 THEN CONCAT(gn.base_name, ' ', gn.period)
|
||||
ELSE CONCAT(gn.base_name, ' ', gn.period, ' ', gn.rn)
|
||||
END
|
||||
ELSE
|
||||
CASE
|
||||
WHEN gn.rn = 1 THEN gn.base_name
|
||||
ELSE CONCAT(gn.base_name, ' ', gn.rn)
|
||||
END
|
||||
END
|
||||
FROM generated_names gn
|
||||
WHERE pf.id = gn.id
|
||||
AND (pf.flock_name IS NULL OR pf.flock_name = '');
|
||||
|
||||
UPDATE project_flocks
|
||||
SET flock_name = CONCAT('Project Flock ', id)
|
||||
WHERE flock_name IS NULL OR flock_name = '';
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ALTER COLUMN flock_name SET NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_name_unique
|
||||
ON project_flocks (flock_name)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique
|
||||
ON project_flocks (
|
||||
LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))),
|
||||
period
|
||||
)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
DROP COLUMN IF EXISTS flock_id;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,143 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
-- Drop newly introduced egg tables
|
||||
DROP TABLE IF EXISTS grading_eggs;
|
||||
DROP TABLE IF EXISTS recording_eggs;
|
||||
|
||||
-- Revert recording_stocks structure
|
||||
ALTER TABLE recording_stocks
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
DROP COLUMN IF EXISTS usage_qty,
|
||||
DROP COLUMN IF EXISTS pending_qty;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
ADD COLUMN increase NUMERIC(10,3),
|
||||
ADD COLUMN decrease NUMERIC(10,3),
|
||||
ADD COLUMN usage_amount BIGINT,
|
||||
ADD COLUMN notes VARCHAR;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
|
||||
(increase IS NULL OR increase >= 0) AND
|
||||
(decrease IS NULL OR decrease >= 0) AND
|
||||
(usage_amount IS NULL OR usage_amount >= 0)
|
||||
);
|
||||
|
||||
-- Revert recording_depletions structure
|
||||
ALTER TABLE recording_depletions
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_depl_qty;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ALTER COLUMN qty TYPE BIGINT USING COALESCE(qty, 0)::BIGINT;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
RENAME COLUMN qty TO total;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ADD COLUMN notes VARCHAR;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ADD CONSTRAINT chk_recording_depl_total CHECK (total >= 0);
|
||||
|
||||
-- Revert recording_bws structure
|
||||
ALTER TABLE recording_bws
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN qty TYPE INT USING COALESCE(qty, 0)::INT;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
DROP COLUMN IF EXISTS total_weight;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
RENAME COLUMN avg_weight TO weight;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ADD COLUMN notes VARCHAR;
|
||||
|
||||
UPDATE recording_bws
|
||||
SET qty = GREATEST(qty, 1);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (weight >= 0 AND qty >= 1);
|
||||
|
||||
-- Revert recordings header
|
||||
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
|
||||
|
||||
ALTER TABLE recordings
|
||||
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock_kandang,
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ALTER COLUMN total_depletion_qty TYPE INT USING COALESCE(total_depletion_qty, 0)::INT,
|
||||
ALTER COLUMN total_chick_qty TYPE BIGINT USING COALESCE(total_chick_qty, 0)::BIGINT;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN total_depletion_qty TO total_depletion;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN total_chick_qty TO total_chick;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD COLUMN record_date DATE,
|
||||
ADD COLUMN status INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN ontime INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN daily_depletion_rate NUMERIC(7,3),
|
||||
ADD COLUMN cum_depletion INT;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN project_flock_kandangs_id TO project_flock_id;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT fk_recordings_project_flock
|
||||
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id);
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT chk_recordings_status CHECK (status IN (0,1,2,3));
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT chk_recordings_ontime CHECK (ontime IN (0,1));
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT chk_recordings_nonnegatives CHECK (
|
||||
(total_depletion IS NULL OR total_depletion >= 0) AND
|
||||
(cum_depletion IS NULL OR cum_depletion >= 0) AND
|
||||
(total_chick IS NULL OR total_chick >= 0) AND
|
||||
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||
(fcr_value IS NULL OR fcr_value > 0) AND
|
||||
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
|
||||
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
|
||||
);
|
||||
|
||||
-- Ensure new columns carry derived data
|
||||
UPDATE recordings
|
||||
SET record_date = (record_datetime AT TIME ZONE 'Asia/Jakarta')::date
|
||||
WHERE record_date IS NULL;
|
||||
|
||||
-- Restore helper trigger/function and indexes
|
||||
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER recordings_set_record_date_trg
|
||||
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
|
||||
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
|
||||
|
||||
CREATE INDEX idx_recordings_flock_datetime
|
||||
ON recordings (project_flock_id, record_datetime);
|
||||
|
||||
CREATE UNIQUE INDEX uq_recordings_flock_record_date
|
||||
ON recordings (project_flock_id, record_date)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,168 +0,0 @@
|
||||
BEGIN;
|
||||
|
||||
-- Drop trigger & helper function tied to record_date before removing the column
|
||||
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
|
||||
DROP FUNCTION IF EXISTS trg_set_record_date();
|
||||
|
||||
-- Drop indexes and constraints that reference legacy columns
|
||||
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
|
||||
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
|
||||
|
||||
ALTER TABLE recordings
|
||||
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock,
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_status,
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_ontime,
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives;
|
||||
|
||||
-- Align recordings header with the new schema
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN project_flock_id TO project_flock_kandangs_id;
|
||||
|
||||
ALTER TABLE recordings
|
||||
DROP COLUMN IF EXISTS record_date,
|
||||
DROP COLUMN IF EXISTS status,
|
||||
DROP COLUMN IF EXISTS ontime,
|
||||
DROP COLUMN IF EXISTS daily_depletion_rate,
|
||||
DROP COLUMN IF EXISTS cum_depletion;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN total_depletion TO total_depletion_qty;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN total_chick TO total_chick_qty;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ALTER COLUMN total_depletion_qty TYPE NUMERIC(15,3) USING COALESCE(total_depletion_qty, 0)::NUMERIC(15,3),
|
||||
ALTER COLUMN total_chick_qty TYPE NUMERIC(15,3) USING COALESCE(total_chick_qty, 0)::NUMERIC(15,3),
|
||||
ALTER COLUMN cum_intake TYPE INT USING COALESCE(cum_intake, 0)::INT;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT fk_recordings_project_flock_kandang
|
||||
FOREIGN KEY (project_flock_kandangs_id) REFERENCES project_flock_kandangs(id);
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK (
|
||||
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
|
||||
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
|
||||
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||
(fcr_value IS NULL OR fcr_value >= 0) AND
|
||||
(total_chick_qty IS NULL OR total_chick_qty >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_recordings_flock_datetime
|
||||
ON recordings (project_flock_kandangs_id, record_datetime);
|
||||
|
||||
-- recording_bws reshape
|
||||
ALTER TABLE recording_bws
|
||||
RENAME COLUMN weight TO avg_weight;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ADD COLUMN total_weight NUMERIC(10,3);
|
||||
|
||||
UPDATE recording_bws
|
||||
SET total_weight = COALESCE(avg_weight, 0) * COALESCE(qty, 0);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN total_weight SET NOT NULL;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
DROP COLUMN IF EXISTS notes;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (
|
||||
avg_weight >= 0 AND qty >= 0 AND total_weight >= 0
|
||||
);
|
||||
|
||||
-- recording_depletions reshape
|
||||
ALTER TABLE recording_depletions
|
||||
RENAME COLUMN total TO qty;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
DROP COLUMN IF EXISTS notes;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_depl_total;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ADD CONSTRAINT chk_recording_depl_qty CHECK (qty >= 0);
|
||||
|
||||
-- recording_stocks reshape
|
||||
ALTER TABLE recording_stocks
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
DROP COLUMN IF EXISTS increase,
|
||||
DROP COLUMN IF EXISTS decrease,
|
||||
DROP COLUMN IF EXISTS usage_amount,
|
||||
DROP COLUMN IF EXISTS notes;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
ADD COLUMN usage_qty NUMERIC(15,3),
|
||||
ADD COLUMN pending_qty NUMERIC(15,3);
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
|
||||
(usage_qty IS NULL OR usage_qty >= 0) AND
|
||||
(pending_qty IS NULL OR pending_qty >= 0)
|
||||
);
|
||||
|
||||
-- recording_eggs table
|
||||
CREATE TABLE recording_eggs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_id BIGINT NOT NULL,
|
||||
product_warehouse_id BIGINT NOT NULL,
|
||||
qty INT NOT NULL,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT fk_recording_eggs_recording
|
||||
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_recording_eggs_product_warehouse
|
||||
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
|
||||
CONSTRAINT fk_recording_eggs_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||
CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_recording_eggs_recording
|
||||
ON recording_eggs (recording_id);
|
||||
|
||||
CREATE INDEX idx_recording_eggs_product
|
||||
ON recording_eggs (product_warehouse_id);
|
||||
|
||||
-- grading_eggs table
|
||||
CREATE TABLE grading_eggs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_egg_id BIGINT NOT NULL,
|
||||
qty NUMERIC(15,3) NOT NULL,
|
||||
grade VARCHAR,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT fk_grading_eggs_recording_egg
|
||||
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_grading_eggs_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_grading_eggs_recording_egg
|
||||
ON grading_eggs (recording_egg_id);
|
||||
|
||||
COMMIT;
|
||||
@@ -35,19 +35,6 @@ func Run(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
productCategories, err := seedProductCategories(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := seedFlocks(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := seedFcr(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kandangs, err := seedKandangs(tx, adminID, locations, users)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -57,6 +44,11 @@ func Run(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
productCategories, err := seedProductCategories(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
suppliers, err := seedSuppliers(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -66,6 +58,10 @@ func Run(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedFcr(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,13 +74,6 @@ func Run(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedProductWarehouse(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedTransferStock(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("✅ Master data seeding completed")
|
||||
return nil
|
||||
})
|
||||
@@ -201,47 +190,16 @@ func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[stri
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
names := []string{"Flock Priangan", "Flock Banten"}
|
||||
result := make(map[string]uint, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
var flock entity.Flock
|
||||
err := tx.Where("name = ?", name).First(&flock).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
flock = entity.Flock{
|
||||
Name: name,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&flock).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if err := tx.Model(&entity.Flock{}).Where("id = ?", flock.Id).Updates(map[string]any{
|
||||
"created_by": createdBy,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result[name] = flock.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Status utils.KandangStatus
|
||||
Location string
|
||||
PicKey string
|
||||
}{
|
||||
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
||||
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
||||
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
||||
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
||||
{"Singaparna 1", "Singaparna", "admin"},
|
||||
{"Singaparna 2", "Singaparna", "admin"},
|
||||
{"Cikaum 1", "Cikaum", "admin"},
|
||||
{"Cikaum 2", "Cikaum", "admin"},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
@@ -261,7 +219,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
kandang = entity.Kandang{
|
||||
Name: seed.Name,
|
||||
Status: string(seed.Status),
|
||||
LocationId: locID,
|
||||
PicId: picID,
|
||||
CreatedBy: createdBy,
|
||||
@@ -271,15 +228,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
updates := map[string]any{
|
||||
"location_id": locID,
|
||||
"pic_id": picID,
|
||||
"status": string(seed.Status),
|
||||
}
|
||||
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result[seed.Name] = kandang.Id
|
||||
}
|
||||
@@ -363,10 +311,8 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error)
|
||||
Name string
|
||||
Code string
|
||||
}{
|
||||
{"Pullet", "PLT"},
|
||||
{"Bahan Baku", "RAW"},
|
||||
{"Day Old Chick", "DOC"},
|
||||
{"Telur", "EGG"},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
@@ -480,7 +426,7 @@ func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
func seedFcr(tx *gorm.DB, createdBy uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Standards []struct {
|
||||
@@ -502,20 +448,17 @@ func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for _, seed := range seeds {
|
||||
var fcr entity.Fcr
|
||||
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
|
||||
if err := tx.Create(&fcr).Error; err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
result[seed.Name] = fcr.Id
|
||||
|
||||
for _, std := range seed.Standards {
|
||||
var standard entity.FcrStandard
|
||||
@@ -528,22 +471,22 @@ func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
Mortality: std.Mortality,
|
||||
}
|
||||
if err := tx.Create(&standard).Error; err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
} else {
|
||||
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
|
||||
"fcr_number": std.FcrNumber,
|
||||
"mortality": std.Mortality,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
|
||||
@@ -570,54 +513,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 Afkir",
|
||||
Brand: "-",
|
||||
Sku: "1",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
Name: "Ayam Mati",
|
||||
Brand: "-",
|
||||
Sku: "2",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
Name: "Ayam Culling",
|
||||
Brand: "-",
|
||||
Sku: "3",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
Name: "Telur Konsumsi Baik",
|
||||
Brand: "-",
|
||||
Sku: "4",
|
||||
Uom: "Unit",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
|
||||
},
|
||||
{
|
||||
Name: "Telur Pecah",
|
||||
Brand: "-",
|
||||
Sku: "5",
|
||||
Uom: "Unit",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
|
||||
},
|
||||
{
|
||||
Name: "281 SPECIAL STARTER",
|
||||
Brand: "281 STARTER",
|
||||
@@ -779,8 +674,6 @@ func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers
|
||||
return nil
|
||||
}
|
||||
|
||||
// nanti saya isi
|
||||
|
||||
func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error {
|
||||
if len(flags) == 0 {
|
||||
return nil
|
||||
@@ -867,139 +760,6 @@ func seedBanks(tx *gorm.DB, createdBy uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
||||
seeds := []struct {
|
||||
ProductName string
|
||||
WarehouseName string
|
||||
Quantity float64
|
||||
}{
|
||||
{ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 100},
|
||||
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
|
||||
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
|
||||
{ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
|
||||
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
|
||||
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
|
||||
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
|
||||
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
var product entity.Product
|
||||
if err := tx.Where("name = ?", seed.ProductName).First(&product).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("product %q not found for product warehouse seeding", seed.ProductName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var warehouse entity.Warehouse
|
||||
if err := tx.Where("name = ?", seed.WarehouseName).First(&warehouse).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("warehouse %q not found for product warehouse seeding", seed.WarehouseName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
err := tx.Where("product_id = ? AND warehouse_id = ?", product.Id, warehouse.Id).First(&productWarehouse).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
productWarehouse = entity.ProductWarehouse{
|
||||
ProductId: product.Id,
|
||||
WarehouseId: warehouse.Id,
|
||||
Quantity: seed.Quantity,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&productWarehouse).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := tx.Model(&productWarehouse).Updates(map[string]any{
|
||||
"quantity": seed.Quantity,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedTransferStock(tx *gorm.DB, createdBy uint) error {
|
||||
|
||||
transfer := entity.StockTransfer{
|
||||
FromWarehouseId: 1,
|
||||
ToWarehouseId: 2,
|
||||
Reason: "Seed transfer stock",
|
||||
TransferDate: time.Now(),
|
||||
MovementNumber: "SEED-TRF-00001",
|
||||
CreatedBy: 1,
|
||||
}
|
||||
if err := tx.Create(&transfer).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
details := []entity.StockTransferDetail{
|
||||
{
|
||||
StockTransferId: transfer.Id,
|
||||
ProductId: 1,
|
||||
Quantity: 10,
|
||||
},
|
||||
{
|
||||
StockTransferId: transfer.Id,
|
||||
ProductId: 2,
|
||||
Quantity: 5,
|
||||
},
|
||||
}
|
||||
for i := range details {
|
||||
if err := tx.Create(&details[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
deliveries := []entity.StockTransferDelivery{
|
||||
{
|
||||
StockTransferId: transfer.Id,
|
||||
SupplierId: 1,
|
||||
VehiclePlate: "B 1234 XYZ",
|
||||
DriverName: "Driver Seed",
|
||||
DocumentPath: "seed.pdf",
|
||||
ShippingCostItem: 1000,
|
||||
ShippingCostTotal: 2000,
|
||||
},
|
||||
}
|
||||
for i := range deliveries {
|
||||
if err := tx.Create(&deliveries[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
detailMap := make(map[uint64]uint64)
|
||||
for _, d := range details {
|
||||
detailMap[d.ProductId] = d.Id
|
||||
}
|
||||
|
||||
deliveryItems := []entity.StockTransferDeliveryItem{
|
||||
{
|
||||
StockTransferDeliveryId: deliveries[0].Id,
|
||||
StockTransferDetailId: detailMap[1],
|
||||
Quantity: 50,
|
||||
},
|
||||
{
|
||||
StockTransferDeliveryId: deliveries[0].Id,
|
||||
StockTransferDetailId: detailMap[2],
|
||||
Quantity: 30,
|
||||
},
|
||||
}
|
||||
for i := range deliveryItems {
|
||||
if err := tx.Create(&deliveryItems[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProjectChickinDetail struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProjectChickinId uint `gorm:"column:project_chickin_id;not null"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
Quantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||
CreatedBy uint `gorm:"column:created_by;not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
|
||||
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ApprovalAction string
|
||||
|
||||
const (
|
||||
ApprovalActionApproved ApprovalAction = "APPROVED"
|
||||
ApprovalActionRejected ApprovalAction = "REJECTED"
|
||||
ApprovalActionCreated ApprovalAction = "CREATED"
|
||||
ApprovalActionUpdated ApprovalAction = "UPDATED"
|
||||
)
|
||||
|
||||
type Approval struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ApprovableType string `gorm:"size:50;not null;index:approvals_approvable_lookup,priority:1"`
|
||||
ApprovableId uint `gorm:"not null;index:approvals_approvable_lookup,priority:2"`
|
||||
StepNumber uint16 `gorm:"not null"`
|
||||
StepName string `gorm:"not null"`
|
||||
Action *ApprovalAction `gorm:"type:VARCHAR(20)"`
|
||||
Notes *string `gorm:"type:text"`
|
||||
ActionAt time.Time `gorm:"autoCreateTime"`
|
||||
ActionBy *uint `gorm:"index"`
|
||||
|
||||
ActionUser *User `gorm:"foreignKey:ActionBy;references:Id"`
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Flock struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:flocks_name_unique,where:deleted_at IS NULL"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -7,17 +7,16 @@ import (
|
||||
)
|
||||
|
||||
type Kandang struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
||||
Status string `gorm:"type:varchar(50);not null"`
|
||||
LocationId uint `gorm:"not null"`
|
||||
PicId uint `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
||||
LocationId uint `gorm:"not null"`
|
||||
PicId uint `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductWarehouse struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
ProductId uint `gorm:"not null"`
|
||||
WarehouseId uint `gorm:"not null"`
|
||||
Quantity float64 `gorm:"default:0"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// Relations
|
||||
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const ()
|
||||
|
||||
type ProjectChickin struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
ChickInDate time.Time `gorm:"not null"`
|
||||
Quantity float64 `gorm:"not null"`
|
||||
Note string `gorm:"type:text"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProjectFlockPopulation struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
|
||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProjectFlock struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
FlockName string `gorm:"type:varchar(255);not null;uniqueIndex"`
|
||||
AreaId uint `gorm:"not null"`
|
||||
Category string `gorm:"type:varchar(20);not null"`
|
||||
FcrId uint `gorm:"not null"`
|
||||
LocationId uint `gorm:"not null"`
|
||||
Period int `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
||||
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
|
||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
||||
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
type ProjectFlockKandang struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
|
||||
|
||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Recording struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProjectFlockKandangId uint `gorm:"column:project_flock_kandangs_id;not null;index"`
|
||||
RecordDatetime time.Time `gorm:"column:record_datetime;not null"`
|
||||
Day *int `gorm:"column:day"`
|
||||
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
|
||||
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
|
||||
DailyGain *float64 `gorm:"column:daily_gain"`
|
||||
AvgDailyGain *float64 `gorm:"column:avg_daily_gain"`
|
||||
CumIntake *int `gorm:"column:cum_intake"`
|
||||
FcrValue *float64 `gorm:"column:fcr_value"`
|
||||
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
type RecordingBW struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
AvgWeight float64 `gorm:"column:avg_weight;not null"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
TotalWeight float64 `gorm:"column:total_weight;not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package entities
|
||||
|
||||
type RecordingDepletion struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
type RecordingEgg struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
Qty int `gorm:"column:qty;not null"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
}
|
||||
|
||||
type GradingEgg struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
Grade string `gorm:"column:grade;type:varchar(50)"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package entities
|
||||
|
||||
type RecordingStock struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
UsageQty *float64 `gorm:"column:usage_qty"`
|
||||
PendingQty *float64 `gorm:"column:pending_qty"`
|
||||
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
// HEADER
|
||||
type StockTransfer struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
MovementNumber string `gorm:"uniqueIndex;not null"`
|
||||
FromWarehouseId uint64
|
||||
ToWarehouseId uint64
|
||||
TransferDate time.Time
|
||||
Reason string
|
||||
CreatedBy uint64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
// Relations
|
||||
FromWarehouse *Warehouse `gorm:"foreignKey:FromWarehouseId"`
|
||||
ToWarehouse *Warehouse `gorm:"foreignKey:ToWarehouseId"`
|
||||
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
||||
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
LogTypeAdjustment = "ADJUSTMENT"
|
||||
LogTypeTransfer = "TRANSFER"
|
||||
)
|
||||
|
||||
const (
|
||||
TransactionTypeIncrease = "INCREASE"
|
||||
TransactionTypeDecrease = "DECREASE"
|
||||
)
|
||||
|
||||
type StockLog struct {
|
||||
Id uint `gorm:"primaryKey;column:id"`
|
||||
TransactionType string `gorm:"type:varchar(20);not null"`
|
||||
Quantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||
BeforeQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||
AfterQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||
LogType string `gorm:"type:varchar(50);not null;index:stock_logs_flaggable_lookup,priority:1"`
|
||||
LogId uint `gorm:"not null;index:stock_logs_flaggable_lookup,priority:2"`
|
||||
Note string `gorm:"type:text"`
|
||||
ProductWarehouseId uint `gorm:"not null;index"`
|
||||
CreatedBy uint `gorm:"index"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
// DETAIL EKSPEDISI
|
||||
type StockTransferDelivery struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
StockTransferId uint64
|
||||
SupplierId uint64
|
||||
VehiclePlate string
|
||||
DriverName string
|
||||
DocumentNumber string
|
||||
DocumentPath string
|
||||
ShippingCostItem float64
|
||||
ShippingCostTotal float64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
// Relations
|
||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
||||
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package entities
|
||||
|
||||
// PIVOT TABLE TRANSFER
|
||||
type StockTransferDeliveryItem struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
StockTransferDeliveryId uint64
|
||||
StockTransferDetailId uint64
|
||||
Quantity float64
|
||||
// Relations
|
||||
StockTransferDelivery *StockTransferDelivery `gorm:"foreignKey:StockTransferDeliveryId"`
|
||||
StockTransferDetail *StockTransferDetail `gorm:"foreignKey:StockTransferDetailId"`
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
// DETAIL PRODUK
|
||||
type StockTransferDetail struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
StockTransferId uint64
|
||||
ProductId uint64
|
||||
Quantity float64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
// Relations
|
||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||
Product *Product `gorm:"foreignKey:ProductId"`
|
||||
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
|
||||
}
|
||||
+48
-155
@@ -4,190 +4,83 @@ import (
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
authContextLocalsKey = "auth.context"
|
||||
authUserLocalsKey = "auth.user"
|
||||
)
|
||||
|
||||
// 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{}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
token := bearerToken(c)
|
||||
authHeader := c.Get("Authorization")
|
||||
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
||||
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName))
|
||||
cookieName := config.SSOAccessCookieName
|
||||
if cookieName == "" {
|
||||
cookieName = "access"
|
||||
}
|
||||
token = strings.TrimSpace(c.Cookies(cookieName))
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
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")
|
||||
}
|
||||
|
||||
if verification.UserID == 0 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint")
|
||||
}
|
||||
|
||||
if err := ensureNotRevoked(c, token, verification); err != nil {
|
||||
return err
|
||||
if len(config.SSOAllowedAudiences) > 0 {
|
||||
allowed := make(map[string]struct{}, len(config.SSOAllowedAudiences))
|
||||
for _, aud := range config.SSOAllowedAudiences {
|
||||
aud = strings.TrimSpace(aud)
|
||||
if aud != "" {
|
||||
allowed[aud] = struct{}{}
|
||||
}
|
||||
}
|
||||
audienceValid := false
|
||||
for _, aud := range verification.Claims.Audience {
|
||||
if _, ok := allowed[aud]; ok {
|
||||
audienceValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !audienceValid {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "invalid audience")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if len(requiredScopes) > 0 {
|
||||
if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) {
|
||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
|
||||
}
|
||||
}
|
||||
c.Locals("user", user)
|
||||
c.Locals("token_claims", verification.Claims)
|
||||
|
||||
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{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx := &AuthContext{
|
||||
Token: token,
|
||||
Verification: verification,
|
||||
User: user,
|
||||
Roles: roles,
|
||||
Permissions: permissions,
|
||||
}
|
||||
|
||||
c.Locals(authContextLocalsKey, ctx)
|
||||
c.Locals(authUserLocalsKey, user)
|
||||
// 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")
|
||||
// }
|
||||
// }
|
||||
|
||||
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
|
||||
}
|
||||
// func hasAllRights(userRights, requiredRights []string) bool {
|
||||
// rightSet := make(map[string]struct{}, len(userRights))
|
||||
// for _, right := range userRights {
|
||||
// rightSet[right] = struct{}{}
|
||||
// }
|
||||
|
||||
// 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
|
||||
}
|
||||
// for _, right := range requiredRights {
|
||||
// if _, exists := rightSet[right]; !exists {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
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))
|
||||
}
|
||||
@@ -16,10 +16,6 @@ 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()
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
)
|
||||
|
||||
type ApprovalController struct {
|
||||
ApprovalService common.ApprovalService
|
||||
}
|
||||
|
||||
func NewApprovalController(approvalService common.ApprovalService) *ApprovalController {
|
||||
return &ApprovalController{
|
||||
ApprovalService: approvalService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
|
||||
moduleName := strings.TrimSpace(c.Query("module_name", ""))
|
||||
if moduleName == "" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "`module_name` is required")
|
||||
}
|
||||
|
||||
moduleIDParam := strings.TrimSpace(c.Query("module_id", ""))
|
||||
var moduleID *uint
|
||||
if moduleIDParam != "" {
|
||||
value, err := strconv.ParseUint(moduleIDParam, 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "module_id must be a positive integer")
|
||||
}
|
||||
id := uint(value)
|
||||
moduleID = &id
|
||||
}
|
||||
|
||||
groupByStep := c.QueryBool("group_step_number", false)
|
||||
|
||||
page := c.QueryInt("page", 1)
|
||||
limit := c.QueryInt("limit", 10)
|
||||
search := strings.TrimSpace(c.Query("search", ""))
|
||||
|
||||
query := &validation.Query{
|
||||
ModuleName: moduleName,
|
||||
ModuleId: moduleID,
|
||||
GroupByStep: groupByStep,
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Search: search,
|
||||
}
|
||||
|
||||
records, totalResults, err := u.ApprovalService.List(
|
||||
c.Context(),
|
||||
query.ModuleName,
|
||||
query.ModuleId,
|
||||
query.Page,
|
||||
query.Limit,
|
||||
query.Search,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if query.GroupByStep {
|
||||
data := dto.ToApprovalGroupDTOs(records)
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.ApprovalGroupDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get All approvals successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
flat := dto.ToApprovalDTOs(records)
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.ApprovalBaseDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get All approvals successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: flat,
|
||||
})
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
)
|
||||
|
||||
type ApprovalBaseDTO struct {
|
||||
StepNumber uint16 `json:"step_number"`
|
||||
StepName string `json:"step_name"`
|
||||
Action *string `json:"action"`
|
||||
Notes *string `json:"notes"`
|
||||
ActionBy userDTO.UserBaseDTO `json:"action_by"`
|
||||
ActionAt time.Time `json:"action_at"`
|
||||
}
|
||||
|
||||
type ApprovalGroupDTO struct {
|
||||
StepNumber uint16 `json:"step_number"`
|
||||
StepName string `json:"step_name"`
|
||||
Approvals []ApprovalBaseDTO `json:"approvals"`
|
||||
}
|
||||
|
||||
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
||||
dto := ApprovalBaseDTO{
|
||||
Notes: e.Notes,
|
||||
}
|
||||
|
||||
if e.StepNumber > 0 {
|
||||
stepCopy := uint16(e.StepNumber)
|
||||
dto.StepNumber = stepCopy
|
||||
}
|
||||
|
||||
stepName := strings.TrimSpace(e.StepName)
|
||||
if stepName == "" && e.ApprovableType != "" && e.StepNumber > 0 {
|
||||
if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(e.ApprovableType), approvalutils.ApprovalStep(e.StepNumber)); ok {
|
||||
stepName = label
|
||||
}
|
||||
}
|
||||
dto.StepName = stepName
|
||||
|
||||
if e.Action != nil {
|
||||
value := strings.TrimSpace(string(*e.Action))
|
||||
if value != "" {
|
||||
valueCopy := value
|
||||
dto.Action = &valueCopy
|
||||
}
|
||||
}
|
||||
|
||||
if e.ActionUser != nil && e.ActionUser.Id != 0 {
|
||||
user := userDTO.ToUserBaseDTO(*e.ActionUser)
|
||||
dto.ActionBy = user
|
||||
} else if e.ActionBy != nil && *e.ActionBy != 0 {
|
||||
dto.ActionBy = userDTO.UserBaseDTO{
|
||||
Id: *e.ActionBy,
|
||||
IdUser: int64(*e.ActionBy),
|
||||
}
|
||||
}
|
||||
|
||||
if !e.ActionAt.IsZero() {
|
||||
at := e.ActionAt
|
||||
dto.ActionAt = at
|
||||
}
|
||||
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToApprovalDTOs(items []entity.Approval) []ApprovalBaseDTO {
|
||||
result := make([]ApprovalBaseDTO, len(items))
|
||||
for i, item := range items {
|
||||
result[i] = ToApprovalDTO(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToApprovalGroupDTOs(items []entity.Approval) []ApprovalGroupDTO {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type groupAccumulator struct {
|
||||
StepName string
|
||||
Approvals []ApprovalBaseDTO
|
||||
}
|
||||
|
||||
groups := make(map[uint16]*groupAccumulator)
|
||||
order := make([]uint16, 0)
|
||||
for _, item := range items {
|
||||
step := item.StepNumber
|
||||
acc, exists := groups[step]
|
||||
if !exists {
|
||||
stepName := strings.TrimSpace(item.StepName)
|
||||
if stepName == "" && item.ApprovableType != "" && item.StepNumber > 0 {
|
||||
if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(item.ApprovableType), approvalutils.ApprovalStep(item.StepNumber)); ok {
|
||||
stepName = label
|
||||
}
|
||||
}
|
||||
acc = &groupAccumulator{StepName: stepName}
|
||||
groups[step] = acc
|
||||
order = append(order, step)
|
||||
}
|
||||
acc.Approvals = append(acc.Approvals, ToApprovalDTO(item))
|
||||
}
|
||||
|
||||
sort.Slice(order, func(i, j int) bool { return order[i] < order[j] })
|
||||
|
||||
result := make([]ApprovalGroupDTO, len(order))
|
||||
for i, step := range order {
|
||||
acc := groups[step]
|
||||
result[i] = ApprovalGroupDTO{
|
||||
StepNumber: step,
|
||||
StepName: acc.StepName,
|
||||
Approvals: acc.Approvals,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package approvals
|
||||
|
||||
import (
|
||||
"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"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type ApprovalModule struct{}
|
||||
|
||||
func (ApprovalModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ApprovalRoutes(router, userService, approvalService)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package approvals
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
|
||||
_ = u
|
||||
ctrl := controller.NewApprovalController(s)
|
||||
|
||||
route := v1.Group("/approvals")
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package validation
|
||||
|
||||
type Query struct {
|
||||
ModuleName string `json:"module_name" validate:"required_strict"`
|
||||
ModuleId *uint `json:"module_id,omitempty" validate:"omitempty,gt=0"`
|
||||
GroupByStep bool `json:"group_by_step"`
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -30,50 +26,6 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
|
||||
for f := range utils.AllFlagTypes() {
|
||||
flagList = append(flagList, string(f))
|
||||
}
|
||||
sort.Strings(flagList)
|
||||
|
||||
type approvalStepConstant struct {
|
||||
StepNumber uint16 `json:"step_number"`
|
||||
StepName string `json:"step_name"`
|
||||
}
|
||||
|
||||
workflowConstants := approvalutils.WorkflowConstants()
|
||||
workflowKeys := make([]string, 0, len(workflowConstants))
|
||||
for key := range workflowConstants {
|
||||
workflowKeys = append(workflowKeys, key)
|
||||
}
|
||||
sort.Strings(workflowKeys)
|
||||
|
||||
approvalWorkflows := make([]map[string]interface{}, 0, len(workflowKeys))
|
||||
for _, key := range workflowKeys {
|
||||
stepMap := workflowConstants[key]
|
||||
if len(stepMap) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
stepList := make([]approvalStepConstant, 0, len(stepMap))
|
||||
for stepStr, label := range stepMap {
|
||||
stepNum, err := strconv.ParseUint(stepStr, 10, 16)
|
||||
if err != nil || stepNum == 0 {
|
||||
continue
|
||||
}
|
||||
stepList = append(stepList, approvalStepConstant{
|
||||
StepNumber: uint16(stepNum),
|
||||
StepName: label,
|
||||
})
|
||||
}
|
||||
if len(stepList) == 0 {
|
||||
continue
|
||||
}
|
||||
sort.Slice(stepList, func(i, j int) bool {
|
||||
return stepList[i].StepNumber < stepList[j].StepNumber
|
||||
})
|
||||
|
||||
approvalWorkflows = append(approvalWorkflows, map[string]interface{}{
|
||||
"key": key,
|
||||
"steps": stepList,
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"flags": flagList,
|
||||
@@ -90,6 +42,5 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
|
||||
"BISNIS",
|
||||
"INDIVIDUAL",
|
||||
},
|
||||
"approval_workflows": approvalWorkflows,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type AdjustmentController struct {
|
||||
AdjustmentService service.AdjustmentService
|
||||
}
|
||||
|
||||
func NewAdjustmentController(adjustmentService service.AdjustmentService) *AdjustmentController {
|
||||
return &AdjustmentController{
|
||||
AdjustmentService: adjustmentService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *AdjustmentController) Adjustment(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
stockLog, err := u.AdjustmentService.Adjustment(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adjustmentDTO := dto.ToAdjustmentDetailDTO(stockLog)
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create adjustment successfully",
|
||||
Data: adjustmentDTO,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *AdjustmentController) AdjustmentHistory(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
ProductID: uint(c.QueryInt("product_id", 0)),
|
||||
WarehouseID: uint(c.QueryInt("warehouse_id", 0)),
|
||||
TransactionType: c.Query("transaction_type", ""),
|
||||
}
|
||||
|
||||
result, totalResults, err := u.AdjustmentService.AdjustmentHistory(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert to DTOs
|
||||
adjustmentDTOs := make([]dto.AdjustmentDetailDTO, len(result))
|
||||
for i, stockLog := range result {
|
||||
adjustmentDTOs[i] = dto.ToAdjustmentDetailDTO(stockLog)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.AdjustmentDetailDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get adjustment history successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: adjustmentDTOs,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *AdjustmentController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
stockLog, err := u.AdjustmentService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Use DTO for response
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get adjustment successfully",
|
||||
Data: dto.ToAdjustmentDetailDTO(stockLog),
|
||||
})
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type ProductBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
SKU string `json:"sku"`
|
||||
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
|
||||
}
|
||||
|
||||
type WarehouseBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ProductWarehouseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProductId uint `json:"product_id"`
|
||||
WarehouseId uint `json:"warehouse_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Product *ProductBaseDTO `json:"product,omitempty"`
|
||||
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||
}
|
||||
|
||||
type AdjustmentBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
TransactionType string `json:"transaction_type"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
BeforeQuantity float64 `json:"before_quantity"`
|
||||
AfterQuantity float64 `json:"after_quantity"`
|
||||
Note string `json:"note,omitempty"`
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
type AdjustmentListDTO struct {
|
||||
AdjustmentBaseDTO
|
||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type AdjustmentDetailDTO struct {
|
||||
AdjustmentListDTO
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
sku := ""
|
||||
if e.Sku != nil {
|
||||
sku = *e.Sku
|
||||
}
|
||||
|
||||
var category *productCategoryDTO.ProductCategoryBaseDTO
|
||||
if e.ProductCategory.Id != 0 {
|
||||
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
|
||||
category = &mapped
|
||||
}
|
||||
|
||||
return &ProductBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
SKU: sku,
|
||||
ProductCategory: category,
|
||||
}
|
||||
}
|
||||
|
||||
func ToWarehouseBaseDTO(e *entity.Warehouse) *WarehouseBaseDTO {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return &WarehouseBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return &ProductWarehouseDTO{
|
||||
Id: e.Id,
|
||||
ProductId: e.ProductId,
|
||||
WarehouseId: e.WarehouseId,
|
||||
Quantity: e.Quantity,
|
||||
Product: ToProductBaseDTO(&e.Product),
|
||||
Warehouse: ToWarehouseBaseDTO(&e.Warehouse),
|
||||
}
|
||||
}
|
||||
|
||||
func ToAdjustmentBaseDTO(e *entity.StockLog) AdjustmentBaseDTO {
|
||||
return AdjustmentBaseDTO{
|
||||
Id: e.Id,
|
||||
TransactionType: e.TransactionType,
|
||||
Quantity: e.Quantity,
|
||||
BeforeQuantity: e.BeforeQuantity,
|
||||
AfterQuantity: e.AfterQuantity,
|
||||
Note: e.Note,
|
||||
ProductWarehouseId: e.ProductWarehouseId,
|
||||
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
||||
}
|
||||
}
|
||||
|
||||
func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
|
||||
var createdUser *userDTO.UserBaseDTO
|
||||
if e.CreatedUser != nil {
|
||||
createdUser = &userDTO.UserBaseDTO{
|
||||
Id: e.CreatedUser.Id,
|
||||
IdUser: e.CreatedUser.IdUser,
|
||||
Email: e.CreatedUser.Email,
|
||||
Name: e.CreatedUser.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return AdjustmentListDTO{
|
||||
AdjustmentBaseDTO: ToAdjustmentBaseDTO(e),
|
||||
CreatedUser: createdUser,
|
||||
CreatedAt: e.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
|
||||
return AdjustmentDetailDTO{
|
||||
AdjustmentListDTO: ToAdjustmentListDTO(e),
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package adjustments
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type AdjustmentModule struct{}
|
||||
|
||||
func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
productRepo := rproduct.NewProductRepository(db)
|
||||
|
||||
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
AdjustmentRoutes(router, userService, adjustmentService)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package adjustments
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers"
|
||||
adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.AdjustmentService) {
|
||||
ctrl := controller.NewAdjustmentController(s)
|
||||
|
||||
route := v1.Group("/adjustments")
|
||||
|
||||
// Standard CRUD routes following master data pattern
|
||||
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
|
||||
route.Post("/", ctrl.Adjustment) // Create adjustment
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
|
||||
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AdjustmentService interface {
|
||||
Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.StockLog, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockLog, error)
|
||||
AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error)
|
||||
}
|
||||
|
||||
type adjustmentService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
StockLogsRepository stockLogsRepo.StockLogRepository
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
||||
ProductRepo productRepo.ProductRepository
|
||||
}
|
||||
|
||||
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService {
|
||||
return &adjustmentService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
StockLogsRepository: stockLogsRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProductRepo: productRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("ProductWarehouse").
|
||||
Preload("ProductWarehouse.Product").
|
||||
Preload("ProductWarehouse.Warehouse").
|
||||
Preload("CreatedUser")
|
||||
}
|
||||
|
||||
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) {
|
||||
stockLog, err := s.StockLogsRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return s.withRelations(db).Preload("ProductWarehouse.Product.ProductCategory")
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get adjustment by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stockLog.LogType != entity.LogTypeAdjustment {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
||||
}
|
||||
|
||||
return stockLog, nil
|
||||
}
|
||||
|
||||
func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*entity.StockLog, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := c.Context()
|
||||
|
||||
if err := common.EnsureRelations(c.Context(),
|
||||
common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists},
|
||||
common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Quantity <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
|
||||
}
|
||||
transactionType := strings.ToUpper(req.TransactionType)
|
||||
if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
|
||||
}
|
||||
|
||||
var createdLogId uint
|
||||
|
||||
isProductWarehouseExist, err := s.ProductWarehouseRepo.ProductWarehouseExistByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check product warehouse existence: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||
}
|
||||
if !isProductWarehouseExist {
|
||||
|
||||
newPW := &entity.ProductWarehouse{
|
||||
ProductId: uint(req.ProductID),
|
||||
WarehouseId: uint(req.WarehouseID),
|
||||
Quantity: 0,
|
||||
CreatedBy: 1, // TODO: should Get from auth middleware
|
||||
}
|
||||
|
||||
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create product warehouse: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
|
||||
}
|
||||
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
||||
}
|
||||
|
||||
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get product warehouse: %+v", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
|
||||
afterQuantity := productWarehouse.Quantity
|
||||
if transactionType == entity.TransactionTypeIncrease {
|
||||
afterQuantity += req.Quantity
|
||||
} else {
|
||||
if productWarehouse.Quantity < req.Quantity {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
|
||||
}
|
||||
afterQuantity -= req.Quantity
|
||||
}
|
||||
|
||||
newLog := &entity.StockLog{
|
||||
TransactionType: transactionType,
|
||||
Quantity: req.Quantity,
|
||||
BeforeQuantity: productWarehouse.Quantity,
|
||||
AfterQuantity: afterQuantity,
|
||||
LogType: entity.LogTypeAdjustment,
|
||||
LogId: 0,
|
||||
Note: req.Note,
|
||||
ProductWarehouseId: productWarehouse.Id,
|
||||
CreatedBy: 1, // TODO: should Get from auth middleware
|
||||
}
|
||||
|
||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock log: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
productWarehouse.Quantity = afterQuantity
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
createdLogId = newLog.Id
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process adjustment transaction")
|
||||
}
|
||||
|
||||
return s.GetOne(c, createdLogId)
|
||||
}
|
||||
|
||||
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
|
||||
if err := s.Validate.Struct(query); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
offset := (query.Page - 1) * query.Limit
|
||||
|
||||
isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
||||
}
|
||||
if query.WarehouseID > 0 && !isWarehousesExist {
|
||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
|
||||
}
|
||||
|
||||
isProductsExist, err := s.ProductRepo.IdExists(c.Context(), uint(query.ProductID))
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check product existence: %+v", err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product")
|
||||
}
|
||||
if query.ProductID > 0 && !isProductsExist {
|
||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
|
||||
}
|
||||
|
||||
stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
|
||||
db = s.withRelations(db)
|
||||
|
||||
db = db.Where("log_type = ?", entity.LogTypeAdjustment)
|
||||
|
||||
if query.TransactionType != "" {
|
||||
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||
}
|
||||
db = s.StockLogsRepository.ApplyProductWarehouseFilters(db, uint(query.ProductID), uint(query.WarehouseID))
|
||||
|
||||
return db.Order("created_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get adjustments: %+v", err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history")
|
||||
}
|
||||
|
||||
result := make([]*entity.StockLog, len(stockLogs))
|
||||
for i, v := range stockLogs {
|
||||
result[i] = &v
|
||||
}
|
||||
|
||||
return result, total, nil
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
ProductID uint `json:"product_id" validate:"required"`
|
||||
WarehouseID uint `json:"warehouse_id" validate:"required"`
|
||||
TransactionType string `json:"transaction_type" validate:"required,oneof=increase decrease"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
Note string `json:"note" validate:"omitempty,max=255"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100"`
|
||||
ProductID uint `query:"product_id" validate:"omitempty,min=0"`
|
||||
WarehouseID uint `query:"warehouse_id" validate:"omitempty,min=0"`
|
||||
TransactionType string `query:"transaction_type" validate:"omitempty,oneof=increase decrease"`
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type InventoryModule struct{}
|
||||
|
||||
func (InventoryModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
RegisterRoutes(router, db, validate)
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ProductWarehouseController struct {
|
||||
ProductWarehouseService service.ProductWarehouseService
|
||||
}
|
||||
|
||||
func NewProductWarehouseController(productWarehouseService service.ProductWarehouseService) *ProductWarehouseController {
|
||||
return &ProductWarehouseController{
|
||||
ProductWarehouseService: productWarehouseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
ProductId: uint(c.QueryInt("product_id", 0)),
|
||||
WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
|
||||
Flags: c.Query("flags", ""),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
result, totalResults, err := u.ProductWarehouseService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.ProductWarehouseListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all productWarehouses successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToProductWarehouseListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ProductWarehouseController) 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.ProductWarehouseService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get productWarehouse successfully",
|
||||
Data: dto.ToProductWarehouseListDTO(*result),
|
||||
})
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type ProductWarehouseBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProductId uint `json:"product_id"`
|
||||
WarehouseId uint `json:"warehouse_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
type ProductWarehouseListDTO struct {
|
||||
ProductWarehouseBaseDTO
|
||||
Product *ProductBaseDTO `json:"product,omitempty"`
|
||||
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||
CreatedUser *UserBaseDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type ProductWarehouseDetailDTO struct {
|
||||
ProductWarehouseListDTO
|
||||
}
|
||||
|
||||
// Nested DTOs for relations
|
||||
type ProductBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Sku string `json:"sku"`
|
||||
Flags []string `json:"flags"`
|
||||
}
|
||||
|
||||
type WarehouseBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Kandang *KandangBaseDTO `json:"kandang,omitempty"`
|
||||
Location *LocationBaseDTO `json:"location,omitempty"`
|
||||
Area *AreaBaseDTO `json:"area,omitempty"`
|
||||
}
|
||||
|
||||
type KandangBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type LocationBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AreaBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDTO {
|
||||
return ProductWarehouseBaseDTO{
|
||||
Id: e.Id,
|
||||
ProductId: e.ProductId, // Field yang benar dari entity
|
||||
WarehouseId: e.WarehouseId, // Field yang benar dari entity
|
||||
Quantity: e.Quantity,
|
||||
}
|
||||
}
|
||||
|
||||
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
|
||||
dto := ProductWarehouseListDTO{
|
||||
ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(e),
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
|
||||
// Map Product relation jika ada
|
||||
if e.Product.Id != 0 {
|
||||
product := ProductBaseDTO{
|
||||
Id: e.Product.Id,
|
||||
Name: e.Product.Name,
|
||||
}
|
||||
if e.Product.Sku != nil {
|
||||
product.Sku = *e.Product.Sku
|
||||
}
|
||||
if len(e.Product.Flags) > 0 {
|
||||
for _, f := range e.Product.Flags {
|
||||
product.Flags = append(product.Flags, f.Name)
|
||||
}
|
||||
}
|
||||
dto.Product = &product
|
||||
}
|
||||
|
||||
// Map Warehouse relation jika ada
|
||||
if e.Warehouse.Id != 0 {
|
||||
warehouse := WarehouseBaseDTO{
|
||||
Id: e.Warehouse.Id,
|
||||
Name: e.Warehouse.Name,
|
||||
}
|
||||
// Map Kandang jika ada
|
||||
if e.Warehouse.Kandang != nil && e.Warehouse.Kandang.Id != 0 {
|
||||
warehouse.Kandang = &KandangBaseDTO{
|
||||
Id: e.Warehouse.Kandang.Id,
|
||||
Name: e.Warehouse.Kandang.Name,
|
||||
}
|
||||
}
|
||||
// Map Location jika ada
|
||||
if e.Warehouse.Location != nil && e.Warehouse.Location.Id != 0 {
|
||||
warehouse.Location = &LocationBaseDTO{
|
||||
Id: e.Warehouse.Location.Id,
|
||||
Name: e.Warehouse.Location.Name,
|
||||
}
|
||||
}
|
||||
|
||||
if &e.Warehouse.Area != nil && e.Warehouse.Area.Id != 0 {
|
||||
warehouse.Area = &AreaBaseDTO{
|
||||
Id: e.Warehouse.Area.Id,
|
||||
Name: e.Warehouse.Area.Name,
|
||||
}
|
||||
}
|
||||
|
||||
dto.Warehouse = &warehouse
|
||||
}
|
||||
|
||||
// Map CreatedUser relation jika ada
|
||||
if e.CreatedUser.Id != 0 {
|
||||
user := UserBaseDTO{
|
||||
Id: e.CreatedUser.Id,
|
||||
Username: e.CreatedUser.Name,
|
||||
}
|
||||
dto.CreatedUser = &user
|
||||
}
|
||||
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToProductWarehouseListDTOs(e []entity.ProductWarehouse) []ProductWarehouseListDTO {
|
||||
result := make([]ProductWarehouseListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToProductWarehouseListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToProductWarehouseDetailDTO(e entity.ProductWarehouse) ProductWarehouseDetailDTO {
|
||||
return ProductWarehouseDetailDTO{
|
||||
ProductWarehouseListDTO: ToProductWarehouseListDTO(e),
|
||||
}
|
||||
}
|
||||
|
||||
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
|
||||
return KandangBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func ToLocationBaseDTO(e entity.Location) LocationBaseDTO {
|
||||
return LocationBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func ToAreaBaseDTO(e entity.Area) AreaBaseDTO {
|
||||
return AreaBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package productWarehouses
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
sProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type ProductWarehouseModule struct{}
|
||||
|
||||
func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ProductWarehouseRoutes(router, userService, productWarehouseService)
|
||||
}
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductWarehouseRepository interface {
|
||||
repository.BaseRepository[entity.ProductWarehouse]
|
||||
ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error)
|
||||
IsProductExist(ctx context.Context, productId uint) (bool, error)
|
||||
IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error)
|
||||
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
|
||||
ExistsByID(ctx context.Context, id uint) (bool, error)
|
||||
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
|
||||
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
|
||||
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
||||
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
||||
}
|
||||
|
||||
type ProductWarehouseRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.ProductWarehouse]
|
||||
}
|
||||
|
||||
func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
|
||||
return &ProductWarehouseRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
|
||||
return repository.Exists[entity.Product](ctx, r.DB(), productId)
|
||||
}
|
||||
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
|
||||
return repository.Exists[entity.Warehouse](ctx, r.DB(), warehouseId)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
|
||||
var count int64
|
||||
query := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId)
|
||||
if excludeID != nil {
|
||||
query = query.Where("id != ?", *excludeID)
|
||||
}
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
|
||||
var count int64
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProductWarehouse{}).
|
||||
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
if err := r.DB().WithContext(ctx).Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).First(&productWarehouse).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
||||
var productWarehouses []entity.ProductWarehouse
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("product_warehouses").
|
||||
Select("product_warehouses.*").
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
||||
Order("product_warehouses.created_at DESC").
|
||||
Find(&productWarehouses).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return productWarehouses, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
query := r.DB()
|
||||
if db != nil {
|
||||
query = db
|
||||
}
|
||||
fmt.Println(warehouseId)
|
||||
err := query.WithContext(ctx).
|
||||
Table("product_warehouses").
|
||||
Select("product_warehouses.*").
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
||||
Order("product_warehouses.created_at DESC").
|
||||
First(&productWarehouse).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB {
|
||||
if len(flags) == 0 {
|
||||
return db
|
||||
}
|
||||
|
||||
return db.
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
|
||||
Where("flags.name IN ?", flags)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error {
|
||||
if len(deltas) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
base := r.DB().WithContext(ctx)
|
||||
if modifier != nil {
|
||||
base = modifier(base)
|
||||
}
|
||||
|
||||
for id, delta := range deltas {
|
||||
if delta == 0 {
|
||||
continue
|
||||
}
|
||||
if err := base.Model(&entity.ProductWarehouse{}).
|
||||
Where("id = ?", id).
|
||||
Update("quantity", gorm.Expr("COALESCE(quantity,0) + ?", delta)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user