mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5650253307 | |||
| 79bbe61dab | |||
| fa5609c183 | |||
| 966d616022 | |||
| 53c321c3e3 | |||
| 91ad7ad5e0 | |||
| 79c754312e | |||
| f3b14cb8f2 | |||
| 886446b55f | |||
| dbeb0b62cb | |||
| 240496584f | |||
| c02f72c5e5 | |||
| 99688c8e11 | |||
| 1ceda3623e | |||
| 2e2aed67b8 | |||
| 1fc750efd3 | |||
| a801081a99 | |||
| b0dfa717d5 | |||
| 16d562e024 | |||
| 8881be2a22 | |||
| 3fc330d8f7 | |||
| af147f4f2b | |||
| 6768092e3b | |||
| 53b226f243 | |||
| cd752f19f4 | |||
| 5a73ad0164 | |||
| b8d1268dfa | |||
| da10861fd2 | |||
| 228aedc215 | |||
| b4b860b9d4 | |||
| 3080a6f8ef | |||
| b502751b4e | |||
| 4c7e5b0731 | |||
| 105b20c333 | |||
| f5b7fd60ad | |||
| ced27e23a0 | |||
| 242ccc9230 | |||
| 1e52c51987 | |||
| bf8519df3f | |||
| a57ef82ebb | |||
| c2b60c1aff | |||
| 320f5e65c6 | |||
| 28c81aac25 | |||
| 1dac74e25b | |||
| 9ca9dfc2be | |||
| 02cc082d67 | |||
| 5c25c84f7f | |||
| aaf129622b | |||
| 69469edb62 | |||
| 09d503f5be | |||
| d528096d56 | |||
| 0708628b78 | |||
| cb1df12b7e | |||
| 1156b376fc | |||
| 11f2389ec5 | |||
| 60757237c0 | |||
| 7905bdb0d7 | |||
| 26f9196876 | |||
| 17d3042586 | |||
| 903b114315 | |||
| 2f5fab9f80 | |||
| 74ec25db5b | |||
| 0a0c3f869b | |||
| 762dfa9fb9 | |||
| 6b5d27ae8e | |||
| fd0943dfaf | |||
| 80c84210b8 | |||
| 05ec64b456 | |||
| 9e97b3951c | |||
| b2ed58c734 | |||
| 3785d52925 | |||
| 4c279baad7 | |||
| 6e69e97d26 | |||
| ba12320d12 | |||
| d21aaead7b | |||
| 954cccd564 | |||
| 663d5129bb | |||
| e54b2157c7 | |||
| 95dad52cea | |||
| 28dcae5865 | |||
| 4129c36f9e | |||
| d587a793fe | |||
| a587584156 | |||
| 4b69afe4fa | |||
| 5cfa97dd03 | |||
| 028d5f6f91 | |||
| 60fe553f63 | |||
| 1c99093ff8 | |||
| 54cb1cf3da | |||
| a0569302c8 | |||
| 8f74391f1e | |||
| 5a2f99196f | |||
| 91fbbf5dd9 | |||
| ca168928c7 | |||
| 4d2a9bd7b4 | |||
| 4c4be2ef41 | |||
| a22c615ac1 | |||
| 4aed480662 | |||
| e5b91161a9 | |||
| a38491fef1 | |||
| b234778634 | |||
| 59e71856ac | |||
| 1ee97b91a5 | |||
| 3a5c49c511 | |||
| 48730e1b74 | |||
| f97d404121 | |||
| 3ecf39814e | |||
| 8220e34302 | |||
| c72db5bd18 | |||
| 86f37a89c1 | |||
| 20f1be2ef8 | |||
| 672c76d26d | |||
| 219a6a39ed | |||
| c91d84b652 | |||
| bf14ab7865 | |||
| b459245c5c | |||
| 31bb28f7da | |||
| a390d1d23a | |||
| c4448594e2 | |||
| fb831208f4 |
@@ -3,13 +3,9 @@ root = "."
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
# Build binary utama
|
cmd = "go build -o ./tmp/main ./cmd/api"
|
||||||
cmd = "go build -o /lti-api/tmp/main ./cmd/api"
|
bin = "tmp/main"
|
||||||
# Lokasi binary hasil build
|
full_bin = "APP_ENV=dev ./tmp/main"
|
||||||
bin = "/lti-api/tmp/main"
|
|
||||||
# Jalankan binary langsung dengan environment dev
|
|
||||||
full_bin = "APP_ENV=dev /lti-api/tmp/main"
|
|
||||||
# File yang dipantau oleh Air
|
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
exclude_dir = ["vendor", "tmp"]
|
exclude_dir = ["vendor", "tmp"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
# server configuration
|
|
||||||
# Env value : prod || dev
|
|
||||||
VERSION=0.0.1
|
|
||||||
APP_ENV=dev
|
|
||||||
APP_HOST=0.0.0.0
|
|
||||||
APP_PORT=8081
|
|
||||||
APP_URL=http://localhost:8081
|
|
||||||
|
|
||||||
# database configuration
|
|
||||||
DB_HOST=postgresdb
|
|
||||||
DB_USER=postgres
|
|
||||||
DB_PASSWORD=changeme
|
|
||||||
DB_NAME=db_lti_erp
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_PORT_HOST=5542
|
|
||||||
|
|
||||||
# JWT
|
|
||||||
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
|
|
||||||
|
|
||||||
# CORS
|
|
||||||
CORS_ALLOW_ORIGINS=changeme
|
|
||||||
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
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
REDIS_URL=redis://redis:6379/0
|
|
||||||
REDIS_PORT_HOST=6381
|
|
||||||
|
|
||||||
# SSO Integration
|
|
||||||
SSO_ISSUER=http://localhost:8080/api
|
|
||||||
# SSO_JWKS_URL=http://localhost:8080/api/.well-known/jwks.json
|
|
||||||
SSO_JWKS_URL=http://host.docker.internal:8080/api/.well-known/jwks.json
|
|
||||||
SSO_ALLOWED_AUDIENCES=client:lti-api
|
|
||||||
SSO_AUTHORIZE_URL=http://localhost:8080/sso/authorize
|
|
||||||
SSO_TOKEN_URL=http://localhost:8080/sso/token
|
|
||||||
SSO_GETME_URL=http://localhost:8080/api/auth/get-me
|
|
||||||
SSO_ACCESS_COOKIE_NAME=sso_access
|
|
||||||
SSO_REFRESH_COOKIE_NAME=sso_refresh
|
|
||||||
SSO_COOKIE_DOMAIN=
|
|
||||||
SSO_COOKIE_SECURE=false
|
|
||||||
SSO_COOKIE_SAMESITE=Lax
|
|
||||||
SSO_TOKEN_BLACKLIST_PREFIX=sso:blacklist
|
|
||||||
SSO_PKCE_TTL_SECONDS=300
|
|
||||||
# Security window and payload limits for SSO user sync webhook
|
|
||||||
SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS=120
|
|
||||||
SSO_USER_SYNC_NONCE_TTL_SECONDS=600
|
|
||||||
SSO_USER_SYNC_MAX_BODY_BYTES=32768
|
|
||||||
# Example JSON (single-line) of client configs (each client requires a unique sync_secret)
|
|
||||||
SSO_CLIENTS={"LTI":{"public_id":"Lumbung-Telur-Indonesia","redirect_uri":"http://localhost:8081/api/sso/callback","scope":"openid profile","default_return_uri":"http://localhost:3000","allowed_return_origins":["http://localhost:3000"],"sync_secret":"onUyfODIMHOh4TgGLgyWLmsNeVNxFRHqoLJFLPjr"}}
|
|
||||||
@@ -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"}}
|
|
||||||
+1
-1
@@ -16,7 +16,7 @@ docker-compose.yaml
|
|||||||
Dockerfile.local
|
Dockerfile.local
|
||||||
# Go build cache
|
# Go build cache
|
||||||
.gocache/
|
.gocache/
|
||||||
vendor/
|
vendor
|
||||||
|
|
||||||
# Logs & reports
|
# Logs & reports
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
stages:
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
deploy-dev:
|
||||||
|
stage: deploy
|
||||||
|
image: alpine:3.20
|
||||||
|
variables:
|
||||||
|
DEPLOY_APP: "LTI-MBUGROUP"
|
||||||
|
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
||||||
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
GIT_DEPTH: "1"
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- echo "🧰 Installing dependencies..."
|
||||||
|
- apk update && apk add --no-cache openssh git curl bash
|
||||||
|
|
||||||
|
# Setup SSH di runner
|
||||||
|
- mkdir -p ~/.ssh
|
||||||
|
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||||
|
- chmod 600 ~/.ssh/id_rsa
|
||||||
|
- eval "$(ssh-agent -s)"
|
||||||
|
- ssh-add ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
|
||||||
|
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
||||||
|
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
script:
|
||||||
|
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||||
|
|
||||||
|
- >
|
||||||
|
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd /home/devops/docker/deployment/development/lti-api
|
||||||
|
|
||||||
|
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
|
||||||
|
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
|
||||||
|
|
||||||
|
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
# Fetch/reset pakai SSH
|
||||||
|
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
|
||||||
|
git reset --hard origin/development
|
||||||
|
|
||||||
|
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
|
||||||
|
"; then
|
||||||
|
STATUS='success';
|
||||||
|
else
|
||||||
|
STATUS='failed';
|
||||||
|
fi;
|
||||||
|
|
||||||
|
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
|
||||||
|
|
||||||
|
if [ "$STATUS" = "success" ]; then
|
||||||
|
COLOR=3066993;
|
||||||
|
TITLE="✅ Deployment API Succeeded";
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
|
||||||
|
else
|
||||||
|
COLOR=15158332;
|
||||||
|
TITLE="❌ Deployment API Failed Gaes";
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
|
||||||
|
fi;
|
||||||
|
|
||||||
|
echo "{
|
||||||
|
\"username\": \"CI Bot\",
|
||||||
|
\"embeds\": [{
|
||||||
|
\"title\": \"$TITLE\",
|
||||||
|
\"description\": \"$DESC\",
|
||||||
|
\"color\": $COLOR,
|
||||||
|
\"fields\": [
|
||||||
|
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
|
||||||
|
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
|
||||||
|
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
|
||||||
|
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}" > payload.json;
|
||||||
|
|
||||||
|
echo "📡 Sending notification to Discord...";
|
||||||
|
curl -sS -H "Content-Type: application/json" \
|
||||||
|
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
||||||
|
|
||||||
|
only:
|
||||||
|
- development
|
||||||
|
|
||||||
|
environment:
|
||||||
|
name: development
|
||||||
@@ -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 +0,0 @@
|
|||||||
# ===============================
|
|
||||||
# LTI-API Makefile (Docker Setup)
|
|
||||||
# ===============================
|
|
||||||
|
|
||||||
APP_NAME := lti-api
|
|
||||||
COMPOSE := docker compose -f docker-compose.yaml
|
|
||||||
NETWORK := lti-network
|
|
||||||
ENV_FILE := .env.lti-api
|
|
||||||
|
|
||||||
include $(ENV_FILE)
|
|
||||||
export $(shell sed 's/=.*//' $(ENV_FILE))
|
|
||||||
|
|
||||||
MIGRATIONS_DIR := ./migrations
|
|
||||||
MIGRATE_IMAGE := migrate/migrate:v4.15.2
|
|
||||||
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@lti-postgres:5432/$(DB_NAME)?sslmode=disable
|
|
||||||
|
|
||||||
# --- Docker ---
|
|
||||||
docker-local:
|
|
||||||
@echo "🚀 Starting $(APP_NAME) with local PostgreSQL & Redis..."
|
|
||||||
@$(COMPOSE) up --build -d
|
|
||||||
|
|
||||||
docker-down:
|
|
||||||
@$(COMPOSE) down --remove-orphans
|
|
||||||
|
|
||||||
docker-nuke:
|
|
||||||
@echo "💣 Removing all containers, images, and volumes..."
|
|
||||||
@$(COMPOSE) down --rmi all --volumes --remove-orphans
|
|
||||||
|
|
||||||
# --- Database / Migration ---
|
|
||||||
|
|
||||||
wait-db:
|
|
||||||
@echo "⏳ Waiting for database lti-postgres to be ready (inside Docker network)..."
|
|
||||||
@$(COMPOSE) run --rm app sh -c 'until nc -z lti-postgres 5432; do echo "Waiting for DB..."; sleep 2; done; echo "✅ Database is ready!"'
|
|
||||||
|
|
||||||
migrate-up: wait-db
|
|
||||||
@echo "⬆️ Running migrations..."
|
|
||||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
|
|
||||||
|
|
||||||
migrate-down: wait-db
|
|
||||||
@echo "⬇️ Rolling back all migrations..."
|
|
||||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all
|
|
||||||
|
|
||||||
seed:
|
|
||||||
@echo "🌱 Running seed script..."
|
|
||||||
@$(COMPOSE) run --rm app go run cmd/seed/main.go
|
|
||||||
|
|
||||||
psql:
|
|
||||||
@docker exec -it lti-postgres psql -U $(DB_USER) -d $(DB_NAME)
|
|
||||||
|
|
||||||
logs:
|
|
||||||
@$(COMPOSE) logs -f app
|
|
||||||
|
|
||||||
restart:
|
|
||||||
@$(COMPOSE) restart
|
|
||||||
|
|
||||||
status:
|
|
||||||
@$(COMPOSE) ps
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=Postgres@Secure2025!
|
||||||
|
POSTGRES_DB=db_lti_erp
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 🧩 INIT SCRIPT: CREATE LIMITED APP USER FOR LTI API
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Buat user aplikasi jika belum ada
|
||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_lti_user') THEN
|
||||||
|
CREATE ROLE app_lti_user WITH LOGIN PASSWORD 'AppLti@Secure2025!' NOINHERIT NOCREATEROLE NOCREATEDB NOSUPERUSER;
|
||||||
|
RAISE NOTICE '✅ Role app_lti_user created successfully.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'ℹ️ Role app_lti_user already exists.';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Buat database jika belum ada
|
||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_database WHERE datname = 'db_lti_erp') THEN
|
||||||
|
CREATE DATABASE db_lti_erp OWNER app_lti_user;
|
||||||
|
RAISE NOTICE '✅ Database db_lti_erp created and owned by app_lti_user.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'ℹ️ Database db_lti_erp already exists.';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
\connect db_lti_erp
|
||||||
|
|
||||||
|
-- Beri hak CRUD untuk app_lti_user
|
||||||
|
GRANT CONNECT ON DATABASE db_lti_erp TO app_lti_user;
|
||||||
|
GRANT USAGE ON SCHEMA public TO app_lti_user;
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_lti_user;
|
||||||
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_lti_user;
|
||||||
|
|
||||||
|
-- Set default privileges agar tabel baru juga bisa diakses
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_lti_user;
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||||
|
GRANT USAGE, SELECT ON SEQUENCES TO app_lti_user;
|
||||||
|
|
||||||
|
-- Tampilkan hasil
|
||||||
|
\du app_lti_user
|
||||||
@@ -41,6 +41,8 @@ services:
|
|||||||
working_dir: /lti-api
|
working_dir: /lti-api
|
||||||
volumes:
|
volumes:
|
||||||
- .:/lti-api
|
- .:/lti-api
|
||||||
|
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
||||||
|
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
||||||
command: air -c .air.toml
|
command: air -c .air.toml
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|||||||
+43
-22
@@ -1,30 +1,28 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
dev-lti-api:
|
dev-api-lti:
|
||||||
container_name: dev-lti-api
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.local
|
dockerfile: Dockerfile
|
||||||
image: dev-lti-api:latest
|
container_name: dev-api-lti
|
||||||
working_dir: /lti-api
|
working_dir: /lti-api
|
||||||
command: air -c .air.toml
|
command: ["/bin/sh", "scripts/entrypoint.sh"]
|
||||||
ports:
|
ports:
|
||||||
- "8081:8081"
|
- "8081:8081"
|
||||||
env_file:
|
env_file:
|
||||||
- .env.lti-api
|
- .env
|
||||||
environment:
|
environment:
|
||||||
# override agar koneksi ke container internal
|
# override agar koneksi ke container internal
|
||||||
DB_HOST: dev-lti-postgres
|
DB_HOST: dev-postgres-lti
|
||||||
DB_PORT: 5432
|
DB_PORT: 5432
|
||||||
REDIS_URL: redis://dev-lti-redis:6379/0
|
REDIS_URL: redis://dev-redis-lti:6379/0
|
||||||
volumes:
|
volumes:
|
||||||
- .:/lti-api
|
- .:/lti-api
|
||||||
|
- ./.air.toml:/lti-api/.air.toml:ro
|
||||||
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
||||||
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
||||||
depends_on:
|
depends_on:
|
||||||
- dev-lti-postgres
|
- dev-postgres-lti
|
||||||
- dev-lti-redis
|
- dev-redis-lti
|
||||||
networks:
|
networks:
|
||||||
- lti-network
|
- lti-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -33,19 +31,26 @@ services:
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 10
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "2.0"
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
dev-lti-postgres:
|
dev-postgres-lti:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: dev-lti-postgres
|
container_name: dev-postgres-lti
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
env_file:
|
||||||
POSTGRES_USER: ${DB_USER:-postgres}
|
- credential/.env.db
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
|
||||||
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
|
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5433:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- dev-lti-postgres-data:/var/lib/postgresql/data
|
- dev-postgres-lti-data:/var/lib/postgresql/data
|
||||||
|
- ./credential:/docker-entrypoint-initdb.d:ro
|
||||||
networks:
|
networks:
|
||||||
- lti-network
|
- lti-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -54,10 +59,18 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 5s
|
start_period: 5s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
dev-lti-redis:
|
dev-redis-lti:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: dev-lti-redis
|
container_name: dev-redis-lti
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "6380:6379"
|
- "6380:6379"
|
||||||
@@ -68,10 +81,18 @@ services:
|
|||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: "0.2"
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
lti-network:
|
lti-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dev-lti-postgres-data:
|
dev-postgres-lti-data:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ go 1.23
|
|||||||
require (
|
require (
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0
|
github.com/MicahParks/keyfunc/v2 v2.1.0
|
||||||
github.com/bytedance/sonic v1.12.1
|
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/go-playground/validator/v10 v10.27.0
|
||||||
github.com/gofiber/contrib/jwt v1.0.10
|
github.com/gofiber/contrib/jwt v1.0.10
|
||||||
github.com/gofiber/fiber/v2 v2.52.5
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
@@ -26,10 +25,8 @@ require (
|
|||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
@@ -54,7 +51,6 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/philhofer/fwd v1.1.2 // indirect
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
@@ -79,8 +75,4 @@ require (
|
|||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
|
||||||
modernc.org/memory v1.5.0 // indirect
|
|
||||||
modernc.org/sqlite v1.23.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,18 +27,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -56,8 +50,6 @@ 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/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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
@@ -154,9 +146,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
@@ -317,12 +306,4 @@ 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/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
|
||||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
|
||||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
|
||||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
|
||||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
|
||||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
|
||||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
|
||||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type ApprovalRepository interface {
|
|||||||
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
|
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)
|
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)
|
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
|
||||||
|
DeleteByTarget(ctx context.Context, workflow string, approvableID uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type approvalRepositoryImpl struct {
|
type approvalRepositoryImpl struct {
|
||||||
@@ -104,3 +105,13 @@ func (r *approvalRepositoryImpl) LatestByTargets(
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *approvalRepositoryImpl) DeleteByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
workflow string,
|
||||||
|
approvableID uint,
|
||||||
|
) error {
|
||||||
|
return r.DB().WithContext(ctx).
|
||||||
|
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
|
||||||
|
Delete(&entity.Approval{}).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StockAllocationRepository interface {
|
||||||
|
BaseRepository[entity.StockAllocation]
|
||||||
|
FindActiveByUsable(ctx context.Context, usableType string, usableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.StockAllocation, error)
|
||||||
|
ReleaseByUsable(ctx context.Context, usableType string, usableID uint, note *string, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockAllocationRepositoryImpl struct {
|
||||||
|
*BaseRepositoryImpl[entity.StockAllocation]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStockAllocationRepository(db *gorm.DB) StockAllocationRepository {
|
||||||
|
return &StockAllocationRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: NewBaseRepository[entity.StockAllocation](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StockAllocationRepositoryImpl) FindActiveByUsable(
|
||||||
|
ctx context.Context,
|
||||||
|
usableType string,
|
||||||
|
usableID uint,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) ([]entity.StockAllocation, error) {
|
||||||
|
var allocations []entity.StockAllocation
|
||||||
|
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Order("created_at ASC").Find(&allocations).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return allocations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StockAllocationRepositoryImpl) ReleaseByUsable(
|
||||||
|
ctx context.Context,
|
||||||
|
usableType string,
|
||||||
|
usableID uint,
|
||||||
|
note *string,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
updates := map[string]any{
|
||||||
|
"status": entity.StockAllocationStatusReleased,
|
||||||
|
"released_at": now,
|
||||||
|
}
|
||||||
|
if note != nil {
|
||||||
|
updates["note"] = *note
|
||||||
|
}
|
||||||
|
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.StockAllocation{}).
|
||||||
|
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Updates(updates).Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,820 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FifoService interface {
|
||||||
|
RegisterStockable(cfg fifo.StockableConfig) error
|
||||||
|
RegisterUsable(cfg fifo.UsableConfig) error
|
||||||
|
|
||||||
|
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
|
||||||
|
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
|
||||||
|
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type fifoService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
logger *logrus.Logger
|
||||||
|
allocations commonRepo.StockAllocationRepository
|
||||||
|
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||||
|
defaultOrderBy []string
|
||||||
|
pendingBatchPerUsable int
|
||||||
|
maxLotsPerStockable int
|
||||||
|
defaultAllocationNotes string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFifoService(
|
||||||
|
db *gorm.DB,
|
||||||
|
allocations commonRepo.StockAllocationRepository,
|
||||||
|
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository,
|
||||||
|
logger *logrus.Logger,
|
||||||
|
) FifoService {
|
||||||
|
if logger == nil {
|
||||||
|
logger = logrus.StandardLogger()
|
||||||
|
}
|
||||||
|
return &fifoService{
|
||||||
|
db: db,
|
||||||
|
logger: logger,
|
||||||
|
allocations: allocations,
|
||||||
|
productWarehouseRepo: productWarehouseRepo,
|
||||||
|
defaultOrderBy: []string{"created_at ASC", "id ASC"},
|
||||||
|
pendingBatchPerUsable: 25,
|
||||||
|
maxLotsPerStockable: 50,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) withTransaction(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
fn func(*gorm.DB) error,
|
||||||
|
) error {
|
||||||
|
if tx != nil {
|
||||||
|
return fn(tx.WithContext(ctx))
|
||||||
|
}
|
||||||
|
return s.db.WithContext(ctx).Transaction(func(inner *gorm.DB) error {
|
||||||
|
return fn(inner)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) txOrDB(tx, db *gorm.DB) *gorm.DB {
|
||||||
|
if tx != nil {
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) RegisterStockable(cfg fifo.StockableConfig) error {
|
||||||
|
return fifo.RegisterStockable(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) RegisterUsable(cfg fifo.UsableConfig) error {
|
||||||
|
return fifo.RegisterUsable(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockReplenishRequest struct {
|
||||||
|
StockableKey fifo.StockableKey
|
||||||
|
StockableID uint
|
||||||
|
ProductWarehouseID uint
|
||||||
|
Quantity float64
|
||||||
|
Note *string
|
||||||
|
Tx *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type PendingResolution struct {
|
||||||
|
UsableKey fifo.UsableKey
|
||||||
|
UsableID uint
|
||||||
|
Quantity float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockReplenishResult struct {
|
||||||
|
AddedQuantity float64
|
||||||
|
PendingResolved []PendingResolution
|
||||||
|
RemainingPending float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockConsumeRequest struct {
|
||||||
|
UsableKey fifo.UsableKey
|
||||||
|
UsableID uint
|
||||||
|
ProductWarehouseID uint
|
||||||
|
Quantity float64
|
||||||
|
AllowPending bool
|
||||||
|
Note *string
|
||||||
|
Tx *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllocationDetail struct {
|
||||||
|
StockableKey fifo.StockableKey
|
||||||
|
StockableID uint
|
||||||
|
Quantity float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockConsumeResult struct {
|
||||||
|
RequestedQuantity float64
|
||||||
|
UsageQuantity float64
|
||||||
|
PendingQuantity float64
|
||||||
|
AddedAllocations []AllocationDetail
|
||||||
|
ReleasedQuantity float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockReleaseRequest struct {
|
||||||
|
UsableKey fifo.UsableKey
|
||||||
|
UsableID uint
|
||||||
|
Reason *string
|
||||||
|
Tx *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
|
||||||
|
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
|
||||||
|
return nil, errors.New("stockable key and id are required")
|
||||||
|
}
|
||||||
|
if req.ProductWarehouseID == 0 {
|
||||||
|
return nil, errors.New("product warehouse id is required")
|
||||||
|
}
|
||||||
|
if req.Quantity <= 0 {
|
||||||
|
return nil, errors.New("quantity must be greater than zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := fifo.Stockable(req.StockableKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("stockable %q is not registered", req.StockableKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &StockReplenishResult{
|
||||||
|
AddedQuantity: req.Quantity,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
|
if err := s.incrementStockableQty(ctx, tx, cfg, req.StockableID, req.Quantity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
||||||
|
req.ProductWarehouseID: req.Quantity,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved, err := s.resolvePendingForWarehouse(ctx, tx, req.ProductWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result.PendingResolved = resolved
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) {
|
||||||
|
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
||||||
|
return nil, errors.New("usable key and id are required")
|
||||||
|
}
|
||||||
|
if req.Quantity < 0 {
|
||||||
|
return nil, errors.New("quantity must be zero or greater")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := fifo.Usable(req.UsableKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &StockConsumeResult{
|
||||||
|
RequestedQuantity: req.Quantity,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
|
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
productWarehouseID := ctxRow.ProductWarehouseID
|
||||||
|
if productWarehouseID == 0 {
|
||||||
|
return fmt.Errorf("usable %q (id: %d) has no product warehouse reference", req.UsableKey, req.UsableID)
|
||||||
|
}
|
||||||
|
if req.ProductWarehouseID != 0 && req.ProductWarehouseID != productWarehouseID {
|
||||||
|
return fmt.Errorf("usable %q (id: %d) references product warehouse %d but %d was provided", req.UsableKey, req.UsableID, productWarehouseID, req.ProductWarehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUsage := ctxRow.UsageQty
|
||||||
|
currentPending := ctxRow.PendingQty
|
||||||
|
currentTotal := currentUsage + currentPending
|
||||||
|
delta := req.Quantity - currentTotal
|
||||||
|
|
||||||
|
var (
|
||||||
|
usageDelta float64
|
||||||
|
pendingDelta float64
|
||||||
|
addedAlloc []AllocationDetail
|
||||||
|
releasedAmount float64
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case delta > 0:
|
||||||
|
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if allocationRes.pending > 0 && !req.AllowPending {
|
||||||
|
return fmt.Errorf("insufficient stock: requested %.3f, allocated %.3f", req.Quantity, currentUsage+allocationRes.allocated)
|
||||||
|
}
|
||||||
|
|
||||||
|
usageDelta += allocationRes.allocated
|
||||||
|
pendingDelta += allocationRes.pending
|
||||||
|
addedAlloc = allocationRes.allocations
|
||||||
|
|
||||||
|
if allocationRes.allocated > 0 {
|
||||||
|
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
||||||
|
productWarehouseID: -allocationRes.allocated,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case delta < 0:
|
||||||
|
reductionTarget := -delta
|
||||||
|
|
||||||
|
if currentPending > 0 {
|
||||||
|
pendingReduction := math.Min(currentPending, reductionTarget)
|
||||||
|
if pendingReduction > 0 {
|
||||||
|
pendingDelta -= pendingReduction
|
||||||
|
reductionTarget -= pendingReduction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reductionTarget > 0 {
|
||||||
|
released, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, reductionTarget)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if released+1e-6 < reductionTarget {
|
||||||
|
return fmt.Errorf("unable to release %.3f from usable %d, only %.3f available", reductionTarget, req.UsableID, released)
|
||||||
|
}
|
||||||
|
usageDelta -= released
|
||||||
|
releasedAmount = released
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// no change
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.AddedAllocations = addedAlloc
|
||||||
|
result.ReleasedQuantity = releasedAmount
|
||||||
|
result.UsageQuantity = currentUsage + usageDelta
|
||||||
|
result.PendingQuantity = currentPending + pendingDelta
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest) error {
|
||||||
|
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
||||||
|
return errors.New("usable key and id are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
|
cfg, ok := fifo.Usable(req.UsableKey)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("usable %q is not registered", req.UsableKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var usageDelta, pendingDelta float64
|
||||||
|
if ctxRow.UsageQty > 0 {
|
||||||
|
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
usageDelta -= ctxRow.UsageQty
|
||||||
|
}
|
||||||
|
if ctxRow.PendingQty > 0 {
|
||||||
|
pendingDelta -= ctxRow.PendingQty
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.allocations.ReleaseByUsable(ctx, req.UsableKey.String(), req.UsableID, req.Reason, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
type usableContextRow struct {
|
||||||
|
ProductWarehouseID uint
|
||||||
|
UsageQty float64
|
||||||
|
PendingQty float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) loadUsableContext(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint) (*usableContextRow, error) {
|
||||||
|
var row usableContextRow
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Select(fmt.Sprintf("%s AS product_warehouse_id, COALESCE(%s,0) AS usage_qty, COALESCE(%s,0) AS pending_qty", cfg.Columns.ProductWarehouseID, cfg.Columns.UsageQuantity, cfg.Columns.PendingQuantity)).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"})
|
||||||
|
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Take(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fmt.Errorf("usable record %d not found", id)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) incrementStockableQty(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
|
||||||
|
column := cfg.Columns.TotalQuantity
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]any{
|
||||||
|
column: gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty),
|
||||||
|
}
|
||||||
|
if cfg.Columns.TotalUsedQuantity != "" {
|
||||||
|
updates[cfg.Columns.TotalUsedQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0)", cfg.Columns.TotalUsedQuantity))
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) incrementStockableUsage(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
|
||||||
|
if qty == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
column := cfg.Columns.TotalUsedQuantity
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.Update(column, gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty)).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type allocationOutcome struct {
|
||||||
|
allocated float64
|
||||||
|
pending float64
|
||||||
|
allocations []AllocationDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
type stockLot struct {
|
||||||
|
StockableKey fifo.StockableKey
|
||||||
|
RecordID uint
|
||||||
|
AvailableQty float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) allocateFromStock(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
productWarehouseID uint,
|
||||||
|
usableKey fifo.UsableKey,
|
||||||
|
usableID uint,
|
||||||
|
requestQty float64,
|
||||||
|
) (*allocationOutcome, error) {
|
||||||
|
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(lots) == 0 {
|
||||||
|
return &allocationOutcome{pending: requestQty}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
remaining = requestQty
|
||||||
|
applied float64
|
||||||
|
allocations []*entities.StockAllocation
|
||||||
|
allocationSummaries []AllocationDetail
|
||||||
|
usageAdjustments = make(map[fifo.StockableKey]map[uint]float64)
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, lot := range lots {
|
||||||
|
if remaining <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if lot.AvailableQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
portion := lot.AvailableQty
|
||||||
|
if portion > remaining {
|
||||||
|
portion = remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
applied += portion
|
||||||
|
remaining -= portion
|
||||||
|
|
||||||
|
allocationSummaries = append(allocationSummaries, AllocationDetail{
|
||||||
|
StockableKey: lot.StockableKey,
|
||||||
|
StockableID: lot.RecordID,
|
||||||
|
Quantity: portion,
|
||||||
|
})
|
||||||
|
|
||||||
|
allocations = append(allocations, &entities.StockAllocation{
|
||||||
|
ProductWarehouseId: productWarehouseID,
|
||||||
|
StockableType: lot.StockableKey.String(),
|
||||||
|
StockableId: lot.RecordID,
|
||||||
|
UsableType: usableKey.String(),
|
||||||
|
UsableId: usableID,
|
||||||
|
Qty: portion,
|
||||||
|
Status: entities.StockAllocationStatusActive,
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, ok := usageAdjustments[lot.StockableKey]; !ok {
|
||||||
|
usageAdjustments[lot.StockableKey] = make(map[uint]float64)
|
||||||
|
}
|
||||||
|
usageAdjustments[lot.StockableKey][lot.RecordID] += portion
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allocations) > 0 {
|
||||||
|
if err := s.allocations.CreateMany(ctx, allocations, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, deltas := range usageAdjustments {
|
||||||
|
cfg, ok := fifo.Stockable(key)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for id, qty := range deltas {
|
||||||
|
if err := s.incrementStockableUsage(ctx, tx, cfg, id, qty); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &allocationOutcome{
|
||||||
|
allocated: applied,
|
||||||
|
pending: remaining,
|
||||||
|
allocations: allocationSummaries,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) {
|
||||||
|
configs := fifo.Stockables()
|
||||||
|
if len(configs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var lots []stockLot
|
||||||
|
for key, cfg := range configs {
|
||||||
|
selectStmt := fmt.Sprintf(
|
||||||
|
"%s AS id, %s AS available_qty, %s AS created_at",
|
||||||
|
cfg.Columns.ID,
|
||||||
|
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||||
|
cfg.Columns.CreatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows []struct {
|
||||||
|
ID uint
|
||||||
|
AvailableQty float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Select(selectStmt).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
|
||||||
|
Where(fmt.Sprintf("%s > %s", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity))
|
||||||
|
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, order := range s.orderClauses(cfg.OrderBy) {
|
||||||
|
query = query.Order(order)
|
||||||
|
}
|
||||||
|
query = query.Limit(s.maxLotsPerStockable)
|
||||||
|
|
||||||
|
if err := query.Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.AvailableQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lots = append(lots, stockLot{
|
||||||
|
StockableKey: key,
|
||||||
|
RecordID: row.ID,
|
||||||
|
AvailableQty: row.AvailableQty,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lots) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(lots, func(i, j int) bool {
|
||||||
|
if lots[i].CreatedAt.Equal(lots[j].CreatedAt) {
|
||||||
|
return lots[i].RecordID < lots[j].RecordID
|
||||||
|
}
|
||||||
|
return lots[i].CreatedAt.Before(lots[j].CreatedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
return lots, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) applyUsableDeltas(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint, usageDelta, pendingDelta float64) error {
|
||||||
|
if usageDelta == 0 && pendingDelta == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]any{}
|
||||||
|
if usageDelta != 0 {
|
||||||
|
updates[cfg.Columns.UsageQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.UsageQuantity), usageDelta)
|
||||||
|
}
|
||||||
|
if pendingDelta != 0 {
|
||||||
|
updates[cfg.Columns.PendingQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.PendingQuantity), pendingDelta)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type pendingCandidate struct {
|
||||||
|
UsableKey fifo.UsableKey
|
||||||
|
Config fifo.UsableConfig
|
||||||
|
UsableID uint
|
||||||
|
Pending float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]PendingResolution, error) {
|
||||||
|
candidates, err := s.fetchPendingCandidates(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolutions []PendingResolution
|
||||||
|
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if candidate.Pending <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if outcome.allocated <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.applyUsableDeltas(ctx, tx, candidate.Config, candidate.UsableID, outcome.allocated, -outcome.allocated); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
||||||
|
productWarehouseID: -outcome.allocated,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutions = append(resolutions, PendingResolution{
|
||||||
|
UsableKey: candidate.UsableKey,
|
||||||
|
UsableID: candidate.UsableID,
|
||||||
|
Quantity: outcome.allocated,
|
||||||
|
})
|
||||||
|
|
||||||
|
if outcome.pending > 0 {
|
||||||
|
// No more stock available for this warehouse at the moment.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolutions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) releaseUsagePortion(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
usableKey fifo.UsableKey,
|
||||||
|
usableID uint,
|
||||||
|
target float64,
|
||||||
|
) (float64, error) {
|
||||||
|
if target <= 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
allocations, err := s.allocations.FindActiveByUsable(ctx, usableKey.String(), usableID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
target := s.txOrDB(tx, db)
|
||||||
|
return target.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(allocations) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
remaining = target
|
||||||
|
totalReleased float64
|
||||||
|
warehouseAdjustments = make(map[uint]float64)
|
||||||
|
stockableAdjustments = make(map[fifo.StockableKey]map[uint]float64)
|
||||||
|
)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for i := len(allocations) - 1; i >= 0 && remaining > 0; i-- {
|
||||||
|
allocation := allocations[i]
|
||||||
|
releaseAmt := allocation.Qty
|
||||||
|
if releaseAmt > remaining {
|
||||||
|
releaseAmt = remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining -= releaseAmt
|
||||||
|
totalReleased += releaseAmt
|
||||||
|
warehouseAdjustments[allocation.ProductWarehouseId] += releaseAmt
|
||||||
|
|
||||||
|
key := fifo.StockableKey(allocation.StockableType)
|
||||||
|
if _, ok := stockableAdjustments[key]; !ok {
|
||||||
|
stockableAdjustments[key] = make(map[uint]float64)
|
||||||
|
}
|
||||||
|
stockableAdjustments[key][allocation.StockableId] += releaseAmt
|
||||||
|
|
||||||
|
if releaseAmt == allocation.Qty {
|
||||||
|
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
|
||||||
|
"status": entities.StockAllocationStatusReleased,
|
||||||
|
"released_at": now,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
|
||||||
|
"quantity": allocation.Qty - releaseAmt,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalReleased == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, deltas := range stockableAdjustments {
|
||||||
|
cfg, ok := fifo.Stockable(key)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for id, qty := range deltas {
|
||||||
|
if err := s.incrementStockableUsage(ctx, tx, cfg, id, -qty); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(warehouseAdjustments) > 0 {
|
||||||
|
if err := s.productWarehouseRepo.AdjustQuantities(ctx, warehouseAdjustments, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for warehouseID := range warehouseAdjustments {
|
||||||
|
if _, err := s.resolvePendingForWarehouse(ctx, tx, warehouseID); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalReleased, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]pendingCandidate, error) {
|
||||||
|
configs := fifo.Usables()
|
||||||
|
if len(configs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates []pendingCandidate
|
||||||
|
|
||||||
|
for key, cfg := range configs {
|
||||||
|
selectStmt := fmt.Sprintf(
|
||||||
|
"%s AS id, %s AS pending_qty, %s AS created_at",
|
||||||
|
cfg.Columns.ID,
|
||||||
|
cfg.Columns.PendingQuantity,
|
||||||
|
cfg.Columns.CreatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows []struct {
|
||||||
|
ID uint
|
||||||
|
Pending float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Select(selectStmt).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
|
||||||
|
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
|
||||||
|
Limit(s.pendingBatchPerUsable)
|
||||||
|
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, order := range s.orderClauses(cfg.OrderBy) {
|
||||||
|
query = query.Order(order)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.Pending <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidates = append(candidates, pendingCandidate{
|
||||||
|
UsableKey: key,
|
||||||
|
Config: cfg,
|
||||||
|
UsableID: row.ID,
|
||||||
|
Pending: row.Pending,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(candidates, func(i, j int) bool {
|
||||||
|
if candidates[i].CreatedAt.Equal(candidates[j].CreatedAt) {
|
||||||
|
return candidates[i].UsableID < candidates[j].UsableID
|
||||||
|
}
|
||||||
|
return candidates[i].CreatedAt.Before(candidates[j].CreatedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
return candidates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) orderClauses(custom []string) []string {
|
||||||
|
if len(custom) > 0 {
|
||||||
|
return custom
|
||||||
|
}
|
||||||
|
return s.defaultOrderBy
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package validation
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
)
|
)
|
||||||
@@ -21,34 +23,41 @@ var customMessages = map[string]string{
|
|||||||
"alphanum": "Field %s must contain only alphanumeric characters",
|
"alphanum": "Field %s must contain only alphanumeric characters",
|
||||||
"oneof": "Invalid value for field %s",
|
"oneof": "Invalid value for field %s",
|
||||||
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
|
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
|
||||||
|
"gt": "Invalid %s, must be greater than %s",
|
||||||
}
|
}
|
||||||
|
|
||||||
func CustomErrorMessages(err error) map[string]string {
|
func CustomErrorMessages(err error) (string, map[string]string) {
|
||||||
var validationErrors validator.ValidationErrors
|
var validationErrors validator.ValidationErrors
|
||||||
if errors.As(err, &validationErrors) {
|
if errors.As(err, &validationErrors) {
|
||||||
return generateErrorMessages(validationErrors)
|
return generateErrorMessages(validationErrors)
|
||||||
}
|
}
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
|
func generateErrorMessages(validationErrors validator.ValidationErrors) (string, map[string]string) {
|
||||||
errorsMap := make(map[string]string)
|
errorsMap := make(map[string]string)
|
||||||
for _, err := range validationErrors {
|
var firstMessage string
|
||||||
|
for i, err := range validationErrors {
|
||||||
fieldName := err.StructNamespace()
|
fieldName := err.StructNamespace()
|
||||||
tag := err.Tag()
|
tag := err.Tag()
|
||||||
|
|
||||||
customMessage := customMessages[tag]
|
customMessage := customMessages[tag]
|
||||||
|
var msg string
|
||||||
if customMessage != "" {
|
if customMessage != "" {
|
||||||
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
|
msg = formatErrorMessage(customMessage, err, tag)
|
||||||
} else {
|
} else {
|
||||||
errorsMap[fieldName] = defaultErrorMessage(err)
|
msg = defaultErrorMessage(err)
|
||||||
|
}
|
||||||
|
errorsMap[fieldName] = msg
|
||||||
|
if i == 0 {
|
||||||
|
firstMessage = msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errorsMap
|
return firstMessage, errorsMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
|
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
|
||||||
if tag == "min" || tag == "max" || tag == "len" {
|
if tag == "min" || tag == "max" || tag == "len" || tag == "gt" {
|
||||||
return fmt.Sprintf(customMessage, err.Field(), err.Param())
|
return fmt.Sprintf(customMessage, err.Field(), err.Param())
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(customMessage, err.Field())
|
return fmt.Sprintf(customMessage, err.Field())
|
||||||
@@ -61,6 +70,16 @@ func defaultErrorMessage(err validator.FieldError) string {
|
|||||||
func Validator() *validator.Validate {
|
func Validator() *validator.Validate {
|
||||||
validate := validator.New()
|
validate := validator.New()
|
||||||
|
|
||||||
|
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
|
||||||
|
if jsonTag := getTagName(fld, "json"); jsonTag != "" {
|
||||||
|
return jsonTag
|
||||||
|
}
|
||||||
|
if queryTag := getTagName(fld, "query"); queryTag != "" {
|
||||||
|
return queryTag
|
||||||
|
}
|
||||||
|
return fld.Name
|
||||||
|
})
|
||||||
|
|
||||||
if err := validate.RegisterValidation("password", Password); err != nil {
|
if err := validate.RegisterValidation("password", Password); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -72,3 +91,16 @@ func Validator() *validator.Validate {
|
|||||||
}
|
}
|
||||||
return validate
|
return validate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTagName(fld reflect.StructField, tag string) string {
|
||||||
|
value, ok := fld.Tag.Lookup(tag)
|
||||||
|
if !ok || value == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.Split(value, ",")[0]
|
||||||
|
if name == "" || name == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
@@ -2,42 +2,42 @@
|
|||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
id_user BIGINT NOT NULL,
|
id_user BIGINT NOT NULL,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
email VARCHAR NOT NULL,
|
email VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
|
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL;
|
CREATE UNIQUE INDEX users_email_unique ON users (email)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- FLAGS
|
-- FLAGS
|
||||||
CREATE TABLE flags (
|
CREATE TABLE flags (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
flagable_id BIGINT NOT NULL,
|
flagable_id BIGINT NOT NULL,
|
||||||
flagable_type VARCHAR(50) NOT NULL,
|
flagable_type VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
updated_at TIMESTAMPTZ DEFAULT NOW ()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX flags_unique_flagable ON flags (
|
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
|
||||||
name,
|
|
||||||
flagable_id,
|
|
||||||
flagable_type
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
||||||
|
|
||||||
-- PRODUCT CATEGORIES
|
-- PRODUCT CATEGORIES
|
||||||
CREATE TABLE product_categories (
|
CREATE TABLE product_categories (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
code VARCHAR(10) NOT NULL,
|
code VARCHAR(10) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -53,9 +53,9 @@ WHERE
|
|||||||
-- UOM
|
-- UOM
|
||||||
CREATE TABLE uoms (
|
CREATE TABLE uoms (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -67,12 +67,12 @@ WHERE
|
|||||||
-- BANKS
|
-- BANKS
|
||||||
CREATE TABLE banks (
|
CREATE TABLE banks (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
alias VARCHAR(5) NOT NULL,
|
alias VARCHAR(5) NOT NULL,
|
||||||
owner VARCHAR,
|
owner VARCHAR(50),
|
||||||
account_number VARCHAR(50) NOT NULL,
|
account_number VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -84,9 +84,9 @@ WHERE
|
|||||||
-- AREAS
|
-- AREAS
|
||||||
CREATE TABLE areas (
|
CREATE TABLE areas (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -98,11 +98,11 @@ WHERE
|
|||||||
-- LOCATIONS
|
-- LOCATIONS
|
||||||
CREATE TABLE locations (
|
CREATE TABLE locations (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -114,11 +114,11 @@ WHERE
|
|||||||
-- KANDANG
|
-- KANDANG
|
||||||
CREATE TABLE kandangs (
|
CREATE TABLE kandangs (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
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,
|
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -130,13 +130,13 @@ WHERE
|
|||||||
-- WAREHOUSES
|
-- WAREHOUSES
|
||||||
CREATE TABLE warehouses (
|
CREATE TABLE warehouses (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
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,
|
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,
|
kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -148,16 +148,16 @@ WHERE
|
|||||||
-- CUSTOMERS
|
-- CUSTOMERS
|
||||||
CREATE TABLE customers (
|
CREATE TABLE customers (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
phone VARCHAR(20) NOT NULL,
|
phone VARCHAR(20) NOT NULL,
|
||||||
email VARCHAR NOT NULL,
|
email VARCHAR(50) NOT NULL,
|
||||||
account_number VARCHAR(50) NOT NULL,
|
account_number VARCHAR(50) NOT NULL,
|
||||||
balance NUMERIC(15, 3) DEFAULT 0,
|
balance NUMERIC(15, 3) DEFAULT 0,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -169,10 +169,10 @@ WHERE
|
|||||||
-- NONSTOCK
|
-- NONSTOCK
|
||||||
CREATE TABLE nonstocks (
|
CREATE TABLE nonstocks (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -184,9 +184,9 @@ WHERE
|
|||||||
-- FCR
|
-- FCR
|
||||||
CREATE TABLE fcrs (
|
CREATE TABLE fcrs (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -201,29 +201,29 @@ CREATE TABLE fcr_standards (
|
|||||||
weight NUMERIC(15, 3) NOT NULL,
|
weight NUMERIC(15, 3) NOT NULL,
|
||||||
fcr_number NUMERIC(15, 3) NOT NULL,
|
fcr_number NUMERIC(15, 3) NOT NULL,
|
||||||
mortality NUMERIC(15, 3) NOT NULL,
|
mortality NUMERIC(15, 3) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
-- SUPPLIERS
|
-- SUPPLIERS
|
||||||
CREATE TABLE suppliers (
|
CREATE TABLE suppliers (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
alias VARCHAR(5) NOT NULL,
|
alias VARCHAR(5) NOT NULL,
|
||||||
pic VARCHAR NOT NULL,
|
pic VARCHAR(50) NOT NULL,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
category VARCHAR(20) NOT NULL,
|
category VARCHAR(20) NOT NULL,
|
||||||
hatchery VARCHAR,
|
hatchery VARCHAR(50),
|
||||||
phone VARCHAR(20) NOT NULL,
|
phone VARCHAR(20) NOT NULL,
|
||||||
email VARCHAR NOT NULL,
|
email VARCHAR(50) NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
npwp VARCHAR(50),
|
npwp VARCHAR(50),
|
||||||
account_number VARCHAR(50),
|
account_number VARCHAR(50),
|
||||||
balance NUMERIC(15, 3) DEFAULT 0,
|
balance NUMERIC(15, 3) DEFAULT 0,
|
||||||
due_date INT NOT NULL,
|
due_date INT NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -235,15 +235,15 @@ WHERE
|
|||||||
CREATE TABLE nonstock_suppliers (
|
CREATE TABLE nonstock_suppliers (
|
||||||
nonstock_id BIGINT NOT NULL REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
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,
|
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
PRIMARY KEY (nonstock_id, supplier_id)
|
PRIMARY KEY (nonstock_id, supplier_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- PRODUCTS
|
-- PRODUCTS
|
||||||
CREATE TABLE products (
|
CREATE TABLE products (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
brand VARCHAR NOT NULL,
|
brand VARCHAR(50) NOT NULL,
|
||||||
sku VARCHAR(100),
|
sku VARCHAR(100),
|
||||||
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
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_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
@@ -251,8 +251,8 @@ CREATE TABLE products (
|
|||||||
selling_price NUMERIC(15, 3),
|
selling_price NUMERIC(15, 3),
|
||||||
tax NUMERIC(15, 3),
|
tax NUMERIC(15, 3),
|
||||||
expiry_period INT,
|
expiry_period INT,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -268,15 +268,15 @@ WHERE
|
|||||||
CREATE TABLE product_suppliers (
|
CREATE TABLE product_suppliers (
|
||||||
product_id BIGINT NOT NULL REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
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,
|
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
PRIMARY KEY (product_id, supplier_id)
|
PRIMARY KEY (product_id, supplier_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- PROJECTS
|
-- PROJECTS
|
||||||
CREATE TABLE projects (
|
CREATE TABLE projects (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -288,8 +288,8 @@ CREATE TABLE product_warehouses (
|
|||||||
warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
|
warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
|
||||||
quantity INTEGER NOT NULL DEFAULT 0,
|
quantity INTEGER NOT NULL DEFAULT 0,
|
||||||
created_by BIGINT NOT NULL REFERENCES users (id),
|
created_by BIGINT NOT NULL REFERENCES users (id),
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -316,8 +316,8 @@ CREATE TABLE stock_logs (
|
|||||||
note TEXT,
|
note TEXT,
|
||||||
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
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_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS project_chickins (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
project_flock_kandang_id BIGINT NOT NULL,
|
|
||||||
chick_in_date DATE NOT NULL,
|
|
||||||
quantity NUMERIC(15, 3) NOT NULL,
|
|
||||||
note TEXT,
|
|
||||||
created_by BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
|
||||||
ALTER TABLE project_chickins
|
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
|
||||||
REFERENCES project_flock_kandangs(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE project_chickins
|
|
||||||
ADD CONSTRAINT fk_created_by
|
|
||||||
FOREIGN KEY (created_by)
|
|
||||||
REFERENCES users(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- INDEXES
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by);
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS project_flock_populations (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
project_flock_kandang_id BIGINT NOT NULL,
|
|
||||||
initial_quantity NUMERIC(15, 3) NOT NULL,
|
|
||||||
current_quantity NUMERIC(15, 3) NOT NULL,
|
|
||||||
reserved_quantity NUMERIC(15, 3),
|
|
||||||
created_by BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
|
||||||
ALTER TABLE project_flock_populations
|
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
|
||||||
REFERENCES project_flock_kandangs(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE project_flock_populations
|
|
||||||
ADD CONSTRAINT fk_created_by
|
|
||||||
FOREIGN KEY (created_by)
|
|
||||||
REFERENCES users(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- INDEXES
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
|
|
||||||
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_chickin_details (
|
|||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
||||||
|
|||||||
+26
-18
@@ -1,22 +1,30 @@
|
|||||||
|
|
||||||
ALTER TABLE kandangs
|
ALTER TABLE kandangs
|
||||||
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
||||||
|
|
||||||
ALTER TABLE kandangs
|
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
|
||||||
DROP COLUMN IF EXISTS project_flock_id;
|
|
||||||
|
|
||||||
ALTER TABLE project_chickins
|
-- Only alter if tables exist
|
||||||
DROP CONSTRAINT fk_project_flock_kandang_id,
|
DO $$
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
BEGIN
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
||||||
REFERENCES project_flock_kandangs(id)
|
ALTER TABLE project_chickins
|
||||||
ON UPDATE CASCADE
|
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||||
ON DELETE CASCADE;
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
ALTER TABLE project_flock_populations
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
|
||||||
DROP CONSTRAINT fk_project_flock_kandang_id,
|
ALTER TABLE project_flock_populations
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
ALTER TABLE project_flock_populations
|
||||||
REFERENCES project_flock_kandangs(id)
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
ON UPDATE CASCADE
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
ON DELETE CASCADE;
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS laying_transfers CASCADE;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS laying_transfers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
transfer_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
from_project_flock_id BIGINT NOT NULL,
|
||||||
|
to_project_flock_id BIGINT NOT NULL,
|
||||||
|
transfer_date DATE NOT NULL,
|
||||||
|
pending_usage_qty NUMERIC(15, 3),
|
||||||
|
usage_qty NUMERIC(15, 3),
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_from_project_flock
|
||||||
|
FOREIGN KEY (from_project_flock_id)
|
||||||
|
REFERENCES project_flocks(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_to_project_flock
|
||||||
|
FOREIGN KEY (to_project_flock_id)
|
||||||
|
REFERENCES project_flocks(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_created_by
|
||||||
|
FOREIGN KEY (created_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- INDEXES
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_laying_transfers_transfer_number ON laying_transfers (transfer_number)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers (deleted_at);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- MIGRATION: project_chickins
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- STEP 1: Hapus tabel jika sudah ada
|
||||||
|
DROP TABLE IF EXISTS project_chickins;
|
||||||
|
|
||||||
|
-- STEP 2: Buat tabel project_chickins
|
||||||
|
CREATE TABLE IF NOT EXISTS project_chickins (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_kandang_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
chick_in_date DATE NOT NULL,
|
||||||
|
usage_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
pending_usage_qty NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- STEP 3: FOREIGN KEYS
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Relasi ke project_flock_kandangs
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke product_warehouses
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke users
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_chickins_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- STEP 4: INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by);
|
||||||
|
|
||||||
|
-- Composite index for common queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_deleted ON project_chickins (
|
||||||
|
project_flock_kandang_id,
|
||||||
|
deleted_at
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for soft delete queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_deleted_at ON project_chickins (deleted_at);
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- MIGRATION: project_flock_populations
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- STEP 1: Hapus tabel jika sudah ada
|
||||||
|
DROP TABLE IF EXISTS project_flock_populations;
|
||||||
|
|
||||||
|
-- STEP 2: Buat tabel project_flock_populations
|
||||||
|
CREATE TABLE IF NOT EXISTS project_flock_populations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_chickin_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
total_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_used_qty NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- STEP 3: FOREIGN KEYS
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Relasi ke project_chickins
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_populations_chickin FOREIGN KEY (project_chickin_id) REFERENCES project_chickins (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke product_warehouses
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_populations_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke users
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_populations_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- STEP 4: INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by);
|
||||||
|
|
||||||
|
-- Composite index for common queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_chickin_deleted ON project_flock_populations (
|
||||||
|
project_chickin_id,
|
||||||
|
deleted_at
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for soft delete queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_deleted_at ON project_flock_populations (deleted_at);
|
||||||
|
|
||||||
|
-- Unique constraint: one population per chickin
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_populations_chickin_unique ON project_flock_populations (project_chickin_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
-- Rollback laying_transfer_sources dan laying_transfer_targets tables
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS laying_transfer_targets CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS laying_transfer_sources CASCADE;
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
-- Create laying_transfer_sources dan laying_transfer_targets tables
|
||||||
|
|
||||||
|
-- 1. Create laying_transfer_sources table (detail sumber - kandang asal growing)
|
||||||
|
CREATE TABLE laying_transfer_sources (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
laying_transfer_id BIGINT NOT NULL,
|
||||||
|
source_project_flock_kandang_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
note TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add foreign keys untuk laying_transfer_sources
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_sources_laying_transfer_id
|
||||||
|
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_sources_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (source_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_sources_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 2. Create laying_transfer_targets table (detail tujuan - kandang laying)
|
||||||
|
CREATE TABLE laying_transfer_targets (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
laying_transfer_id BIGINT NOT NULL,
|
||||||
|
target_project_flock_kandang_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
note TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add foreign keys untuk laying_transfer_targets
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_targets_laying_transfer_id
|
||||||
|
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_targets_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (target_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_targets_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 3. Create indexes untuk laying_transfer_sources
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_laying_transfer_id ON laying_transfer_sources (laying_transfer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_source_kandang_id ON laying_transfer_sources (
|
||||||
|
source_project_flock_kandang_id
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_product_warehouse_id ON laying_transfer_sources (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_deleted_at ON laying_transfer_sources (deleted_at);
|
||||||
|
|
||||||
|
-- 4. Create indexes untuk laying_transfer_targets
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_laying_transfer_id ON laying_transfer_targets (laying_transfer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_target_kandang_id ON laying_transfer_targets (
|
||||||
|
target_project_flock_kandang_id
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_product_warehouse_id ON laying_transfer_targets (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_deleted_at ON laying_transfer_targets (deleted_at);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS purchase_items;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS purchase_items (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
purchase_id BIGINT NOT NULL,
|
||||||
|
product_id BIGINT NOT NULL,
|
||||||
|
warehouse_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
received_date TIMESTAMPTZ,
|
||||||
|
travel_number VARCHAR,
|
||||||
|
travel_number_docs VARCHAR,
|
||||||
|
vehicle_number VARCHAR,
|
||||||
|
sub_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_qty NUMERIC(15, 3) NOT NULL DEFAULT 0,
|
||||||
|
total_used NUMERIC(15, 3) NOT NULL DEFAULT 0,
|
||||||
|
price NUMERIC(15, 3) NOT NULL DEFAULT 0,
|
||||||
|
total_price NUMERIC(15, 3) NOT NULL DEFAULT 0,
|
||||||
|
CONSTRAINT uq_purchase_items_purchase_product_warehouse
|
||||||
|
UNIQUE (purchase_id, product_id, warehouse_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_product
|
||||||
|
FOREIGN KEY (product_id)
|
||||||
|
REFERENCES products(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_warehouse
|
||||||
|
FOREIGN KEY (warehouse_id)
|
||||||
|
REFERENCES warehouses(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_product_warehouse
|
||||||
|
FOREIGN KEY (product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_purchase_items_purchase'
|
||||||
|
AND conrelid = 'purchase_items'::regclass
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_purchase;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS purchases;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS purchases (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
pr_number VARCHAR NOT NULL,
|
||||||
|
po_number VARCHAR NULL,
|
||||||
|
po_date TIMESTAMPTZ NULL,
|
||||||
|
supplier_id BIGINT NOT NULL,
|
||||||
|
credit_term INT NOT NULL,
|
||||||
|
due_date TIMESTAMPTZ,
|
||||||
|
grand_total NUMERIC(15, 3) NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
CONSTRAINT uq_purchases_pr_number UNIQUE (pr_number),
|
||||||
|
CONSTRAINT uq_purchases_po_number UNIQUE (po_number)
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchases
|
||||||
|
ADD CONSTRAINT fk_purchases_supplier
|
||||||
|
FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchases
|
||||||
|
ADD CONSTRAINT fk_purchases_created_by
|
||||||
|
FOREIGN KEY (created_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_tables WHERE tablename = 'purchase_items'
|
||||||
|
) AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_purchase_items_purchase'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_purchase
|
||||||
|
FOREIGN KEY (purchase_id)
|
||||||
|
REFERENCES purchases(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_deleted_at ON purchases (deleted_at);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
DROP TABLE IF EXISTS marketing_delivery_products CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS marketing_products CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS marketings CASCADE;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
CREATE TABLE marketings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
so_number VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
customer_id BIGINT NOT NULL,
|
||||||
|
so_docs VARCHAR(20),
|
||||||
|
so_date DATE NOT NULL,
|
||||||
|
sales_person_id BIGINT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'customers') THEN
|
||||||
|
ALTER TABLE marketings
|
||||||
|
ADD CONSTRAINT fk_marketings_customer_id
|
||||||
|
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE marketings
|
||||||
|
ADD CONSTRAINT fk_marketings_sales_person_id
|
||||||
|
FOREIGN KEY (sales_person_id) REFERENCES users(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE marketings
|
||||||
|
ADD CONSTRAINT fk_marketings_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_customer_id ON marketings (customer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_sales_person_id ON marketings (sales_person_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_created_by ON marketings (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_so_date ON marketings (so_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketings_deleted_at ON marketings (deleted_at);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS marketing_products CASCADE;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
CREATE TABLE marketing_products (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
marketing_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
avg_weight NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_weight NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketings') THEN
|
||||||
|
ALTER TABLE marketing_products
|
||||||
|
ADD CONSTRAINT fk_marketing_products_marketing_id
|
||||||
|
FOREIGN KEY (marketing_id) REFERENCES marketings(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE marketing_products
|
||||||
|
ADD CONSTRAINT fk_marketing_products_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_products_marketing_id ON marketing_products (marketing_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_products_product_warehouse_id ON marketing_products (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_products_deleted_at ON marketing_products (deleted_at);
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
DROP TABLE IF EXISTS marketing_delivery_products CASCADE;
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
CREATE TABLE marketing_delivery_products (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
marketing_product_id BIGINT UNIQUE NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_weight NUMERIC(15, 3) NOT NULL,
|
||||||
|
avg_weight NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
delivery_date DATE,
|
||||||
|
vehicle_number VARCHAR(50),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketing_products') THEN
|
||||||
|
ALTER TABLE marketing_delivery_products
|
||||||
|
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
|
||||||
|
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_delivery_products_marketing_product_id ON marketing_delivery_products (marketing_product_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_delivery_products_delivery_date ON marketing_delivery_products (delivery_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_marketing_delivery_products_deleted_at ON marketing_delivery_products (deleted_at);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
DROP INDEX IF EXISTS stock_allocations_released_at_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_allocations_status_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_allocations_usage_lookup;
|
||||||
|
DROP INDEX IF EXISTS stock_allocations_lookup;
|
||||||
|
DROP INDEX IF EXISTS stock_allocations_product_warehouse_id_idx;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS stock_allocations;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS stock_allocations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses(id),
|
||||||
|
stockable_type VARCHAR(100) NOT NULL,
|
||||||
|
stockable_id BIGINT NOT NULL,
|
||||||
|
usable_type VARCHAR(100) NOT NULL,
|
||||||
|
usable_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15,3) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
note TEXT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
released_at TIMESTAMPTZ NULL,
|
||||||
|
deleted_at TIMESTAMPTZ NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_allocations_product_warehouse_id_idx
|
||||||
|
ON stock_allocations (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_allocations_lookup
|
||||||
|
ON stock_allocations (stockable_type, stockable_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_allocations_usage_lookup
|
||||||
|
ON stock_allocations (usable_type, usable_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_allocations_status_idx
|
||||||
|
ON stock_allocations (status);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_allocations_released_at_idx
|
||||||
|
ON stock_allocations (released_at);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
DROP COLUMN IF EXISTS capacity;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
ADD COLUMN capacity NUMERIC(15,3) NOT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS expenses;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
CREATE TABLE expenses (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
reference_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
supplier_id BIGINT NOT NULL,
|
||||||
|
category VARCHAR(50) NOT NULL CHECK (
|
||||||
|
category IN ('BOP', 'NON-BOP')
|
||||||
|
),
|
||||||
|
po_number VARCHAR(50) NULL,
|
||||||
|
document_path JSON,
|
||||||
|
realization_document_path JSON,
|
||||||
|
expense_date DATE NOT NULL,
|
||||||
|
realization_date DATE,
|
||||||
|
grand_total NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
note TEXT,
|
||||||
|
created_by BIGINT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE SEQUENCE expenses_ref_seq INCREMENT BY 1 START WITH 1;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke suppliers
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
|
||||||
|
ALTER TABLE expenses
|
||||||
|
ADD CONSTRAINT fk_expenses_supplier_id
|
||||||
|
FOREIGN KEY (supplier_id) REFERENCES suppliers(id);
|
||||||
|
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke users (created_by)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE expenses
|
||||||
|
ADD CONSTRAINT fk_expenses_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Index
|
||||||
|
CREATE INDEX idx_expenses_supplier_id ON expenses (supplier_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expenses_expense_date ON expenses (expense_date);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expenses_deleted_at ON expenses (deleted_at);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE IF EXISTS expense_nonstocks;
|
||||||
|
|
||||||
|
DROP SEQUENCE expenses_ref_seq;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
CREATE TABLE expense_nonstocks (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
expense_id BIGINT NOT NULL,
|
||||||
|
project_flock_kandang_id BIGINT NULL,
|
||||||
|
kandang_id BIGINT NULL,
|
||||||
|
nonstock_id BIGINT,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
note TEXT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke expenses
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expenses') THEN
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_expense_id
|
||||||
|
FOREIGN KEY (expense_id) REFERENCES expenses(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke project_flock_kandangs
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign key ke kandang_id
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_kandang_id_2
|
||||||
|
FOREIGN KEY (kandang_id) REFERENCES kandangs(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke nonstocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_nonstock_id
|
||||||
|
FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Index
|
||||||
|
CREATE INDEX idx_expense_nonstocks_expense_id ON expense_nonstocks (expense_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expense_nonstocks_nonstock_id ON expense_nonstocks (nonstock_id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS expense_realizations;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
CREATE TABLE expense_realizations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
expense_nonstock_id BIGINT UNIQUE,
|
||||||
|
realization_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
realization_unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
realization_total_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
realization_date DATE NOT NULL,
|
||||||
|
note TEXT,
|
||||||
|
created_by BIGINT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke expense_nonstocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD CONSTRAINT fk_expense_realizations_nonstock_id
|
||||||
|
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke users (created_by)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD CONSTRAINT fk_expense_realizations_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Index
|
||||||
|
CREATE INDEX idx_expense_realizations_nonstock_id ON expense_realizations (expense_nonstock_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_expense_realizations_date ON expense_realizations (realization_date);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
DROP COLUMN IF EXISTS period;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ADD COLUMN IF NOT EXISTS period INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique
|
||||||
|
ON project_flocks (
|
||||||
|
LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))),
|
||||||
|
period
|
||||||
|
)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ADD COLUMN IF NOT EXISTS period INT;
|
||||||
|
|
||||||
|
UPDATE project_flock_kandangs pfk
|
||||||
|
SET period = pf.period
|
||||||
|
FROM project_flocks pf
|
||||||
|
WHERE pfk.project_flock_id = pf.id
|
||||||
|
AND (pfk.period IS NULL OR pfk.period = 0)
|
||||||
|
AND pf.period IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ALTER COLUMN period SET DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE project_flock_kandangs
|
||||||
|
SET period = 0
|
||||||
|
WHERE period IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ALTER COLUMN period SET NOT NULL;
|
||||||
|
|
||||||
|
-- Drop period from project_flocks as the source of truth
|
||||||
|
DROP INDEX IF EXISTS project_flocks_base_period_unique;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
DROP COLUMN IF EXISTS period;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
-- Add back timestamp columns to marketing_products table
|
||||||
|
ALTER TABLE marketing_products
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Add back timestamp columns to marketing_delivery_products table
|
||||||
|
ALTER TABLE marketing_delivery_products
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
-- Drop timestamp columns from marketing_products table if it exists
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'created_at') THEN
|
||||||
|
ALTER TABLE marketing_products DROP COLUMN created_at;
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'updated_at') THEN
|
||||||
|
ALTER TABLE marketing_products DROP COLUMN updated_at;
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'deleted_at') THEN
|
||||||
|
ALTER TABLE marketing_products DROP COLUMN deleted_at;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Drop timestamp columns from marketing_delivery_products table if it exists
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'created_at') THEN
|
||||||
|
ALTER TABLE marketing_delivery_products DROP COLUMN created_at;
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'updated_at') THEN
|
||||||
|
ALTER TABLE marketing_delivery_products DROP COLUMN updated_at;
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'deleted_at') THEN
|
||||||
|
ALTER TABLE marketing_delivery_products DROP COLUMN deleted_at;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
-- ============================
|
||||||
|
-- EXPENSES
|
||||||
|
-- ============================
|
||||||
|
ALTER TABLE expenses DROP COLUMN IF EXISTS grand_total;
|
||||||
|
|
||||||
|
ALTER TABLE expenses RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
ALTER TABLE expenses RENAME COLUMN expense_date TO transaction_date;
|
||||||
|
|
||||||
|
-- ============================
|
||||||
|
-- EXPENSE_REALIZATIONS
|
||||||
|
-- ============================
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
RENAME COLUMN realization_qty TO qty;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
RENAME COLUMN realization_unit_price TO price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
DROP COLUMN IF EXISTS realization_total_price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
DROP COLUMN IF EXISTS realization_date;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations DROP COLUMN IF EXISTS created_by;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
|
|
||||||
|
-- ============================
|
||||||
|
-- EXPENSE_NONSTOCKS
|
||||||
|
-- ============================
|
||||||
|
ALTER TABLE expense_nonstocks RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS total_price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks RENAME COLUMN unit_price TO price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS created_by;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP Table IF EXISTS project_budgets;
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
CREATE TABLE project_budgets (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_id BIGINT NOT NULL,
|
||||||
|
nonstock_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
price NUMERIC(15, 3) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke project_flocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
ADD CONSTRAINT fk_project_budgets_project_flock_id
|
||||||
|
FOREIGN KEY (project_flock_id) REFERENCES project_flocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
-- Tambahkan Foreign Key ke nonstocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
ADD CONSTRAINT fk_project_budgets_nonstock_id
|
||||||
|
FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
-- Index
|
||||||
|
CREATE INDEX idx_project_budgets_project_flock_id ON project_budgets (project_flock_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_project_budgets_nonstock_id ON project_budgets (nonstock_id);
|
||||||
@@ -82,7 +82,7 @@ func Run(db *gorm.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := seedTransferStock(tx, adminID); err != nil {
|
if err := seedTransferStock(tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("✅ Master data seeding completed")
|
fmt.Println("✅ Master data seeding completed")
|
||||||
@@ -235,13 +235,14 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
seeds := []struct {
|
seeds := []struct {
|
||||||
Name string
|
Name string
|
||||||
Status utils.KandangStatus
|
Status utils.KandangStatus
|
||||||
|
Capacity float64
|
||||||
Location string
|
Location string
|
||||||
PicKey string
|
PicKey string
|
||||||
}{
|
}{
|
||||||
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
|
||||||
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
|
||||||
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
|
||||||
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]uint, len(seeds))
|
result := make(map[string]uint, len(seeds))
|
||||||
@@ -571,52 +572,54 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Flags: []utils.FlagType{utils.FlagDOC},
|
Flags: []utils.FlagType{utils.FlagDOC},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Afkir",
|
Name: "Ayam Pullet",
|
||||||
Brand: "-",
|
Brand: "MBU Pullet",
|
||||||
Sku: "1",
|
Sku: "PLT0001",
|
||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Pullet",
|
||||||
Price: 1,
|
Price: 15000,
|
||||||
|
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||||
|
Flags: []utils.FlagType{utils.FlagPullet},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Mati",
|
Name: "Ayam Afkir",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "2",
|
Sku: "1",
|
||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Culling",
|
Name: "Ayam Mati",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "3",
|
Sku: "2",
|
||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Konsumsi Baik",
|
Name: "Ayam Culling",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "4",
|
Sku: "3",
|
||||||
Uom: "Unit",
|
Uom: "Ekor",
|
||||||
Category: "Telur",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Pecah",
|
Name: "Telur Konsumsi Baik",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "5",
|
Sku: "4",
|
||||||
Uom: "Unit",
|
Uom: "Unit",
|
||||||
Category: "Telur",
|
Category: "Telur",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Telur Pecah",
|
||||||
|
Brand: "-",
|
||||||
|
Sku: "5",
|
||||||
|
Uom: "Unit",
|
||||||
|
Category: "Telur",
|
||||||
|
Price: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "281 SPECIAL STARTER",
|
Name: "281 SPECIAL STARTER",
|
||||||
@@ -689,7 +692,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
var existing entity.ProductSupplier
|
var existing entity.ProductSupplier
|
||||||
err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error
|
err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
link := entity.ProductSupplier{ProductID: product.Id, SupplierID: supplierID}
|
link := entity.ProductSupplier{ProductId: product.Id, SupplierId: supplierID}
|
||||||
if err := tx.Create(&link).Error; err != nil {
|
if err := tx.Create(&link).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -762,7 +765,7 @@ func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers
|
|||||||
var existing entity.NonstockSupplier
|
var existing entity.NonstockSupplier
|
||||||
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
|
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
link := entity.NonstockSupplier{NonstockID: nonstock.Id, SupplierID: supplierID}
|
link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID}
|
||||||
if err := tx.Create(&link).Error; err != nil {
|
if err := tx.Create(&link).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -926,7 +929,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedTransferStock(tx *gorm.DB, createdBy uint) error {
|
func seedTransferStock(tx *gorm.DB) error {
|
||||||
|
|
||||||
transfer := entity.StockTransfer{
|
transfer := entity.StockTransfer{
|
||||||
FromWarehouseId: 1,
|
FromWarehouseId: 1,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Area struct {
|
type Area struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
|
|
||||||
type Bank struct {
|
type Bank struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
|
||||||
Alias string `gorm:"not null;size:5"`
|
Alias string `gorm:"not null;size:5"`
|
||||||
Owner *string `gorm:""`
|
Owner *string `gorm:"type:varchar(50)"`
|
||||||
AccountNumber string `gorm:"not null;size:50"`
|
AccountNumber string `gorm:"not null;size:50"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
|
|
||||||
type Customer struct {
|
type Customer struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
|
||||||
PicId uint `gorm:"not null"`
|
PicId uint `gorm:"not null"`
|
||||||
Type string `gorm:"not null;size:50"`
|
Type string `gorm:"not null;size:50"`
|
||||||
Address string `gorm:"not null"`
|
Address string `gorm:"not null"`
|
||||||
Phone string `gorm:"not null;size:20"`
|
Phone string `gorm:"not null;size:20"`
|
||||||
Email string `gorm:"not null"`
|
Email string `gorm:"type:varchar(50);not null"`
|
||||||
AccountNumber string `gorm:"not null;size:50"`
|
AccountNumber string `gorm:"not null;size:50"`
|
||||||
Balance float64 `gorm:"default:0"`
|
Balance float64 `gorm:"default:0"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Expense struct {
|
||||||
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
ReferenceNumber string `gorm:"type:varchar(50);uniqueIndex"`
|
||||||
|
SupplierId uint64 `gorm:""`
|
||||||
|
Category string `gorm:"type:varchar(50);not null"`
|
||||||
|
PoNumber string `gorm:"type:varchar(50)"`
|
||||||
|
DocumentPath sql.NullString `gorm:"type:json"`
|
||||||
|
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
|
||||||
|
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
|
||||||
|
TransactionDate time.Time `gorm:"type:date;not null"`
|
||||||
|
Notes string `gorm:"type:text;column:notes"`
|
||||||
|
CreatedBy uint64 `gorm:""`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExpenseNonstock struct {
|
||||||
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
ExpenseId *uint64 `gorm:""`
|
||||||
|
ProjectFlockKandangId *uint64 `gorm:""`
|
||||||
|
KandangId *uint64 `gorm:""`
|
||||||
|
NonstockId *uint64 `gorm:""`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
|
||||||
|
Notes string `gorm:"type:text;column:notes"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
||||||
|
|
||||||
|
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||||
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
||||||
|
Realization *ExpenseRealization `gorm:"foreignKey:Id;references:ExpenseNonstockId"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExpenseRealization struct {
|
||||||
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
ExpenseNonstockId *uint64 `gorm:""`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null;"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);not null;"`
|
||||||
|
Notes string `gorm:"type:text;"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
||||||
|
|
||||||
|
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Fcr struct {
|
type Fcr struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const (
|
|||||||
|
|
||||||
type Flag struct {
|
type Flag struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:flags_unique_flagable"`
|
Name string `gorm:"type:varchar(50);size:50;not null;uniqueIndex:flags_unique_flagable"`
|
||||||
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"`
|
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"`
|
||||||
FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
|
FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Kandang struct {
|
type Kandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
||||||
Status string `gorm:"type:varchar(50);not null"`
|
Status string `gorm:"type:varchar(50);not null"`
|
||||||
LocationId uint `gorm:"not null"`
|
LocationId uint `gorm:"not null"`
|
||||||
PicId uint `gorm:"not null"`
|
Capacity float64 `gorm:"not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
PicId uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
|
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||||
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingKandangTransfer struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
KandangId uint
|
||||||
|
ProductWarehouseId uint
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
LayingTransferId uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransfer struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
TransferNumber string `gorm:"uniqueIndex;not null"`
|
||||||
|
FromProjectFlockId uint `gorm:"not null"`
|
||||||
|
ToProjectFlockId uint `gorm:"not null"`
|
||||||
|
TransferDate time.Time `gorm:"type:date;not null"`
|
||||||
|
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
UsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
Notes string `gorm:"type:text"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||||
|
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
|
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransferSource struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
|
SourceProjectFlockKandangId uint `gorm:"not null"`
|
||||||
|
ProductWarehouseId *uint `gorm:""`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Note string `gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
|
||||||
|
SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransferTarget struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
|
TargetProjectFlockKandangId uint `gorm:"not null"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
ProductWarehouseId *uint `gorm:""`
|
||||||
|
Note string `gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
|
||||||
|
TargetProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:TargetProjectFlockKandangId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Location struct {
|
type Location struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
|
||||||
Address string `gorm:"not null"`
|
Address string `gorm:"not null"`
|
||||||
AreaId uint `gorm:"not null"`
|
AreaId uint `gorm:"not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
@@ -16,6 +16,7 @@ type Location struct {
|
|||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
||||||
|
Kandangs []Kandang `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Marketing struct {
|
||||||
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
SoNumber string `gorm:"uniqueIndex;not null"`
|
||||||
|
CustomerId uint `gorm:"not null"`
|
||||||
|
SoDocs string `gorm:"type:varchar(20)"`
|
||||||
|
SoDate time.Time `gorm:"type:date;not null"`
|
||||||
|
SalesPersonId uint `gorm:"not null"`
|
||||||
|
Notes string `gorm:"type:text"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
Customer Customer `gorm:"foreignKey:CustomerId;references:Id"`
|
||||||
|
SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"`
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingDeliveryProduct struct {
|
||||||
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||||
|
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||||
|
|
||||||
|
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
type MarketingProduct struct {
|
||||||
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
MarketingId uint `gorm:"not null"`
|
||||||
|
ProductWarehouseId uint `gorm:"not null"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
AvgWeight float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
TotalWeight float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
|
||||||
|
Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"`
|
||||||
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
DeliveryProduct *MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||||
|
}
|
||||||
@@ -8,15 +8,15 @@ import (
|
|||||||
|
|
||||||
type Nonstock struct {
|
type Nonstock struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
|
||||||
UomId uint `gorm:"not null"`
|
UomId uint `gorm:"not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
||||||
Suppliers []Supplier `gorm:"many2many:nonstock_suppliers;joinForeignKey:NonstockID;joinReferences:SupplierID"`
|
NonstockSuppliers []NonstockSupplier `gorm:"foreignKey:NonstockId;references:Id"`
|
||||||
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"`
|
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package entities
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type NonstockSupplier struct {
|
type NonstockSupplier struct {
|
||||||
NonstockID uint `gorm:"primaryKey"`
|
NonstockId uint `gorm:"not null"`
|
||||||
SupplierID uint `gorm:"primaryKey"`
|
SupplierId uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
|
Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
||||||
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type ProductCategory struct {
|
type ProductCategory struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
|
||||||
Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"`
|
Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
type Product struct {
|
type Product struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
|
||||||
Brand string `gorm:"not null"`
|
Brand string `gorm:"type:varchar(50);not null"`
|
||||||
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
|
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
|
||||||
UomId uint `gorm:"not null"`
|
UomId uint `gorm:"not null"`
|
||||||
ProductCategoryId uint `gorm:"not null"`
|
ProductCategoryId uint `gorm:"not null"`
|
||||||
@@ -22,9 +22,9 @@ type Product struct {
|
|||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
||||||
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
||||||
Suppliers []Supplier `gorm:"many2many:product_suppliers;joinForeignKey:ProductID;joinReferences:SupplierID"`
|
ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
|
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package entities
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ProductSupplier struct {
|
type ProductSupplier struct {
|
||||||
ProductID uint `gorm:"primaryKey"`
|
ProductId uint `gorm:"not null"`
|
||||||
SupplierID uint `gorm:"primaryKey"`
|
SupplierId uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
|
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectBudget struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
|
Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"`
|
||||||
|
ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"`
|
||||||
|
}
|
||||||
@@ -12,13 +12,16 @@ type ProjectChickin struct {
|
|||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
ChickInDate time.Time `gorm:"not null"`
|
ChickInDate time.Time `gorm:"not null"`
|
||||||
Quantity float64 `gorm:"not null"`
|
ProductWarehouseId uint `gorm:"not null"`
|
||||||
Note string `gorm:"type:text"`
|
UsageQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Notes string `gorm:"type:text"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectFlockPopulation struct {
|
type ProjectFlockPopulation struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
ProjectChickinId uint `gorm:"not null"`
|
||||||
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
ProductWarehouseId uint `gorm:"not null"`
|
||||||
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
TotalQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
|
TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
Notes string `gorm:"type:text"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ type ProjectFlock struct {
|
|||||||
Category string `gorm:"type:varchar(20);not null"`
|
Category string `gorm:"type:varchar(20);not null"`
|
||||||
FcrId uint `gorm:"not null"`
|
FcrId uint `gorm:"not null"`
|
||||||
LocationId uint `gorm:"not null"`
|
LocationId uint `gorm:"not null"`
|
||||||
Period int `gorm:"not null"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ package entities
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ProjectFlockKandang struct {
|
type ProjectFlockKandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Period int `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
|
||||||
|
|
||||||
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Purchase struct {
|
||||||
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
PrNumber string `gorm:"not null"`
|
||||||
|
PoNumber *string
|
||||||
|
PoDate *time.Time
|
||||||
|
SupplierId uint `gorm:"not null"`
|
||||||
|
CreditTerm *int
|
||||||
|
DueDate *time.Time
|
||||||
|
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Notes *string
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
Items []PurchaseItem `gorm:"foreignKey:PurchaseId"`
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseItem struct {
|
||||||
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
PurchaseId uint `gorm:"not null"`
|
||||||
|
ProductId uint `gorm:"not null"`
|
||||||
|
WarehouseId uint `gorm:"not null"`
|
||||||
|
ProductWarehouseId *uint
|
||||||
|
ReceivedDate *time.Time
|
||||||
|
TravelNumber *string
|
||||||
|
TravelNumberDocs *string
|
||||||
|
VehicleNumber *string
|
||||||
|
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||||
|
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
|
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StockAllocationStatusPending = "PENDING"
|
||||||
|
StockAllocationStatusActive = "ACTIVE"
|
||||||
|
StockAllocationStatusReleased = "RELEASED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StockAllocation links a usable record (consumption) with an incoming stock record.
|
||||||
|
// The combination lets us trace FIFO deductions while keeping each module focused on its own fields.
|
||||||
|
type StockAllocation struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
ProductWarehouseId uint `gorm:"not null;index"`
|
||||||
|
StockableType string `gorm:"size:100;not null;index:stock_allocations_lookup,priority:1"`
|
||||||
|
StockableId uint `gorm:"not null;index:stock_allocations_lookup,priority:2"`
|
||||||
|
UsableType string `gorm:"size:100;not null;index:stock_allocations_usage_lookup,priority:1"`
|
||||||
|
UsableId uint `gorm:"not null;index:stock_allocations_usage_lookup,priority:2"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Status string `gorm:"size:20;not null;default:ACTIVE"`
|
||||||
|
Note *string `gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
ReleasedAt *time.Time `gorm:"index"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
@@ -8,14 +8,14 @@ import (
|
|||||||
|
|
||||||
type Supplier struct {
|
type Supplier struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
|
||||||
Alias string `gorm:"not null;size:5"`
|
Alias string `gorm:"not null;size:5"`
|
||||||
Pic string `gorm:"not null"`
|
Pic string `gorm:"type:varchar(50);not null"`
|
||||||
Type string `gorm:"not null;size:50"`
|
Type string `gorm:"not null;size:50"`
|
||||||
Category string `gorm:"not null;size:20"`
|
Category string `gorm:"not null;size:20"`
|
||||||
Hatchery *string `gorm:"size:255"`
|
Hatchery *string `gorm:"type:varchar(50)"`
|
||||||
Phone string `gorm:"not null;size:20"`
|
Phone string `gorm:"not null;size:20"`
|
||||||
Email string `gorm:"not null"`
|
Email string `gorm:"type:varchar(50);not null"`
|
||||||
Address string `gorm:"not null"`
|
Address string `gorm:"not null"`
|
||||||
Npwp *string `gorm:"size:50"`
|
Npwp *string `gorm:"size:50"`
|
||||||
AccountNumber *string `gorm:"size:50"`
|
AccountNumber *string `gorm:"size:50"`
|
||||||
@@ -26,5 +26,7 @@ type Supplier struct {
|
|||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
ProductSuppliers []ProductSupplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
NonstockSuppliers []NonstockSupplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Uom struct {
|
type Uom struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
IdUser int64 `gorm:"uniqueIndex"`
|
IdUser int64 `gorm:"uniqueIndex"`
|
||||||
Email string `gorm:"uniqueIndex"`
|
Email string `gorm:"type:varchar(50);uniqueIndex"`
|
||||||
Name string `gorm:"not null"`
|
Name string `gorm:"type:varchar(50);not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Warehouse struct {
|
type Warehouse struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null"`
|
Name string `gorm:"type:varchar(50);not null"`
|
||||||
Type string `gorm:"not null"`
|
Type string `gorm:"not null"`
|
||||||
AreaId uint `gorm:"not null"`
|
AreaId uint `gorm:"not null"`
|
||||||
LocationId *uint
|
LocationId *uint
|
||||||
|
|||||||
@@ -105,6 +105,14 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||||
|
user, ok := AuthenticatedUser(c)
|
||||||
|
if !ok || user == nil || user.Id == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
return user.Id, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AuthDetails returns the full authentication context (token, claims, user).
|
// AuthDetails returns the full authentication context (token, claims, user).
|
||||||
func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) {
|
func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) {
|
||||||
value := c.Locals(authContextLocalsKey)
|
value := c.Locals(authContextLocalsKey)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
flat := dto.ToApprovalDTOs(records)
|
flat := dto.ToApprovalDTOs(records)
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.SuccessWithPaginate[dto.ApprovalBaseDTO]{
|
JSON(response.SuccessWithPaginate[dto.ApprovalRelationDTO]{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get All approvals successfully",
|
Message: "Get All approvals successfully",
|
||||||
|
|||||||
@@ -10,23 +10,25 @@ import (
|
|||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApprovalBaseDTO struct {
|
type ApprovalRelationDTO struct {
|
||||||
StepNumber uint16 `json:"step_number"`
|
Id uint `json:"id"`
|
||||||
StepName string `json:"step_name"`
|
StepNumber uint16 `json:"step_number"`
|
||||||
Action *string `json:"action"`
|
StepName string `json:"step_name"`
|
||||||
Notes *string `json:"notes"`
|
Action *string `json:"action"`
|
||||||
ActionBy userDTO.UserBaseDTO `json:"action_by"`
|
Notes *string `json:"notes"`
|
||||||
ActionAt time.Time `json:"action_at"`
|
ActionBy userDTO.UserRelationDTO `json:"action_by"`
|
||||||
|
ActionAt time.Time `json:"action_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApprovalGroupDTO struct {
|
type ApprovalGroupDTO struct {
|
||||||
StepNumber uint16 `json:"step_number"`
|
StepNumber uint16 `json:"step_number"`
|
||||||
StepName string `json:"step_name"`
|
StepName string `json:"step_name"`
|
||||||
Approvals []ApprovalBaseDTO `json:"approvals"`
|
Approvals []ApprovalRelationDTO `json:"approvals"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
func ToApprovalDTO(e entity.Approval) ApprovalRelationDTO {
|
||||||
dto := ApprovalBaseDTO{
|
dto := ApprovalRelationDTO{
|
||||||
|
Id: e.Id,
|
||||||
Notes: e.Notes,
|
Notes: e.Notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,10 +54,10 @@ func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if e.ActionUser != nil && e.ActionUser.Id != 0 {
|
if e.ActionUser != nil && e.ActionUser.Id != 0 {
|
||||||
user := userDTO.ToUserBaseDTO(*e.ActionUser)
|
user := userDTO.ToUserRelationDTO(*e.ActionUser)
|
||||||
dto.ActionBy = user
|
dto.ActionBy = user
|
||||||
} else if e.ActionBy != nil && *e.ActionBy != 0 {
|
} else if e.ActionBy != nil && *e.ActionBy != 0 {
|
||||||
dto.ActionBy = userDTO.UserBaseDTO{
|
dto.ActionBy = userDTO.UserRelationDTO{
|
||||||
Id: *e.ActionBy,
|
Id: *e.ActionBy,
|
||||||
IdUser: int64(*e.ActionBy),
|
IdUser: int64(*e.ActionBy),
|
||||||
}
|
}
|
||||||
@@ -69,8 +71,8 @@ func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
|||||||
return dto
|
return dto
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToApprovalDTOs(items []entity.Approval) []ApprovalBaseDTO {
|
func ToApprovalDTOs(items []entity.Approval) []ApprovalRelationDTO {
|
||||||
result := make([]ApprovalBaseDTO, len(items))
|
result := make([]ApprovalRelationDTO, len(items))
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
result[i] = ToApprovalDTO(item)
|
result[i] = ToApprovalDTO(item)
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ func ToApprovalGroupDTOs(items []entity.Approval) []ApprovalGroupDTO {
|
|||||||
|
|
||||||
type groupAccumulator struct {
|
type groupAccumulator struct {
|
||||||
StepName string
|
StepName string
|
||||||
Approvals []ApprovalBaseDTO
|
Approvals []ApprovalRelationDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
groups := make(map[uint16]*groupAccumulator)
|
groups := make(map[uint16]*groupAccumulator)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package approvals
|
|||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
|
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
|
||||||
_ = u
|
_ = u
|
||||||
ctrl := controller.NewApprovalController(s)
|
ctrl := controller.NewApprovalController(s)
|
||||||
|
|
||||||
route := v1.Group("/approvals")
|
route := v1.Group("/approvals")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClosingController struct {
|
||||||
|
ClosingService service.ClosingService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClosingController(closingService service.ClosingService) *ClosingController {
|
||||||
|
return &ClosingController{
|
||||||
|
ClosingService: closingService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ClosingService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all closings successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToClosingListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) 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.ClosingService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get closing successfully",
|
||||||
|
Data: dto.ToClosingListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user