mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
Compare commits
423 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1ba13de76 | |||
| e30ef5ef10 | |||
| bb76d27f25 | |||
| dbb13da7c4 | |||
| ac8536a4a1 | |||
| 96c2917834 | |||
| c3302397cc | |||
| c7ae836cf0 | |||
| 20f8a45823 | |||
| 67ddd8e667 | |||
| ebf0f8c5ab | |||
| 7dc5c9e9a5 | |||
| 306cf11fee | |||
| 9ee3b7582c | |||
| cdfa77566c | |||
| 1c875a916b | |||
| 85dc0ecd13 | |||
| c9633d1308 | |||
| b156e06cee | |||
| cd14de4dd2 | |||
| 54487b0fcf | |||
| a9037991ef | |||
| 12e5706318 | |||
| 3e575d96a7 | |||
| 98a34a1640 | |||
| c643e66282 | |||
| 9c3d0a44a6 | |||
| e935843cba | |||
| e33b23a2aa | |||
| c55fdb75a7 | |||
| 3a27917afc | |||
| c0132e5880 | |||
| 3d13cd966a | |||
| b41bb79125 | |||
| a2b8ebe665 | |||
| 2d8f20b70e | |||
| 824eb5905f | |||
| 817b6f82d0 | |||
| cbd3047a17 | |||
| ff4b4afcca | |||
| 240cd72204 | |||
| eae69a08fc | |||
| 17be6abc49 | |||
| ef117e66d1 | |||
| 4dfb988994 | |||
| dc726c49cf | |||
| a82df468d2 | |||
| 1af8f0a726 | |||
| 63068b8c3e | |||
| 5461c8b0ce | |||
| 5dc5f4c589 | |||
| ab9c7c216a | |||
| faa0861451 | |||
| 2eade07f0a | |||
| dbb9db960f | |||
| fa6d82b79a | |||
| 207382b3b0 | |||
| e551995c66 | |||
| cb076d92ac | |||
| f5c80fa560 | |||
| 14a4d9e944 | |||
| 84da0c27e0 | |||
| 047162699e | |||
| c95f90f0b9 | |||
| 9e0b4be4dd | |||
| f2df7f4847 | |||
| d675b1e826 | |||
| e52a02b1c0 | |||
| 096a446450 | |||
| 1b23861656 | |||
| a7069a2e50 | |||
| 3bfc401206 | |||
| 21d22c20a3 | |||
| d9a1372077 | |||
| 40f192660d | |||
| afe4b2ffe3 | |||
| eef254021c | |||
| cd739f41b9 | |||
| 8f77031e02 | |||
| 062a7937e2 | |||
| 4094d38d7b | |||
| cf7b3418a5 | |||
| d5bc6838c8 | |||
| efaeb89ca1 | |||
| a0a143b8ac | |||
| cbb3368141 | |||
| fc49cef781 | |||
| c79e35c217 | |||
| f60564d673 | |||
| b8425c0f58 | |||
| 0de2021308 | |||
| 3ada837b8b | |||
| c062d838e0 | |||
| 4ce7611c26 | |||
| 2dd3e3e271 | |||
| e98d0a9fa1 | |||
| 08c8c4a747 | |||
| de6304332b | |||
| f073bcc2c1 | |||
| 4853891191 | |||
| 086184bbaa | |||
| 4161dcfbdd | |||
| d0309f25dd | |||
| 59ebe29ec8 | |||
| 2b6ba3a41d | |||
| bb1e6833f0 | |||
| a536094481 | |||
| c33cc05f72 | |||
| 3f9865d267 | |||
| 822ca0268e | |||
| 16d1358b3a | |||
| e00f168a15 | |||
| 79d488c979 | |||
| 2effa08648 | |||
| 576f8083a3 | |||
| d7c543bc9d | |||
| 4a2a80916f | |||
| 511e5501bb | |||
| 0fbf04fc1d | |||
| 536e76d481 | |||
| 29aa737422 | |||
| 26f2f3ccbf | |||
| 4b147a3be7 | |||
| 7094d90034 | |||
| e6094528b5 | |||
| 347f21b45c | |||
| 89b23b0653 | |||
| e2a6c2a733 | |||
| e0e2d91db5 | |||
| 6e176688fa | |||
| cbb7f45c5f | |||
| fc9197d00a | |||
| f8e0614d50 | |||
| a8434a5246 | |||
| 9f239b1840 | |||
| 167fd6d6cb | |||
| ec2aca936c | |||
| f701b30cb3 | |||
| 0a18753dde | |||
| 4638fba318 | |||
| 296e8e4c18 | |||
| a586fe3781 | |||
| 2d3f7f7ef9 | |||
| 67f7ec3a40 | |||
| 2fbf66f9f7 | |||
| 70b2a5a2d1 | |||
| 008709c19c | |||
| 6572176cca | |||
| 4c63bd14c3 | |||
| c593df661c | |||
| ee2db748ea | |||
| 5afee298b0 | |||
| 2bc67a8433 | |||
| b4ccd33ea0 | |||
| c279303b99 | |||
| b8a769dc72 | |||
| 8c883669d3 | |||
| 1e9a637202 | |||
| fc14f9a98f | |||
| 17269d701c | |||
| c3305d3089 | |||
| b43e2b44ec | |||
| 6e3a8f3551 | |||
| c064fb1765 | |||
| 4af631a1d3 | |||
| 91e4762945 | |||
| b8403f1c7e | |||
| 415d5c0e67 | |||
| 1bca29cd31 | |||
| 753d8575c4 | |||
| 4c5266da23 | |||
| d79b1868fc | |||
| 33a9d7806e | |||
| ea294c6a18 | |||
| d572d04e3b | |||
| 730fb22cc2 | |||
| 94fc9219af | |||
| 5650253307 | |||
| 79bbe61dab | |||
| fa5609c183 | |||
| beee88322a | |||
| 1b464884c5 | |||
| 31699f4162 | |||
| 966d616022 | |||
| e667d88218 | |||
| 002981e63b | |||
| 1d0ef8fb93 | |||
| 53c321c3e3 | |||
| d76db26a4d | |||
| 91ad7ad5e0 | |||
| 29f0fd6edb | |||
| 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 | |||
| 770adbd3ff | |||
| 50119ac538 | |||
| 98e0d56c64 | |||
| f5c0441337 | |||
| c72db5bd18 | |||
| 86f37a89c1 | |||
| 20f1be2ef8 | |||
| 6ab6ee8070 | |||
| 4f4c6d66d4 | |||
| 672c76d26d | |||
| 4b39f52d5a | |||
| f869943573 | |||
| 219a6a39ed | |||
| c91d84b652 | |||
| bf14ab7865 | |||
| b459245c5c | |||
| 31bb28f7da | |||
| a390d1d23a | |||
| 614da067f7 | |||
| 7f00a5a7a4 | |||
| 9e08b9c44c | |||
| d4a0d5c68b | |||
| c4448594e2 | |||
| fb831208f4 | |||
| 054ad2ad20 | |||
| cb4d27de7e | |||
| 8ae614540f | |||
| cedd5365d8 | |||
| 63fb7d37f1 | |||
| 313276001c | |||
| 3065669d60 | |||
| 29b5a2aa81 | |||
| ef99a4a3c1 | |||
| 3a162972ba | |||
| 7f2175a8cf | |||
| 222d53aa37 | |||
| aeeb5a38c1 | |||
| 79b3dd47b8 | |||
| 69ded31eb1 | |||
| 171191c97d | |||
| 587cfabb4a | |||
| 3ede6461cf | |||
| 1dfd1f747e | |||
| 40665b0d8f | |||
| 94f4929749 | |||
| ad815b3412 | |||
| d41e16cab9 | |||
| 22e4728738 | |||
| 501b6f8440 | |||
| 3ea5bf6787 | |||
| 0a4e06614b | |||
| df7cf86711 | |||
| 26825ab831 | |||
| 1b02b660b9 | |||
| 4c6ead4272 | |||
| f33eb7fcc7 | |||
| 00837e0da2 | |||
| 346ae15314 | |||
| 45f41f87ff | |||
| 02defcb86a | |||
| 0c791898ff | |||
| 1dbf3ce93e | |||
| dae9fe9e01 | |||
| b330fbfeb1 | |||
| 56b1134872 | |||
| 6dd45c3289 | |||
| e2818b11f0 | |||
| ddcda59239 | |||
| 85dfc33191 | |||
| bb60e987e5 | |||
| d5e8487f44 | |||
| ab8c5d2ec4 | |||
| 452403d71e | |||
| 5d676d5993 | |||
| 3ed2c9027a | |||
| 8d0bd3724d | |||
| 95d6eb3445 | |||
| 69f5ec8775 | |||
| 445789edfe | |||
| 0dedd1149e | |||
| 04ffa66a59 | |||
| e4799fa2dd | |||
| 55b14f5fc7 | |||
| 13c04460f0 | |||
| 7f39ad8fed | |||
| 0824f03a61 | |||
| a4ac431ed4 | |||
| b1b63d266a | |||
| 1afbdea4ff | |||
| 542e503360 | |||
| 9b2b62429c | |||
| ee033b8fe6 | |||
| a1f579f616 | |||
| 748c959dbe | |||
| 7b99b39529 | |||
| c61bccb700 | |||
| 81d6b2c6fc | |||
| 5c3787886b | |||
| 4218298234 | |||
| 83c3e61113 | |||
| f15e0d62e3 | |||
| 68a670a2bd | |||
| a45c20d2ff | |||
| 79700420d4 | |||
| c9b4b3008e | |||
| 47823963ae | |||
| 151f66b4cc | |||
| 8c0790627a | |||
| 62a1011a4b | |||
| b44322e448 | |||
| 3a8a1318f5 | |||
| 3ec05eb76f | |||
| 9f26d5c784 | |||
| 6c7ab8a0f8 | |||
| f6f62246c6 | |||
| 0ffb8a44f2 | |||
| 4107cf19ec | |||
| d1b377ddac | |||
| 9b016dc30a | |||
| 7392d8a679 | |||
| 6628356958 | |||
| 5283aed996 | |||
| ce28429efd | |||
| 05bf2f4fff | |||
| a0a416c330 | |||
| cd4c908334 | |||
| 81cbb230f3 | |||
| 91b320d489 | |||
| a0bdc7b23c | |||
| e239246d02 |
@@ -3,7 +3,7 @@ root = "."
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
cmd = "go build -o ./tmp/main ./cmd/api"
|
cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
|
||||||
bin = "tmp/main"
|
bin = "tmp/main"
|
||||||
full_bin = "APP_ENV=dev ./tmp/main"
|
full_bin = "APP_ENV=dev ./tmp/main"
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
|||||||
@@ -1,54 +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_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_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":"client:lti","redirect_uri":"http://localhost:8081/api/sso/callback","scope":"openid profile","default_return_uri":"http://localhost:3000","allowed_return_origins":["http://localhost:3000"],"sync_secret":"changeme"}}
|
|
||||||
@@ -10,8 +10,13 @@ bin/
|
|||||||
*.exe
|
*.exe
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
Makefile
|
||||||
|
docker-compose.local.yml
|
||||||
|
docker-compose.yaml
|
||||||
|
Dockerfile.local
|
||||||
# Go build cache
|
# Go build cache
|
||||||
.gocache/
|
.gocache/
|
||||||
|
vendor
|
||||||
|
|
||||||
# Logs & reports
|
# Logs & reports
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
+82
-68
@@ -1,76 +1,90 @@
|
|||||||
stages: [notify]
|
stages:
|
||||||
|
- deploy
|
||||||
|
|
||||||
# --- Notify when MR is opened/updated ---
|
deploy-dev:
|
||||||
notify_discord_mr:
|
stage: deploy
|
||||||
stage: notify
|
|
||||||
image: alpine:3.20
|
image: alpine:3.20
|
||||||
rules:
|
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
||||||
variables:
|
variables:
|
||||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
DEPLOY_APP: "LTI-MBUGROUP"
|
||||||
before_script:
|
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
||||||
- apk add --no-cache curl jq
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
script: |
|
GIT_DEPTH: "1"
|
||||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
|
||||||
|
|
||||||
jq -n \
|
before_script:
|
||||||
--arg repo "$CI_PROJECT_PATH" \
|
- echo "🧰 Installing dependencies..."
|
||||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
- apk update && apk add --no-cache openssh git curl bash
|
||||||
--arg url "$MR_URL" \
|
|
||||||
--arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
|
# Setup SSH di runner
|
||||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
- mkdir -p ~/.ssh
|
||||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
- chmod 600 ~/.ssh/id_rsa
|
||||||
'{
|
- eval "$(ssh-agent -s)"
|
||||||
username: "CI Bot - BE",
|
- ssh-add ~/.ssh/id_rsa
|
||||||
embeds: [{
|
|
||||||
title: "📣 [LTI API] Merge Request Opened/Updated",
|
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
|
||||||
description: ($mr + " in " + $repo),
|
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
||||||
url: $url,
|
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||||
color: 3447003,
|
|
||||||
fields: [
|
script:
|
||||||
{name: "Author", value: $requestor, inline: true},
|
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
|
||||||
{name: "Title", value: $title}
|
- >
|
||||||
|
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;
|
||||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
|
||||||
|
|
||||||
# --- Notify when MR is merged ---
|
echo "📡 Sending notification to Discord...";
|
||||||
notify_discord_merge:
|
curl -sS -H "Content-Type: application/json" \
|
||||||
stage: notify
|
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
||||||
image: alpine:3.20
|
|
||||||
rules:
|
|
||||||
# Only run for merge request pipelines that are in merged state
|
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
|
|
||||||
variables:
|
|
||||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
|
||||||
before_script:
|
|
||||||
- apk add --no-cache curl jq
|
|
||||||
script: |
|
|
||||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
|
||||||
|
|
||||||
jq -n \
|
only:
|
||||||
--arg repo "$CI_PROJECT_PATH" \
|
- development
|
||||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
|
||||||
--arg url "$MR_URL" \
|
environment:
|
||||||
--arg requestor "${CI_MERGE_REQUEST_AUTHOR}" \
|
name: development
|
||||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
|
||||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
|
||||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
|
||||||
'{
|
|
||||||
username: "CI Bot - BE",
|
|
||||||
embeds: [{
|
|
||||||
title: "✅ [LTI API] Merge Request Merged",
|
|
||||||
description: ($mr + " has been merged into " + $repo),
|
|
||||||
url: $url,
|
|
||||||
color: 3066993,
|
|
||||||
fields: [
|
|
||||||
{name: "Author", value: $requestor, inline: true},
|
|
||||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
|
||||||
{name: "Title", value: $title}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}' \
|
|
||||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# --- Load .env kalau ada, dan export ke shell child ---
|
|
||||||
ifneq (,$(wildcard .env))
|
|
||||||
include .env
|
|
||||||
export
|
|
||||||
endif
|
|
||||||
|
|
||||||
# --- Konfigurasi umum ---
|
|
||||||
COMPOSE ?= docker compose -f docker-compose.local.yml
|
|
||||||
NETWORK ?= lti-api_go-network
|
|
||||||
MIGRATE_IMAGE ?= migrate/migrate
|
|
||||||
MIGRATIONS_DIR := $(PWD)/internal/database/migrations
|
|
||||||
|
|
||||||
# Fallback agar tetap jalan meski .env kosong
|
|
||||||
DB_HOST ?= postgresdb
|
|
||||||
DB_PORT ?= 5432
|
|
||||||
DB_USER ?= postgres
|
|
||||||
DB_PASSWORD ?= postgres
|
|
||||||
DB_NAME ?= db_lti_erp
|
|
||||||
|
|
||||||
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
|
||||||
|
|
||||||
# Tunggu DB ready memakai pg_isready dari image postgres
|
|
||||||
WAIT_DB := docker run --rm --network $(NETWORK) postgres:alpine \
|
|
||||||
sh -c 'until pg_isready -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -d $(DB_NAME); do echo "waiting for postgres..."; sleep 1; done'
|
|
||||||
|
|
||||||
# Default target
|
|
||||||
.DEFAULT_GOAL := start
|
|
||||||
|
|
||||||
# --- Daftar phony targets ---
|
|
||||||
.PHONY: start build test lint gen \
|
|
||||||
db-up wait-db \
|
|
||||||
migration-% migrate-up migrate-down migrate-fresh \
|
|
||||||
seed \
|
|
||||||
docker-local docker-down docker-nuke docker-cache psql
|
|
||||||
|
|
||||||
# --- Go workflow ---
|
|
||||||
start:
|
|
||||||
@go run cmd/api/main.go
|
|
||||||
|
|
||||||
build:
|
|
||||||
@go build -o tmp/app ./cmd/api
|
|
||||||
|
|
||||||
test:
|
|
||||||
@go test ./test/...
|
|
||||||
|
|
||||||
lint:
|
|
||||||
@golangci-lint run
|
|
||||||
|
|
||||||
# --- Compose / DB helpers ---
|
|
||||||
db-up:
|
|
||||||
@$(COMPOSE) up -d postgresdb
|
|
||||||
|
|
||||||
wait-db:
|
|
||||||
@$(WAIT_DB)
|
|
||||||
|
|
||||||
# --- Migration (pembuatan file) ---
|
|
||||||
# Contoh: make migration-create_users_table
|
|
||||||
# ":" akan diubah ke "_" (biar aman untuk nama file)
|
|
||||||
migration-%:
|
|
||||||
@migrate create -ext sql -dir internal/database/migrations $(subst :,_,$*)
|
|
||||||
|
|
||||||
# --- Migration (apply via docker image 'migrate') ---
|
|
||||||
migrate-up: db-up wait-db
|
|
||||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
|
|
||||||
|
|
||||||
# Contoh:
|
|
||||||
# make migrate-down step=2 → rollback 2 step
|
|
||||||
# make migrate-down → rollback semua
|
|
||||||
|
|
||||||
migrate-down: db-up wait-db
|
|
||||||
@if [ -n "$(step)" ]; then \
|
|
||||||
echo "⬇️ Migrating down $(step) step(s)..."; \
|
|
||||||
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down $(step); \
|
|
||||||
else \
|
|
||||||
echo "⬇️ Migrating down ALL steps..."; \
|
|
||||||
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
migrate-fresh: migrate-down migrate-up
|
|
||||||
@true
|
|
||||||
|
|
||||||
# Pakai: make migrate-force v=20250917120000
|
|
||||||
migrate-force:
|
|
||||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" force $(v)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Seeder ---
|
|
||||||
seed: db-up wait-db
|
|
||||||
@$(COMPOSE) run --rm app go run cmd/seed/main.go
|
|
||||||
|
|
||||||
# --- Docker orchestration convenience ---
|
|
||||||
docker-local:
|
|
||||||
@$(COMPOSE) up --build -d
|
|
||||||
|
|
||||||
docker-down:
|
|
||||||
@$(COMPOSE) down --remove-orphans
|
|
||||||
|
|
||||||
# ⚠️ Akan menghapus container, images dan volumes.
|
|
||||||
docker-nuke:
|
|
||||||
@$(COMPOSE) down --rmi all --volumes --remove-orphans
|
|
||||||
|
|
||||||
docker-cache:
|
|
||||||
@docker builder prune -f
|
|
||||||
|
|
||||||
# --- PSQL shell ke DB di container ---
|
|
||||||
psql: db-up
|
|
||||||
@$(COMPOSE) exec -it postgresdb psql -U $(DB_USER) -d $(DB_NAME)
|
|
||||||
|
|
||||||
# Single feature
|
|
||||||
# example: make gen feat=product-category
|
|
||||||
|
|
||||||
# Sub feature
|
|
||||||
# make gen feat=master/area
|
|
||||||
gen:
|
|
||||||
@go run tools/gen.go $(feat)
|
|
||||||
# @goimports -w internal
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "VERCEL_GIT_COMMIT_REF: $VERCEL_GIT_COMMIT_REF"
|
|
||||||
|
|
||||||
if [[ "$VERCEL_GIT_COMMIT_REF" == "master" || "$VERCEL_GIT_COMMIT_REF" == "development" ]]; then
|
|
||||||
echo "✅ - Build can proceed"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "🛑 - Build cancelled"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
+35
-4
@@ -13,6 +13,7 @@ import (
|
|||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
"gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/route"
|
"gitlab.com/mbugroup/lti-api.git/internal/route"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -35,7 +36,7 @@ func main() {
|
|||||||
defer closeDatabase(db)
|
defer closeDatabase(db)
|
||||||
rdb := setupRedis()
|
rdb := setupRedis()
|
||||||
defer rdb.Close()
|
defer rdb.Close()
|
||||||
setupSSO(ctx)
|
setupSSO(ctx, rdb)
|
||||||
setupRoutes(app, db, rdb)
|
setupRoutes(app, db, rdb)
|
||||||
|
|
||||||
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
|
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
|
||||||
@@ -60,9 +61,39 @@ func setupRedis() *redis.Client {
|
|||||||
return rdb
|
return rdb
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupSSO(ctx context.Context) {
|
func setupSSO(ctx context.Context, rdb *redis.Client) {
|
||||||
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil {
|
const (
|
||||||
utils.Log.Fatalf("SSO initialization failed: %v", err)
|
maxAttempts = 12
|
||||||
|
retryDelay = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||||
|
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
utils.Log.WithError(err).Warnf("SSO initialization attempt %d/%d failed", attempt, maxAttempts)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
utils.Log.Fatalf("SSO initialization aborted: %v", ctx.Err())
|
||||||
|
case <-time.After(retryDelay):
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lastErr = nil
|
||||||
|
if attempt > 1 {
|
||||||
|
utils.Log.Infof("SSO initialization succeeded after %d attempts", attempt)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil {
|
||||||
|
utils.Log.Fatalf("SSO initialization failed: %v", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rdb != nil {
|
||||||
|
session.SetRevocationStore(session.NewRevocationStore(rdb, config.SSOTokenBlacklistPrefix))
|
||||||
|
} else {
|
||||||
|
session.SetRevocationStore(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
services:
|
||||||
|
dev-api-lti:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: dev-api-lti
|
||||||
|
working_dir: /lti-api
|
||||||
|
command: ["/bin/sh", "scripts/entrypoint.sh"]
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
# override agar koneksi ke container internal
|
||||||
|
DB_HOST: dev-postgres-lti
|
||||||
|
DB_PORT: 5432
|
||||||
|
REDIS_URL: redis://dev-redis-lti:6379/0
|
||||||
|
volumes:
|
||||||
|
- .:/lti-api
|
||||||
|
- ./.air.toml:/lti-api/.air.toml:ro
|
||||||
|
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
||||||
|
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
||||||
|
depends_on:
|
||||||
|
- dev-postgres-lti
|
||||||
|
- dev-redis-lti
|
||||||
|
networks:
|
||||||
|
- lti-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "2.0"
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
dev-postgres-lti:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: dev-postgres-lti
|
||||||
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- credential/.env.db
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- dev-postgres-lti-data:/var/lib/postgresql/data
|
||||||
|
- ./credential:/docker-entrypoint-initdb.d:ro
|
||||||
|
networks:
|
||||||
|
- lti-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 5s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
dev-redis-lti:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: dev-redis-lti
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6380:6379"
|
||||||
|
networks:
|
||||||
|
- lti-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: "0.2"
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
networks:
|
||||||
|
lti-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dev-postgres-lti-data:
|
||||||
@@ -3,11 +3,19 @@ module gitlab.com/mbugroup/lti-api.git
|
|||||||
go 1.23
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/MicahParks/keyfunc/v2 v2.1.0
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.40.0
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.2
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1
|
||||||
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
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgconn v1.14.1
|
||||||
github.com/redis/go-redis/v9 v9.14.0
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
@@ -17,8 +25,22 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
|
||||||
|
github.com/aws/smithy-go v1.23.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
@@ -28,13 +50,14 @@ require (
|
|||||||
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/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
github.com/glebarez/sqlite v1.11.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
@@ -47,7 +70,6 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/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
|
||||||
@@ -76,7 +98,6 @@ 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
|
||||||
gorm.io/driver/sqlite v1.5.5 // indirect
|
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
|||||||
@@ -2,6 +2,44 @@ github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+G
|
|||||||
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
|
||||||
|
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||||
|
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
@@ -17,6 +55,10 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
|
|||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
@@ -43,6 +85,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gofiber/contrib/jwt v1.0.10 h1:/ilGepl6i0Bntl0Zcd+lAzagY8BiS1+fEiAj32HMApk=
|
github.com/gofiber/contrib/jwt v1.0.10 h1:/ilGepl6i0Bntl0Zcd+lAzagY8BiS1+fEiAj32HMApk=
|
||||||
github.com/gofiber/contrib/jwt v1.0.10/go.mod h1:1qBENE6sZ6PPT4xIpBzx1VxeyROQO7sj48OlM1I9qdU=
|
github.com/gofiber/contrib/jwt v1.0.10/go.mod h1:1qBENE6sZ6PPT4xIpBzx1VxeyROQO7sj48OlM1I9qdU=
|
||||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||||
@@ -51,16 +94,53 @@ 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=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||||
|
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||||
|
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||||
|
github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4=
|
||||||
|
github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
|
||||||
|
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||||
|
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -69,33 +149,44 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
|||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
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=
|
||||||
@@ -109,10 +200,17 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
@@ -126,10 +224,15 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
|||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@@ -150,24 +253,41 @@ github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRV
|
|||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -175,7 +295,14 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -184,35 +311,48 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
gorm.io/driver/postgres v1.5.9 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/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
|
||||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
|
||||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
gorm.io/gorm v1.25.11 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 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalRepository interface {
|
||||||
|
BaseRepository[entity.Approval]
|
||||||
|
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
|
||||||
|
LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
|
||||||
|
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
|
||||||
|
DeleteByTarget(ctx context.Context, workflow string, approvableID uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type approvalRepositoryImpl struct {
|
||||||
|
*BaseRepositoryImpl[entity.Approval]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalRepository(db *gorm.DB) ApprovalRepository {
|
||||||
|
return &approvalRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: NewBaseRepository[entity.Approval](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *approvalRepositoryImpl) FindByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
workflow string,
|
||||||
|
approvableID uint,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) ([]entity.Approval, error) {
|
||||||
|
var approvals []entity.Approval
|
||||||
|
|
||||||
|
q := r.DB().WithContext(ctx).Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID)
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Order("action_at ASC").Find(&approvals).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return approvals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *approvalRepositoryImpl) LatestByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
workflow string,
|
||||||
|
approvableID uint,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) (*entity.Approval, error) {
|
||||||
|
var approval entity.Approval
|
||||||
|
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
|
||||||
|
Order("action_at DESC")
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Limit(1).First(&approval).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &approval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *approvalRepositoryImpl) LatestByTargets(
|
||||||
|
ctx context.Context,
|
||||||
|
workflow string,
|
||||||
|
approvableIDs []uint,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) (map[uint]entity.Approval, error) {
|
||||||
|
if len(approvableIDs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint]entity.Approval, len(approvableIDs))
|
||||||
|
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Select("DISTINCT ON (approvable_id) *").
|
||||||
|
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
|
||||||
|
Order("approvable_id, action_at DESC")
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
var approvals []entity.Approval
|
||||||
|
if err := q.Find(&approvals).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, approval := range approvals {
|
||||||
|
if _, exists := result[approval.ApprovableId]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[approval.ApprovableId] = approval
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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,62 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentRepository interface {
|
||||||
|
BaseRepository[entity.Document]
|
||||||
|
ListByTarget(ctx context.Context, documentableType string, documentableID uint64, modifier func(*gorm.DB) *gorm.DB) ([]entity.Document, error)
|
||||||
|
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type documentRepositoryImpl struct {
|
||||||
|
*BaseRepositoryImpl[entity.Document]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentRepository(db *gorm.DB) DocumentRepository {
|
||||||
|
return &documentRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: NewBaseRepository[entity.Document](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *documentRepositoryImpl) ListByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
documentableType string,
|
||||||
|
documentableID uint64,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) ([]entity.Document, error) {
|
||||||
|
var documents []entity.Document
|
||||||
|
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Where("documentable_type = ? AND documentable_id = ?", documentableType, documentableID)
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Order("created_at ASC").Find(&documents).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return documents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *documentRepositoryImpl) DeleteByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
documentableType string,
|
||||||
|
documentableID uint64,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Where("documentable_type = ? AND documentable_id = ?", documentableType, documentableID)
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Delete(&entity.Document{}).Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exists reports whether a record with the given ID exists for type T.
|
||||||
|
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
|
||||||
|
var marker int
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Model(new(T)).
|
||||||
|
Select("1").
|
||||||
|
Where("id = ?", id).
|
||||||
|
Limit(1).
|
||||||
|
Take(&marker).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
|
||||||
|
q := db.WithContext(ctx).
|
||||||
|
Model(new(T)).
|
||||||
|
Select("1").
|
||||||
|
Where("name = ?", name).
|
||||||
|
Where("deleted_at IS NULL")
|
||||||
|
if excludeID != nil {
|
||||||
|
q = q.Where("id <> ?", *excludeID)
|
||||||
|
}
|
||||||
|
var marker int
|
||||||
|
if err := q.Limit(1).Take(&marker).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
|
||||||
|
if field == "" {
|
||||||
|
return false, fmt.Errorf("field is required")
|
||||||
|
}
|
||||||
|
q := db.WithContext(ctx).
|
||||||
|
Model(new(T)).
|
||||||
|
Select("1").
|
||||||
|
Where(fmt.Sprintf("%s = ?", field), value).
|
||||||
|
Where("deleted_at IS NULL")
|
||||||
|
if excludeID != nil {
|
||||||
|
q = q.Where("id <> ?", *excludeID)
|
||||||
|
}
|
||||||
|
var marker int
|
||||||
|
if err := q.Limit(1).Take(&marker).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDB := r.DB()
|
||||||
|
if modifier != nil {
|
||||||
|
baseDB = modifier(baseDB)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := baseDB.WithContext(ctx).
|
||||||
|
Model(&entity.StockAllocation{}).
|
||||||
|
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
|
||||||
|
|
||||||
|
return q.Updates(updates).Error
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Exists reports whether a record with the given ID exists for type T.
|
|
||||||
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
|
|
||||||
var count int64
|
|
||||||
if err := db.WithContext(ctx).
|
|
||||||
Model(new(T)).
|
|
||||||
Where("id = ?", id).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return count > 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
|
|
||||||
var count int64
|
|
||||||
q := db.WithContext(ctx).
|
|
||||||
Model(new(T)).
|
|
||||||
Where("name = ?", name).
|
|
||||||
Where("deleted_at IS NULL")
|
|
||||||
if excludeID != nil {
|
|
||||||
q = q.Where("id <> ?", *excludeID)
|
|
||||||
}
|
|
||||||
if err := q.Count(&count).Error; err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return count > 0, nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalService interface {
|
||||||
|
RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error
|
||||||
|
WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string
|
||||||
|
WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool)
|
||||||
|
CreateApproval(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, step approvalutils.ApprovalStep, action *entity.ApprovalAction, actorID uint, note *string) (*entity.Approval, error)
|
||||||
|
List(ctx context.Context, module string, approvableID *uint, page, limit int, search string) ([]entity.Approval, int64, error)
|
||||||
|
ListByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
|
||||||
|
LatestByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
|
||||||
|
LatestByTargets(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]*entity.Approval, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type approvalService struct {
|
||||||
|
repo commonRepo.ApprovalRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalService(repo commonRepo.ApprovalRepository) ApprovalService {
|
||||||
|
return &approvalService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error {
|
||||||
|
return approvalutils.RegisterWorkflowSteps(workflow, steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string {
|
||||||
|
return approvalutils.WorkflowSteps(workflow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool) {
|
||||||
|
return approvalutils.ApprovalStepName(workflow, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) CreateApproval(
|
||||||
|
ctx context.Context,
|
||||||
|
workflow approvalutils.ApprovalWorkflowKey,
|
||||||
|
approvableID uint,
|
||||||
|
step approvalutils.ApprovalStep,
|
||||||
|
action *entity.ApprovalAction,
|
||||||
|
actorID uint,
|
||||||
|
note *string,
|
||||||
|
) (*entity.Approval, error) {
|
||||||
|
record, err := approvalutils.NewApproval(workflow, approvableID, step, action, actorID, note)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.CreateOne(ctx, record, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.decorateApproval(workflow, record)
|
||||||
|
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) List(
|
||||||
|
ctx context.Context,
|
||||||
|
module string,
|
||||||
|
approvableID *uint,
|
||||||
|
page, limit int,
|
||||||
|
search string,
|
||||||
|
) ([]entity.Approval, int64, error) {
|
||||||
|
module = strings.TrimSpace(strings.ToUpper(module))
|
||||||
|
search = strings.TrimSpace(search)
|
||||||
|
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
|
records, total, err := s.repo.GetAll(
|
||||||
|
ctx,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
func(db *gorm.DB) *gorm.DB {
|
||||||
|
query := db.
|
||||||
|
Where("approvable_type = ?", module).
|
||||||
|
Order("action_at DESC").
|
||||||
|
Preload("ActionUser")
|
||||||
|
|
||||||
|
if approvableID != nil {
|
||||||
|
query = query.Where("approvable_id = ?", *approvableID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if search != "" {
|
||||||
|
like := "%" + strings.ToLower(search) + "%"
|
||||||
|
query = query.Where("(LOWER(step_name) LIKE ? OR LOWER(action) LIKE ? OR LOWER(notes) LIKE ?)", like, like, like)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if s.isApprovalTableMissing(err) {
|
||||||
|
return nil, 0, nil
|
||||||
|
}
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow := approvalutils.ApprovalWorkflowKey(module)
|
||||||
|
for i := range records {
|
||||||
|
s.decorateApproval(workflow, &records[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) ListByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
workflow approvalutils.ApprovalWorkflowKey,
|
||||||
|
approvableID uint,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) ([]entity.Approval, error) {
|
||||||
|
records, err := s.repo.FindByTarget(ctx, workflow.String(), approvableID, modifier)
|
||||||
|
if err != nil {
|
||||||
|
if s.isApprovalTableMissing(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range records {
|
||||||
|
s.decorateApproval(workflow, &records[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) LatestByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
workflow approvalutils.ApprovalWorkflowKey,
|
||||||
|
approvableID uint,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) (*entity.Approval, error) {
|
||||||
|
record, err := s.repo.LatestByTarget(ctx, workflow.String(), approvableID, modifier)
|
||||||
|
if err != nil {
|
||||||
|
if s.isApprovalTableMissing(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if record == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
s.decorateApproval(workflow, record)
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) LatestByTargets(
|
||||||
|
ctx context.Context,
|
||||||
|
workflow approvalutils.ApprovalWorkflowKey,
|
||||||
|
approvableIDs []uint,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) (map[uint]*entity.Approval, error) {
|
||||||
|
records, err := s.repo.LatestByTargets(ctx, workflow.String(), approvableIDs, modifier)
|
||||||
|
if err != nil {
|
||||||
|
if s.isApprovalTableMissing(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint]*entity.Approval, len(records))
|
||||||
|
for approvableID, approval := range records {
|
||||||
|
approvalCopy := approval
|
||||||
|
s.decorateApproval(workflow, &approvalCopy)
|
||||||
|
result[approvableID] = &approvalCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) decorateApproval(workflow approvalutils.ApprovalWorkflowKey, approval *entity.Approval) {
|
||||||
|
if approval == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentName := strings.TrimSpace(approval.StepName)
|
||||||
|
if currentName == "" {
|
||||||
|
if name, ok := approvalutils.ApprovalStepName(workflow, approvalutils.ApprovalStep(approval.StepNumber)); ok {
|
||||||
|
approval.StepName = name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
approval.StepName = currentName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *approvalService) isApprovalTableMissing(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errMsg := strings.ToLower(err.Error())
|
||||||
|
|
||||||
|
if strings.Contains(errMsg, "no such table: approvals") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaIssues := []string{
|
||||||
|
`relation "approvals" does not exist`,
|
||||||
|
`column "step_name" does not exist`,
|
||||||
|
`column "step_number" does not exist`,
|
||||||
|
`column "action" does not exist`,
|
||||||
|
`column "status" does not exist`,
|
||||||
|
`column "step" does not exist`,
|
||||||
|
}
|
||||||
|
for _, issue := range schemaIssues {
|
||||||
|
if strings.Contains(errMsg, issue) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dipakai untuk semua module yang butuh cek:
|
||||||
|
// "PW ini → warehouse → kandang → project_flock_kandang sudah closing atau belum"
|
||||||
|
func EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
productWarehouseIDs []uint,
|
||||||
|
) error {
|
||||||
|
if len(productWarehouseIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(db)
|
||||||
|
wRepo := warehouseRepo.NewWarehouseRepository(db)
|
||||||
|
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
|
seenPW := make(map[uint]struct{})
|
||||||
|
seenKandang := make(map[uint]struct{})
|
||||||
|
|
||||||
|
for _, pwID := range productWarehouseIDs {
|
||||||
|
if pwID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seenPW[pwID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenPW[pwID] = struct{}{}
|
||||||
|
|
||||||
|
pw, err := pwRepo.GetByID(ctx, pwID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Product warehouse %d tidak ditemukan", pwID))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
wh, err := wRepo.GetByID(ctx, uint(pw.WarehouseId), nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Warehouse %d tidak ditemukan", pw.WarehouseId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warehouse tanpa kandang → bukan kandang produksi → skip
|
||||||
|
if wh.KandangId == nil || *wh.KandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := uint(*wh.KandangId)
|
||||||
|
if _, ok := seenKandang[kandangID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenKandang[kandangID] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := pfkRepo.GetActiveByKandangID(ctx, kandangID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// nggak ada project aktif untuk kandang ini → aman
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
// INTI RULE: kalau aktif tapi sudah punya ClosedAt → anggap "project sudah closing"
|
||||||
|
if pfk != nil && pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureProjectFlockNotClosedByProjectFlockKandangID(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
pfkIDs []uint,
|
||||||
|
) error {
|
||||||
|
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
|
seen := make(map[uint]struct{})
|
||||||
|
for _, id := range pfkIDs {
|
||||||
|
if id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := pfkRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Project flock kandang %d tidak ditemukan", id))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultDocumentPathLimit = 50
|
||||||
|
defaultDocumentKeyPrefix = "docs"
|
||||||
|
maxDocumentNameLength = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentService interface {
|
||||||
|
UploadDocuments(ctx context.Context, req DocumentUploadRequest) ([]DocumentUploadResult, error)
|
||||||
|
ListByTarget(ctx context.Context, documentableType string, documentableID uint64) ([]entity.Document, error)
|
||||||
|
DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error
|
||||||
|
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error
|
||||||
|
PublicURL(document entity.Document) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentUploadRequest struct {
|
||||||
|
DocumentableType string
|
||||||
|
DocumentableID uint64
|
||||||
|
CreatedBy *uint
|
||||||
|
Files []DocumentFile
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentFile struct {
|
||||||
|
File *multipart.FileHeader
|
||||||
|
Type string
|
||||||
|
Index *int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentUploadResult struct {
|
||||||
|
Document entity.Document
|
||||||
|
URL string
|
||||||
|
Index *int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentServiceOption func(*documentService)
|
||||||
|
|
||||||
|
type documentService struct {
|
||||||
|
repo commonRepo.DocumentRepository
|
||||||
|
storage DocumentStorage
|
||||||
|
keyPrefix string
|
||||||
|
maxPathLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentService(repo commonRepo.DocumentRepository, storage DocumentStorage, opts ...DocumentServiceOption) DocumentService {
|
||||||
|
svc := &documentService{
|
||||||
|
repo: repo,
|
||||||
|
storage: storage,
|
||||||
|
keyPrefix: defaultDocumentKeyPrefix,
|
||||||
|
maxPathLength: defaultDocumentPathLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(svc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentServiceFromConfig(ctx context.Context, repo commonRepo.DocumentRepository) (DocumentService, error) {
|
||||||
|
if repo == nil {
|
||||||
|
return nil, errors.New("document repository is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(config.S3Bucket) == "" {
|
||||||
|
return nil, errors.New("S3_BUCKET is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err := NewS3DocumentStorage(ctx, S3DocumentStorageConfig{
|
||||||
|
Region: config.S3Region,
|
||||||
|
Bucket: config.S3Bucket,
|
||||||
|
AccessKey: config.S3AccessKey,
|
||||||
|
SecretKey: config.S3SecretKey,
|
||||||
|
Endpoint: config.S3Endpoint,
|
||||||
|
BaseURL: config.S3PublicBaseURL,
|
||||||
|
ForcePathStyle: config.S3ForcePathStyle,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := config.S3DocumentKeyPrefix
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = defaultDocumentKeyPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewDocumentService(
|
||||||
|
repo,
|
||||||
|
storage,
|
||||||
|
WithDocumentKeyPrefix(prefix),
|
||||||
|
WithDocumentPathLimit(defaultDocumentPathLimit),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDocumentKeyPrefix(prefix string) DocumentServiceOption {
|
||||||
|
return func(svc *documentService) {
|
||||||
|
prefix = strings.Trim(prefix, "/")
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = defaultDocumentKeyPrefix
|
||||||
|
}
|
||||||
|
svc.keyPrefix = prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDocumentPathLimit(limit int) DocumentServiceOption {
|
||||||
|
return func(svc *documentService) {
|
||||||
|
if limit > 0 {
|
||||||
|
svc.maxPathLength = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) UploadDocuments(ctx context.Context, req DocumentUploadRequest) ([]DocumentUploadResult, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
if s.storage == nil {
|
||||||
|
return nil, errors.New("document storage not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentableType := strings.ToUpper(strings.TrimSpace(req.DocumentableType))
|
||||||
|
if documentableType == "" {
|
||||||
|
return nil, errors.New("documentable type is required")
|
||||||
|
}
|
||||||
|
if req.DocumentableID == 0 {
|
||||||
|
return nil, errors.New("documentable id is required")
|
||||||
|
}
|
||||||
|
if len(req.Files) == 0 {
|
||||||
|
return nil, errors.New("no files to upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdBy *uint
|
||||||
|
if req.CreatedBy != nil && *req.CreatedBy != 0 {
|
||||||
|
idCopy := *req.CreatedBy
|
||||||
|
createdBy = &idCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]DocumentUploadResult, 0, len(req.Files))
|
||||||
|
createdDocs := make([]entity.Document, 0, len(req.Files))
|
||||||
|
|
||||||
|
for _, file := range req.Files {
|
||||||
|
if file.File == nil {
|
||||||
|
return nil, errors.New("file header is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
originalName := sanitizeDocumentName(file.File.Filename)
|
||||||
|
contentType := detectContentType(file.File, originalName)
|
||||||
|
ext := detectExtension(file.File.Filename, contentType)
|
||||||
|
key, err := s.generateObjectKey(ext)
|
||||||
|
if err != nil {
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := file.File.Open()
|
||||||
|
if err != nil {
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uploadRes, err := s.storage.Upload(ctx, key, reader, file.File.Size, contentType)
|
||||||
|
_ = reader.Close()
|
||||||
|
if err != nil {
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
docType := resolveDocumentType(file.Type, documentableType)
|
||||||
|
doc := entity.Document{
|
||||||
|
DocumentableType: documentableType,
|
||||||
|
DocumentableId: req.DocumentableID,
|
||||||
|
Type: docType,
|
||||||
|
Path: uploadRes.Key,
|
||||||
|
Name: originalName,
|
||||||
|
Ext: strings.TrimPrefix(ext, "."),
|
||||||
|
Size: float64(file.File.Size),
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.CreateOne(ctx, &doc, nil); err != nil {
|
||||||
|
_ = s.storage.Delete(ctx, uploadRes.Key)
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdDocs = append(createdDocs, doc)
|
||||||
|
results = append(results, DocumentUploadResult{
|
||||||
|
Document: doc,
|
||||||
|
URL: uploadRes.URL,
|
||||||
|
Index: cloneIndex(file.Index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) ListByTarget(ctx context.Context, documentableType string, documentableID uint64) ([]entity.Document, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentableType = strings.ToUpper(strings.TrimSpace(documentableType))
|
||||||
|
if documentableType == "" {
|
||||||
|
return nil, errors.New("documentable type is required")
|
||||||
|
}
|
||||||
|
if documentableID == 0 {
|
||||||
|
return nil, errors.New("documentable id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.ListByTarget(ctx, documentableType, documentableID, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error {
|
||||||
|
if s.repo == nil {
|
||||||
|
return errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
docs, err := s.repo.GetByIDs(ctx, ids, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, doc := range docs {
|
||||||
|
if err := s.repo.DeleteOne(ctx, doc.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if removeFromStorage && s.storage != nil {
|
||||||
|
if err := s.storage.Delete(ctx, doc.Path); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to delete document object %s", doc.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error {
|
||||||
|
if s.repo == nil {
|
||||||
|
return errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentableType = strings.ToUpper(strings.TrimSpace(documentableType))
|
||||||
|
if documentableType == "" || documentableID == 0 {
|
||||||
|
return errors.New("documentable type and id are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var docs []entity.Document
|
||||||
|
if removeFromStorage && s.storage != nil {
|
||||||
|
var err error
|
||||||
|
docs, err = s.repo.ListByTarget(ctx, documentableType, documentableID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.DeleteByTarget(ctx, documentableType, documentableID, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if removeFromStorage && len(docs) > 0 {
|
||||||
|
for _, doc := range docs {
|
||||||
|
if err := s.storage.Delete(ctx, doc.Path); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to delete document object %s", doc.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) PublicURL(document entity.Document) string {
|
||||||
|
if s.storage == nil || strings.TrimSpace(document.Path) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s.storage.URL(document.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) generateObjectKey(ext string) (string, error) {
|
||||||
|
normalizedExt := strings.TrimSpace(ext)
|
||||||
|
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
|
||||||
|
normalizedExt = "." + normalizedExt
|
||||||
|
}
|
||||||
|
|
||||||
|
u := uuid.New().String()
|
||||||
|
key := fmt.Sprintf("%s/%s%s", strings.Trim(s.keyPrefix, "/"), u, normalizedExt)
|
||||||
|
if s.keyPrefix == "" {
|
||||||
|
key = fmt.Sprintf("%s%s", u, normalizedExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) > s.maxPathLength {
|
||||||
|
key = fmt.Sprintf("%s%s", u, normalizedExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) > s.maxPathLength {
|
||||||
|
return "", fmt.Errorf("object key exceeds maximum length (%d)", s.maxPathLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) rollbackDocuments(ctx context.Context, docs []entity.Document) {
|
||||||
|
if len(docs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(docs) - 1; i >= 0; i-- {
|
||||||
|
doc := docs[i]
|
||||||
|
if s.repo != nil && doc.Id != 0 {
|
||||||
|
if err := s.repo.DeleteOne(ctx, doc.Id); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to rollback document #%d", doc.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.storage != nil && strings.TrimSpace(doc.Path) != "" {
|
||||||
|
if err := s.storage.Delete(ctx, doc.Path); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to rollback document object %s", doc.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeDocumentName(name string) string {
|
||||||
|
name = filepath.Base(strings.TrimSpace(name))
|
||||||
|
if name == "." || name == "" {
|
||||||
|
name = "document"
|
||||||
|
}
|
||||||
|
name = strings.Map(func(r rune) rune {
|
||||||
|
if r < 32 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case '\\', '/', ':', '*', '?', '"', '<', '>', '|':
|
||||||
|
return '-'
|
||||||
|
default:
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}, name)
|
||||||
|
|
||||||
|
if len(name) > maxDocumentNameLength {
|
||||||
|
runes := []rune(name)
|
||||||
|
if len(runes) > maxDocumentNameLength {
|
||||||
|
name = string(runes[:maxDocumentNameLength])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectExtension(filename, contentType string) string {
|
||||||
|
ext := strings.ToLower(strings.TrimSpace(filepath.Ext(filename)))
|
||||||
|
if ext == "" && contentType != "" {
|
||||||
|
if exts, _ := mime.ExtensionsByType(contentType); len(exts) > 0 {
|
||||||
|
ext = exts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ext == "" {
|
||||||
|
return ".bin"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(ext, ".") {
|
||||||
|
ext = "." + ext
|
||||||
|
}
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectContentType(file *multipart.FileHeader, filename string) string {
|
||||||
|
if file == nil {
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
contentType := strings.TrimSpace(file.Header.Get("Content-Type"))
|
||||||
|
if contentType != "" {
|
||||||
|
return contentType
|
||||||
|
}
|
||||||
|
if ext := filepath.Ext(filename); ext != "" {
|
||||||
|
if guess := mime.TypeByExtension(ext); guess != "" {
|
||||||
|
return guess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDocumentType(fileType, fallback string) string {
|
||||||
|
value := strings.ToUpper(strings.TrimSpace(fileType))
|
||||||
|
if value == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneIndex(index *int) *int {
|
||||||
|
if index == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := *index
|
||||||
|
return &value
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDocumentServiceUpload(t *testing.T) {
|
||||||
|
if strings.TrimSpace(config.S3Bucket) == "" {
|
||||||
|
t.Fatal("S3 bucket is not configured; set S3_* env vars to run this test")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
db := setupDocumentTestDB(t)
|
||||||
|
repo := commonRepo.NewDocumentRepository(db)
|
||||||
|
|
||||||
|
svc, err := NewDocumentServiceFromConfig(ctx, repo)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create document service from config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := newTestFileHeader(t, "integration-proof.txt", "text/plain", []byte("document integration test"))
|
||||||
|
userID := uint(100)
|
||||||
|
|
||||||
|
results, err := svc.UploadDocuments(ctx, DocumentUploadRequest{
|
||||||
|
DocumentableType: "INVENTORY_TRANSFER",
|
||||||
|
DocumentableID: 99,
|
||||||
|
CreatedBy: &userID,
|
||||||
|
Files: []DocumentFile{
|
||||||
|
{File: file, Type: "integration"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("upload to S3 failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("expected 1 uploaded document, got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := results[0].Document
|
||||||
|
if doc.Path == "" {
|
||||||
|
t.Fatalf("expected non-empty storage path")
|
||||||
|
}
|
||||||
|
if results[0].URL == "" {
|
||||||
|
t.Fatalf("expected public URL for uploaded document")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("uploaded document #%d to %s (path=%s)", doc.Id, results[0].URL, doc.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDocumentTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
if strings.TrimSpace(config.DBHost) == "" || strings.TrimSpace(config.DBName) == "" {
|
||||||
|
t.Fatal("database configuration missing; ensure DB_HOST and DB_NAME are set")
|
||||||
|
}
|
||||||
|
db := database.Connect(config.DBHost, config.DBName)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatal("failed to create database connection")
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&entity.Document{}); err != nil {
|
||||||
|
t.Fatalf("failed to migrate document table: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestFileHeader(t *testing.T, filename, contentType string, data []byte) *multipart.FileHeader {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, err := writer.CreateFormFile("documents", filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create form file: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := part.Write(data); err != nil {
|
||||||
|
t.Fatalf("failed to write file data: %v", err)
|
||||||
|
}
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close writer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://example.com/upload", body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
_, fileHeader, err := req.FormFile("documents")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse form file: %v", err)
|
||||||
|
}
|
||||||
|
fileHeader.Header.Set("Content-Type", contentType)
|
||||||
|
return fileHeader
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentStorage interface {
|
||||||
|
Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error)
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
URL(key string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentStorageUploadResult struct {
|
||||||
|
Key string
|
||||||
|
URL string
|
||||||
|
ETag string
|
||||||
|
}
|
||||||
|
|
||||||
|
type S3DocumentStorageConfig struct {
|
||||||
|
Region string
|
||||||
|
Bucket string
|
||||||
|
AccessKey string
|
||||||
|
SecretKey string
|
||||||
|
Endpoint string
|
||||||
|
BaseURL string
|
||||||
|
ForcePathStyle bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type s3DocumentStorage struct {
|
||||||
|
client *s3.Client
|
||||||
|
bucket string
|
||||||
|
base string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (DocumentStorage, error) {
|
||||||
|
bucket := strings.TrimSpace(cfg.Bucket)
|
||||||
|
if bucket == "" {
|
||||||
|
return nil, errors.New("s3 bucket is required")
|
||||||
|
}
|
||||||
|
region := strings.TrimSpace(cfg.Region)
|
||||||
|
if region == "" {
|
||||||
|
region = "us-east-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []func(*awsconfig.LoadOptions) error{
|
||||||
|
awsconfig.WithRegion(region),
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := strings.TrimSpace(cfg.Endpoint)
|
||||||
|
if endpoint != "" {
|
||||||
|
resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) {
|
||||||
|
if service == s3.ServiceID {
|
||||||
|
return aws.Endpoint{
|
||||||
|
URL: endpoint,
|
||||||
|
SigningRegion: region,
|
||||||
|
HostnameImmutable: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
|
||||||
|
})
|
||||||
|
options = append(options, awsconfig.WithEndpointResolverWithOptions(resolver))
|
||||||
|
}
|
||||||
|
|
||||||
|
accessKey := strings.TrimSpace(cfg.AccessKey)
|
||||||
|
secretKey := strings.TrimSpace(cfg.SecretKey)
|
||||||
|
if accessKey != "" && secretKey != "" {
|
||||||
|
options = append(options, awsconfig.WithCredentialsProvider(
|
||||||
|
credentials.NewStaticCredentialsProvider(accessKey, secretKey, ""),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||||
|
o.UsePathStyle = cfg.ForcePathStyle
|
||||||
|
})
|
||||||
|
|
||||||
|
baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/")
|
||||||
|
if baseURL == "" {
|
||||||
|
if endpoint != "" {
|
||||||
|
baseURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(endpoint, "/"), bucket)
|
||||||
|
} else {
|
||||||
|
baseURL = fmt.Sprintf("https://%s.s3.%s.amazonaws.com", bucket, region)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &s3DocumentStorage{
|
||||||
|
client: client,
|
||||||
|
bucket: bucket,
|
||||||
|
base: baseURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3DocumentStorage) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error) {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return DocumentStorageUploadResult{}, errors.New("storage key is required")
|
||||||
|
}
|
||||||
|
if size < 0 {
|
||||||
|
size = 0
|
||||||
|
}
|
||||||
|
input := &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
input.ContentLength = aws.Int64(size)
|
||||||
|
if ct := strings.TrimSpace(contentType); ct != "" {
|
||||||
|
input.ContentType = aws.String(ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := s.client.PutObject(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return DocumentStorageUploadResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var etag string
|
||||||
|
if out.ETag != nil {
|
||||||
|
etag = strings.Trim(*out.ETag, "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentStorageUploadResult{
|
||||||
|
Key: key,
|
||||||
|
URL: s.URL(key),
|
||||||
|
ETag: etag,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3DocumentStorage) Delete(ctx context.Context, key string) error {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3DocumentStorage) URL(key string) string {
|
||||||
|
key = strings.TrimPrefix(strings.TrimSpace(key), "/")
|
||||||
|
if key == "" {
|
||||||
|
return s.base
|
||||||
|
}
|
||||||
|
if s.base == "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", s.base, key)
|
||||||
|
}
|
||||||
@@ -0,0 +1,833 @@
|
|||||||
|
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 {
|
||||||
|
|
||||||
|
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
||||||
|
|
||||||
|
var selectStmt string
|
||||||
|
if usesNumericTime {
|
||||||
|
|
||||||
|
selectStmt = fmt.Sprintf(
|
||||||
|
"%s AS id, %s AS available_qty, '1970-01-01 00:00:00 UTC'::timestamp AS created_at",
|
||||||
|
cfg.Columns.ID,
|
||||||
|
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
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.
@@ -12,10 +12,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SSOClientConfig struct {
|
type SSOClientConfig struct {
|
||||||
PublicID string `json:"public_id"`
|
PublicID string `json:"public_id"`
|
||||||
RedirectURI string `json:"redirect_uri"`
|
RedirectURI string `json:"redirect_uri"`
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
Prompt string `json:"prompt"`
|
// Prompt string `json:"prompt"`
|
||||||
DefaultReturnURI string `json:"default_return_uri"`
|
DefaultReturnURI string `json:"default_return_uri"`
|
||||||
AllowedReturnOrigins []string `json:"allowed_return_origins"`
|
AllowedReturnOrigins []string `json:"allowed_return_origins"`
|
||||||
SyncSecret string `json:"sync_secret"`
|
SyncSecret string `json:"sync_secret"`
|
||||||
@@ -32,6 +32,10 @@ var (
|
|||||||
DBPassword string
|
DBPassword string
|
||||||
DBName string
|
DBName string
|
||||||
DBPort int
|
DBPort int
|
||||||
|
DBSSLMode string
|
||||||
|
DBSSLRootCert string
|
||||||
|
DBSSLCert string
|
||||||
|
DBSSLKey string
|
||||||
JWTSecret string
|
JWTSecret string
|
||||||
JWTAccessExp int
|
JWTAccessExp int
|
||||||
JWTRefreshExp int
|
JWTRefreshExp int
|
||||||
@@ -56,10 +60,19 @@ var (
|
|||||||
SSOCookieDomain string
|
SSOCookieDomain string
|
||||||
SSOCookieSecure bool
|
SSOCookieSecure bool
|
||||||
SSOCookieSameSite string
|
SSOCookieSameSite string
|
||||||
|
SSOTokenBlacklistPrefix string
|
||||||
SSOPKCETTL time.Duration
|
SSOPKCETTL time.Duration
|
||||||
SSOUserSyncDrift time.Duration
|
SSOUserSyncDrift time.Duration
|
||||||
SSOUserSyncNonceTTL time.Duration
|
SSOUserSyncNonceTTL time.Duration
|
||||||
SSOUserSyncMaxBodyBytes int
|
SSOUserSyncMaxBodyBytes int
|
||||||
|
S3Endpoint string
|
||||||
|
S3Region string
|
||||||
|
S3Bucket string
|
||||||
|
S3AccessKey string
|
||||||
|
S3SecretKey string
|
||||||
|
S3ForcePathStyle bool
|
||||||
|
S3PublicBaseURL string
|
||||||
|
S3DocumentKeyPrefix string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -78,6 +91,10 @@ func init() {
|
|||||||
DBPassword = viper.GetString("DB_PASSWORD")
|
DBPassword = viper.GetString("DB_PASSWORD")
|
||||||
DBName = viper.GetString("DB_NAME")
|
DBName = viper.GetString("DB_NAME")
|
||||||
DBPort = viper.GetInt("DB_PORT")
|
DBPort = viper.GetInt("DB_PORT")
|
||||||
|
DBSSLMode = defaultString(viper.GetString("DB_SSLMODE"), "disable")
|
||||||
|
DBSSLRootCert = strings.TrimSpace(viper.GetString("DB_SSLROOTCERT"))
|
||||||
|
DBSSLCert = strings.TrimSpace(viper.GetString("DB_SSLCERT"))
|
||||||
|
DBSSLKey = strings.TrimSpace(viper.GetString("DB_SSLKEY"))
|
||||||
|
|
||||||
// jwt configuration
|
// jwt configuration
|
||||||
JWTSecret = viper.GetString("JWT_SECRET")
|
JWTSecret = viper.GetString("JWT_SECRET")
|
||||||
@@ -97,6 +114,16 @@ func init() {
|
|||||||
// Redis
|
// Redis
|
||||||
RedisURL = viper.GetString("REDIS_URL")
|
RedisURL = viper.GetString("REDIS_URL")
|
||||||
|
|
||||||
|
// Object storage
|
||||||
|
S3Endpoint = strings.TrimSpace(viper.GetString("S3_ENDPOINT"))
|
||||||
|
S3Region = strings.TrimSpace(viper.GetString("S3_REGION"))
|
||||||
|
S3Bucket = strings.TrimSpace(viper.GetString("S3_BUCKET"))
|
||||||
|
S3AccessKey = strings.TrimSpace(viper.GetString("S3_ACCESS_KEY"))
|
||||||
|
S3SecretKey = strings.TrimSpace(viper.GetString("S3_SECRET_KEY"))
|
||||||
|
S3ForcePathStyle = viper.GetBool("S3_FORCE_PATH_STYLE")
|
||||||
|
S3PublicBaseURL = strings.TrimSuffix(strings.TrimSpace(viper.GetString("S3_PUBLIC_BASE_URL")), "/")
|
||||||
|
S3DocumentKeyPrefix = defaultString(strings.Trim(strings.TrimSpace(viper.GetString("S3_DOCUMENT_PREFIX")), "/"), "docs")
|
||||||
|
|
||||||
// SSO integration
|
// SSO integration
|
||||||
SSOIssuer = viper.GetString("SSO_ISSUER")
|
SSOIssuer = viper.GetString("SSO_ISSUER")
|
||||||
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
|
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
|
||||||
@@ -109,6 +136,7 @@ func init() {
|
|||||||
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
|
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
|
||||||
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
|
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
|
||||||
SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax")
|
SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax")
|
||||||
|
SSOTokenBlacklistPrefix = defaultString(viper.GetString("SSO_TOKEN_BLACKLIST_PREFIX"), "sso:blacklist")
|
||||||
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
|
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
|
||||||
SSOPKCETTL = time.Duration(ttl) * time.Second
|
SSOPKCETTL = time.Duration(ttl) * time.Second
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
@@ -13,10 +14,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Connect(dbHost, dbName string) *gorm.DB {
|
func Connect(dbHost, dbName string) *gorm.DB {
|
||||||
dsn := fmt.Sprintf(
|
parts := []string{
|
||||||
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
|
fmt.Sprintf("host=%s", dbHost),
|
||||||
dbHost, config.DBUser, config.DBPassword, dbName, config.DBPort,
|
fmt.Sprintf("user=%s", config.DBUser),
|
||||||
)
|
fmt.Sprintf("password=%s", config.DBPassword),
|
||||||
|
fmt.Sprintf("dbname=%s", dbName),
|
||||||
|
fmt.Sprintf("port=%d", config.DBPort),
|
||||||
|
fmt.Sprintf("sslmode=%s", config.DBSSLMode),
|
||||||
|
"TimeZone=Asia/Shanghai",
|
||||||
|
}
|
||||||
|
if config.DBSSLRootCert != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("sslrootcert=%s", config.DBSSLRootCert))
|
||||||
|
}
|
||||||
|
if config.DBSSLCert != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("sslcert=%s", config.DBSSLCert))
|
||||||
|
}
|
||||||
|
if config.DBSSLKey != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("sslkey=%s", config.DBSSLKey))
|
||||||
|
}
|
||||||
|
dsn := strings.Join(parts, " ")
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||||
Logger: logger.Default.LogMode(logger.Info),
|
Logger: logger.Default.LogMode(logger.Info),
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
|
DROP TABLE IF EXISTS stock_logs;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_unique;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_deleted_at;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_warehouse_id;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_product_id;
|
||||||
|
DROP TABLE IF EXISTS product_warehouses;
|
||||||
DROP TABLE IF EXISTS fcr_standards;
|
DROP TABLE IF EXISTS fcr_standards;
|
||||||
DROP INDEX IF EXISTS suppliers_name_unique;
|
DROP INDEX IF EXISTS suppliers_name_unique;
|
||||||
DROP TABLE IF EXISTS product_suppliers;
|
DROP TABLE IF EXISTS product_suppliers;
|
||||||
|
|||||||
@@ -1,235 +1,333 @@
|
|||||||
-- USERS
|
-- USERS
|
||||||
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 (name, flagable_id, flagable_type);
|
CREATE UNIQUE INDEX flags_unique_flagable ON flags (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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name) WHERE deleted_at IS NULL;
|
|
||||||
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code) WHERE deleted_at IS NULL;
|
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- UOM
|
-- 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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- BANKS
|
-- 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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX banks_name_unique ON banks (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX banks_name_unique ON banks (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- AREAS
|
-- 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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX areas_name_unique ON areas (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX areas_name_unique ON areas (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- LOCATIONS
|
-- 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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX locations_name_unique ON locations (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX locations_name_unique ON locations (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- KANDANG
|
-- 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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- WAREHOUSES
|
-- 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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- CUSTOMERS
|
-- 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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX customers_name_unique ON customers (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX customers_name_unique ON customers (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- NONSTOCK
|
-- 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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- FCR
|
-- 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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
CREATE TABLE fcr_standards (
|
CREATE TABLE fcr_standards (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
fcr_id BIGINT NOT NULL REFERENCES fcrs(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
CREATE TABLE nonstock_suppliers (
|
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,
|
||||||
product_price NUMERIC(15,3) NOT NULL,
|
product_price NUMERIC(15, 3) NOT NULL,
|
||||||
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
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
|
|
||||||
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
|
CREATE UNIQUE INDEX products_name_unique ON products (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX products_sku_unique ON products (sku)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
CREATE TABLE product_suppliers (
|
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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- PRODUCT WAREHOUSES TABLE
|
||||||
|
CREATE TABLE product_warehouses (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
product_id BIGINT NOT NULL REFERENCES products (id),
|
||||||
|
warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
|
||||||
|
quantity INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_by BIGINT NOT NULL REFERENCES users (id),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- INDEXES
|
||||||
|
CREATE INDEX idx_product_warehouses_product_id ON product_warehouses (product_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_product_warehouses_warehouse_id ON product_warehouses (warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_product_warehouses_deleted_at ON product_warehouses (deleted_at);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_product_warehouses_unique ON product_warehouses (product_id, warehouse_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- STOCK LOGS
|
||||||
|
CREATE TABLE stock_logs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
transaction_type VARCHAR(20) NOT NULL,
|
||||||
|
quantity NUMERIC(15, 3) NOT NULL,
|
||||||
|
before_quantity NUMERIC(15, 3) NOT NULL,
|
||||||
|
after_quantity NUMERIC(15, 3) NOT NULL,
|
||||||
|
log_type VARCHAR(50) NOT NULL,
|
||||||
|
log_id BIGINT,
|
||||||
|
note TEXT,
|
||||||
|
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for better performance
|
||||||
|
CREATE INDEX stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX stock_logs_log_type_log_id_idx ON stock_logs (log_type, log_id);
|
||||||
|
|
||||||
|
CREATE INDEX stock_logs_created_by_idx ON stock_logs (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX stock_logs_created_at_idx ON stock_logs (created_at);
|
||||||
|
|
||||||
|
CREATE INDEX stock_logs_deleted_at_idx ON stock_logs (deleted_at);
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
DROP CONSTRAINT IF EXISTS users_id_user_key;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD CONSTRAINT users_id_user_key UNIQUE (id_user);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- DROP TABLE: STOCK_TRANSFERS DAN SEQUENCE-NYA
|
||||||
|
DROP TABLE IF EXISTS stock_transfers CASCADE;
|
||||||
|
|
||||||
|
DROP SEQUENCE IF EXISTS stock_transfer_seq CASCADE;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- ===============================================================
|
||||||
|
-- STOCK TRANSFERS (HEADER)
|
||||||
|
-- ===============================================================
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS stock_transfer_seq START 1;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS stock_transfers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
movement_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
from_warehouse_id BIGINT NOT NULL,
|
||||||
|
to_warehouse_id BIGINT NOT NULL,
|
||||||
|
area_id BIGINT,
|
||||||
|
reason TEXT,
|
||||||
|
transfer_date DATE NOT NULL,
|
||||||
|
created_by BIGINT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
|
||||||
|
ALTER TABLE stock_transfers
|
||||||
|
ADD CONSTRAINT fk_stock_transfers_from_warehouse
|
||||||
|
FOREIGN KEY (from_warehouse_id)
|
||||||
|
REFERENCES warehouses(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE stock_transfers
|
||||||
|
ADD CONSTRAINT fk_stock_transfers_to_warehouse
|
||||||
|
FOREIGN KEY (to_warehouse_id)
|
||||||
|
REFERENCES warehouses(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'areas') THEN
|
||||||
|
ALTER TABLE stock_transfers
|
||||||
|
ADD CONSTRAINT fk_stock_transfers_area
|
||||||
|
FOREIGN KEY (area_id)
|
||||||
|
REFERENCES areas(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE stock_transfers
|
||||||
|
ADD CONSTRAINT fk_stock_transfers_created_by
|
||||||
|
FOREIGN KEY (created_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transfers_from_warehouse_id ON stock_transfers(from_warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transfers_to_warehouse_id ON stock_transfers(to_warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transfers_transfer_date ON stock_transfers(transfer_date);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- DROP TABLE: STOCK_TRANSFER_DETAILS
|
||||||
|
DROP TABLE IF EXISTS stock_transfer_details CASCADE;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- ===============================================================
|
||||||
|
-- STOCK TRANSFER DETAILS (PRODUK)
|
||||||
|
-- ===============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS stock_transfer_details (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
stock_transfer_id BIGINT NOT NULL,
|
||||||
|
product_id BIGINT NOT NULL,
|
||||||
|
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0),
|
||||||
|
before_quantity NUMERIC(15, 3),
|
||||||
|
after_quantity NUMERIC(15, 3),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ===============================================================
|
||||||
|
-- FOREIGN KEYS (dengan pengecekan tabel agar anti gagal)
|
||||||
|
-- ===============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE stock_transfer_details
|
||||||
|
ADD CONSTRAINT fk_stock_transfer_details_transfer
|
||||||
|
FOREIGN KEY (stock_transfer_id)
|
||||||
|
REFERENCES stock_transfers(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE stock_transfer_details
|
||||||
|
ADD CONSTRAINT fk_stock_transfer_details_product
|
||||||
|
FOREIGN KEY (product_id)
|
||||||
|
REFERENCES products(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ===============================================================
|
||||||
|
-- INDEXES
|
||||||
|
-- ===============================================================
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_transfer_id ON stock_transfer_details (stock_transfer_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_product_id ON stock_transfer_details (product_id);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- DROP TABLE: STOCK_TRANSFER_DELIVERIES
|
||||||
|
DROP TABLE IF EXISTS stock_transfer_deliveries CASCADE;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- ===============================================================
|
||||||
|
-- STOCK TRANSFER DELIVERIES (EKSPEDISI)
|
||||||
|
-- ===============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS stock_transfer_deliveries (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
stock_transfer_id BIGINT NOT NULL,
|
||||||
|
supplier_id BIGINT,
|
||||||
|
vehicle_plate VARCHAR(20),
|
||||||
|
driver_name VARCHAR(100),
|
||||||
|
document_number VARCHAR(50),
|
||||||
|
document_path TEXT,
|
||||||
|
shipping_cost_item NUMERIC(15,3),
|
||||||
|
shipping_cost_total NUMERIC(15,3),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- FOREIGN KEYS
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
|
||||||
|
ALTER TABLE stock_transfer_deliveries
|
||||||
|
ADD CONSTRAINT fk_stock_transfer_deliveries_transfer
|
||||||
|
FOREIGN KEY (stock_transfer_id)
|
||||||
|
REFERENCES stock_transfers(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
|
||||||
|
ALTER TABLE stock_transfer_deliveries
|
||||||
|
ADD CONSTRAINT fk_stock_transfer_deliveries_supplier
|
||||||
|
FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_transfer_id ON stock_transfer_deliveries(stock_transfer_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_supplier_id ON stock_transfer_deliveries(supplier_id);
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- DROP PIVOT TABLE: STOCK_TRANSFER_DELIVERY_ITEMS
|
||||||
|
DROP TABLE IF EXISTS stock_transfer_delivery_items CASCADE;
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
-- ===============================================================
|
||||||
|
-- STOCK TRANSFER DELIVERY ITEMS (PIVOT)
|
||||||
|
-- ===============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS stock_transfer_delivery_items (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
stock_transfer_delivery_id BIGINT NOT NULL,
|
||||||
|
stock_transfer_detail_id BIGINT NOT NULL,
|
||||||
|
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- FOREIGN KEYS
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_deliveries') THEN
|
||||||
|
ALTER TABLE stock_transfer_delivery_items
|
||||||
|
ADD CONSTRAINT fk_delivery_items_delivery
|
||||||
|
FOREIGN KEY (stock_transfer_delivery_id)
|
||||||
|
REFERENCES stock_transfer_deliveries(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_details') THEN
|
||||||
|
ALTER TABLE stock_transfer_delivery_items
|
||||||
|
ADD CONSTRAINT fk_delivery_items_detail
|
||||||
|
FOREIGN KEY (stock_transfer_detail_id)
|
||||||
|
REFERENCES stock_transfer_details(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_delivery_id ON stock_transfer_delivery_items (stock_transfer_delivery_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_detail_id ON stock_transfer_delivery_items (stock_transfer_detail_id);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
DROP COLUMN IF EXISTS status;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
ADD COLUMN status VARCHAR(20);
|
||||||
|
|
||||||
|
UPDATE kandangs
|
||||||
|
SET status = 'NON_ACTIVE'
|
||||||
|
WHERE status IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE kandangs
|
||||||
|
ALTER COLUMN status SET NOT NULL;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
DROP COLUMN IF EXISTS project_flock_id;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS project_flocks;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS flocks;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
CREATE TABLE flocks (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX flocks_name_unique ON flocks (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE TABLE project_flocks (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
flock_id BIGINT NOT NULL REFERENCES flocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
period INT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE kandangs
|
||||||
|
ADD COLUMN project_flock_id BIGINT REFERENCES project_flocks (id) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX IF EXISTS approvals_approvable_lookup;
|
||||||
|
DROP TABLE IF EXISTS approvals;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE approvals (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
approvable_type VARCHAR(50) NOT NULL,
|
||||||
|
approvable_id BIGINT NOT NULL,
|
||||||
|
step SMALLINT NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||||
|
action_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX approvals_approvable_lookup ON approvals (approvable_type, approvable_id);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
ALTER TABLE approvals
|
||||||
|
RENAME COLUMN action TO status;
|
||||||
|
|
||||||
|
UPDATE approvals
|
||||||
|
SET status = 'PENDING'
|
||||||
|
WHERE status IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE approvals
|
||||||
|
ALTER COLUMN status SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE approvals
|
||||||
|
RENAME COLUMN step_number TO step;
|
||||||
|
|
||||||
|
ALTER TABLE approvals
|
||||||
|
DROP COLUMN step_name;
|
||||||
|
|
||||||
|
ALTER TABLE approvals
|
||||||
|
RENAME COLUMN action_at TO created_at;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
ALTER TABLE approvals
|
||||||
|
RENAME COLUMN status TO action;
|
||||||
|
|
||||||
|
ALTER TABLE approvals
|
||||||
|
ALTER COLUMN action DROP NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE approvals
|
||||||
|
RENAME COLUMN step TO step_number;
|
||||||
|
|
||||||
|
ALTER TABLE approvals
|
||||||
|
ADD COLUMN step_name VARCHAR NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE approvals
|
||||||
|
RENAME COLUMN created_at TO action_at;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS project_flock_kandangs;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE project_flock_kandangs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_id BIGINT NOT NULL REFERENCES project_flocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
kandang_id BIGINT NOT NULL REFERENCES kandangs (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
detached_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_project_flock_kandangs_project ON project_flock_kandangs (project_flock_id);
|
||||||
|
CREATE INDEX idx_project_flock_kandangs_kandang ON project_flock_kandangs (kandang_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_project_flock_kandangs_active ON project_flock_kandangs (project_flock_id, kandang_id)
|
||||||
|
WHERE
|
||||||
|
detached_at IS NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS project_chickins;
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS project_flock_populations;
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Recreate legacy columns on project_flock_kandangs
|
||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandangs_unique;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
ADD COLUMN IF NOT EXISTS assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS detached_at TIMESTAMPTZ,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_active
|
||||||
|
ON project_flock_kandangs (project_flock_id, kandang_id)
|
||||||
|
WHERE detached_at IS NULL;
|
||||||
|
|
||||||
|
-- Restore product_category_id reference and drop category column
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ADD COLUMN IF NOT EXISTS product_category_id BIGINT REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
DROP COLUMN IF EXISTS category;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Add category column to project_flocks and backfill existing rows
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ADD COLUMN IF NOT EXISTS category VARCHAR(20);
|
||||||
|
|
||||||
|
UPDATE project_flocks
|
||||||
|
SET category = 'GROWING'
|
||||||
|
WHERE category IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ALTER COLUMN category SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ALTER COLUMN category SET DEFAULT 'GROWING';
|
||||||
|
|
||||||
|
-- Drop legacy foreign key reference and column
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
DROP CONSTRAINT IF EXISTS project_flocks_product_category_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
DROP COLUMN IF EXISTS product_category_id;
|
||||||
|
|
||||||
|
-- Simplify project_flock_kandangs structure
|
||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandangs_active;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
DROP COLUMN IF EXISTS created_by,
|
||||||
|
DROP COLUMN IF EXISTS assigned_at,
|
||||||
|
DROP COLUMN IF EXISTS detached_at,
|
||||||
|
DROP COLUMN IF EXISTS updated_at;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ALTER COLUMN created_at SET DEFAULT NOW();
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_unique
|
||||||
|
ON project_flock_kandangs (project_flock_id, kandang_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX project_flocks_flock_period_unique
|
||||||
|
ON project_flocks (flock_id, period)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS project_chickin_details;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS project_chickin_details (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_chickin_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
quantity NUMERIC(15, 3) NOT NULL,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
||||||
|
ALTER TABLE project_chickin_details
|
||||||
|
ADD CONSTRAINT fk_project_chickin_id
|
||||||
|
FOREIGN KEY (project_chickin_id)
|
||||||
|
REFERENCES project_chickins(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE project_chickin_details
|
||||||
|
ADD CONSTRAINT fk_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE project_chickin_details
|
||||||
|
ADD CONSTRAINT fk_created_by
|
||||||
|
FOREIGN KEY (created_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_project_chickin_id ON project_chickin_details (project_chickin_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_product_warehouse_id ON project_chickin_details (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_created_by ON project_chickin_details (created_by);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
--? Child Indexes(optional, biar rapi tapi klo gada juga ilang pas di drop)
|
||||||
|
DROP INDEX IF EXISTS idx_recording_stocks_product;
|
||||||
|
DROP INDEX IF EXISTS idx_recording_stocks_recording;
|
||||||
|
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_recording_depl_recording;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_recording_bws_recording;
|
||||||
|
|
||||||
|
--? Child Tables
|
||||||
|
DROP TABLE IF EXISTS recording_stocks;
|
||||||
|
DROP TABLE IF EXISTS recording_depletions;
|
||||||
|
DROP TABLE IF EXISTS recording_bws;
|
||||||
|
|
||||||
|
--? Parent Indexes ON recordings
|
||||||
|
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
|
||||||
|
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
|
||||||
|
|
||||||
|
--? Parent table
|
||||||
|
DROP TABLE IF EXISTS recordings;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
--? RECORDINGS (tabel induk recording harian)
|
||||||
|
CREATE TABLE IF NOT EXISTS recordings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_id BIGINT NOT NULL,
|
||||||
|
record_datetime TIMESTAMPTZ NOT NULL,
|
||||||
|
record_date DATE,
|
||||||
|
status INT NOT NULL DEFAULT 0, --? 0=draft,1=submitted,2=approved,3=rejected
|
||||||
|
ontime INT NOT NULL DEFAULT 0, --? 1=ontime,0=late (pakai INT/BOOLEAN sesuai preferensi)
|
||||||
|
day INT,
|
||||||
|
total_depletion INT,
|
||||||
|
cum_depletion_rate NUMERIC(7,3),
|
||||||
|
daily_gain NUMERIC(7,3),
|
||||||
|
avg_daily_gain NUMERIC(7,3),
|
||||||
|
cum_intake INT,
|
||||||
|
fcr_value NUMERIC(7,3),
|
||||||
|
total_chick BIGINT,
|
||||||
|
daily_depletion_rate NUMERIC(7,3),
|
||||||
|
cum_depletion INT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recordings_project_flock
|
||||||
|
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id),
|
||||||
|
|
||||||
|
CONSTRAINT fk_recordings_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||||
|
|
||||||
|
|
||||||
|
CONSTRAINT chk_recordings_status
|
||||||
|
CHECK (status IN (0,1,2,3)),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recordings_ontime
|
||||||
|
CHECK (ontime IN (0,1)),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recordings_day
|
||||||
|
CHECK (day IS NULL OR day >= 1),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recordings_nonnegatives
|
||||||
|
CHECK (
|
||||||
|
(total_depletion IS NULL OR total_depletion >= 0) AND
|
||||||
|
(cum_depletion IS NULL OR cum_depletion >= 0) AND
|
||||||
|
(total_chick IS NULL OR total_chick >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||||
|
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value > 0) AND
|
||||||
|
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
--? Set record_date otomatis berdasarkan record_datetime (pakai zona Asia/Jakarta)
|
||||||
|
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
|
||||||
|
CREATE TRIGGER recordings_set_record_date_trg
|
||||||
|
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recordings_flock_datetime
|
||||||
|
ON recordings (project_flock_id, record_datetime);
|
||||||
|
|
||||||
|
--? Unique harian (1 recording per hari dan per flock)
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_recordings_flock_record_date
|
||||||
|
ON recordings (project_flock_id, record_date)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
|
||||||
|
--? RECORDING_BWS (BW per recording)
|
||||||
|
CREATE TABLE IF NOT EXISTS recording_bws (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_id BIGINT NOT NULL,
|
||||||
|
weight NUMERIC(8,2) NOT NULL, --? bobot per ekor/kelompok
|
||||||
|
qty INT NOT NULL DEFAULT 1, --? jumlah ekor pada bobot ini
|
||||||
|
notes VARCHAR,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_bws_recording
|
||||||
|
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT chk_recording_bws_nonneg
|
||||||
|
CHECK (weight >= 0 AND qty >= 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_bws_recording
|
||||||
|
ON recording_bws (recording_id);
|
||||||
|
|
||||||
|
--? RECORDING_DEPLETIONS
|
||||||
|
CREATE TABLE IF NOT EXISTS recording_depletions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
total BIGINT NOT NULL,
|
||||||
|
notes VARCHAR,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_depl_recording
|
||||||
|
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_depl_prodwh
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recording_depl_total
|
||||||
|
CHECK (total >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_depl_recording
|
||||||
|
ON recording_depletions (recording_id);
|
||||||
|
|
||||||
|
--? RECORDING_STOCKS
|
||||||
|
CREATE TABLE IF NOT EXISTS recording_stocks (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
increase NUMERIC(10,3), --? penambahan (boleh NULL)
|
||||||
|
decrease NUMERIC(10,3), --? pengurangan (boleh NULL)
|
||||||
|
usage_amount BIGINT, --? pemakaian (opsional, jika konsep dipisah dari decrease)
|
||||||
|
notes VARCHAR,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_stocks_recording
|
||||||
|
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_stocks_prodwh
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recording_stocks_nonneg
|
||||||
|
CHECK (
|
||||||
|
(increase IS NULL OR increase >= 0) AND
|
||||||
|
(decrease IS NULL OR decrease >= 0) AND
|
||||||
|
(usage_amount IS NULL OR usage_amount >= 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_stocks_recording
|
||||||
|
ON recording_stocks (recording_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_stocks_product
|
||||||
|
ON recording_stocks (product_warehouse_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
|
||||||
|
|
||||||
|
-- Only alter if tables exist
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS project_flocks_base_period_unique;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ADD COLUMN IF NOT EXISTS flock_id BIGINT;
|
||||||
|
|
||||||
|
WITH normalized AS (
|
||||||
|
SELECT
|
||||||
|
pf.id,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||||
|
CONCAT('Project Flock ', pf.id)
|
||||||
|
) AS normalized_name,
|
||||||
|
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
|
||||||
|
FROM project_flocks pf
|
||||||
|
),
|
||||||
|
seed_flocks AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
n.normalized_name,
|
||||||
|
MIN(n.created_by) AS created_by
|
||||||
|
FROM normalized n
|
||||||
|
GROUP BY n.normalized_name
|
||||||
|
)
|
||||||
|
INSERT INTO flocks (name, created_by, created_at, updated_at)
|
||||||
|
SELECT sf.normalized_name, sf.created_by, NOW(), NOW()
|
||||||
|
FROM seed_flocks sf
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
WITH normalized AS (
|
||||||
|
SELECT
|
||||||
|
pf.id,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||||
|
CONCAT('Project Flock ', pf.id)
|
||||||
|
) AS normalized_name
|
||||||
|
FROM project_flocks pf
|
||||||
|
),
|
||||||
|
resolved AS (
|
||||||
|
SELECT
|
||||||
|
n.id,
|
||||||
|
f.id AS flock_id
|
||||||
|
FROM normalized n
|
||||||
|
JOIN flocks f ON LOWER(f.name) = LOWER(n.normalized_name)
|
||||||
|
)
|
||||||
|
UPDATE project_flocks pf
|
||||||
|
SET flock_id = resolved.flock_id
|
||||||
|
FROM resolved
|
||||||
|
WHERE pf.id = resolved.id;
|
||||||
|
|
||||||
|
WITH missing AS (
|
||||||
|
SELECT
|
||||||
|
pf.id,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||||
|
CONCAT('Project Flock ', pf.id)
|
||||||
|
) AS normalized_name,
|
||||||
|
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
|
||||||
|
FROM project_flocks pf
|
||||||
|
WHERE pf.flock_id IS NULL
|
||||||
|
),
|
||||||
|
seed_missing AS (
|
||||||
|
SELECT DISTINCT normalized_name, created_by FROM missing
|
||||||
|
)
|
||||||
|
INSERT INTO flocks (name, created_by, created_at, updated_at)
|
||||||
|
SELECT sm.normalized_name, sm.created_by, NOW(), NOW()
|
||||||
|
FROM seed_missing sm
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
WITH missing AS (
|
||||||
|
SELECT
|
||||||
|
pf.id,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||||
|
CONCAT('Project Flock ', pf.id)
|
||||||
|
) AS normalized_name
|
||||||
|
FROM project_flocks pf
|
||||||
|
WHERE pf.flock_id IS NULL
|
||||||
|
)
|
||||||
|
UPDATE project_flocks pf
|
||||||
|
SET flock_id = f.id
|
||||||
|
FROM missing m
|
||||||
|
JOIN flocks f ON LOWER(f.name) = LOWER(m.normalized_name)
|
||||||
|
WHERE pf.id = m.id;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ALTER COLUMN flock_id SET NOT NULL;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS project_flocks_flock_name_unique;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
DROP COLUMN IF EXISTS flock_name;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_period_unique
|
||||||
|
ON project_flocks (flock_id, period)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ADD COLUMN IF NOT EXISTS flock_name VARCHAR(255);
|
||||||
|
|
||||||
|
WITH generated_names AS (
|
||||||
|
SELECT
|
||||||
|
pf.id,
|
||||||
|
COALESCE(f.name, CONCAT('Project Flock ', pf.id)) AS base_name,
|
||||||
|
pf.period,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY COALESCE(f.name, CONCAT('Project Flock ', pf.id)) ORDER BY pf.id) AS rn
|
||||||
|
FROM project_flocks pf
|
||||||
|
LEFT JOIN flocks f ON f.id = pf.flock_id
|
||||||
|
)
|
||||||
|
UPDATE project_flocks pf
|
||||||
|
SET flock_name = CASE
|
||||||
|
WHEN gn.period IS NOT NULL THEN
|
||||||
|
CASE
|
||||||
|
WHEN gn.rn = 1 THEN CONCAT(gn.base_name, ' ', gn.period)
|
||||||
|
ELSE CONCAT(gn.base_name, ' ', gn.period, ' ', gn.rn)
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
CASE
|
||||||
|
WHEN gn.rn = 1 THEN gn.base_name
|
||||||
|
ELSE CONCAT(gn.base_name, ' ', gn.rn)
|
||||||
|
END
|
||||||
|
END
|
||||||
|
FROM generated_names gn
|
||||||
|
WHERE pf.id = gn.id
|
||||||
|
AND (pf.flock_name IS NULL OR pf.flock_name = '');
|
||||||
|
|
||||||
|
UPDATE project_flocks
|
||||||
|
SET flock_name = CONCAT('Project Flock ', id)
|
||||||
|
WHERE flock_name IS NULL OR flock_name = '';
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ALTER COLUMN flock_name SET NOT NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_name_unique
|
||||||
|
ON project_flocks (flock_name)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique
|
||||||
|
ON project_flocks (
|
||||||
|
LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))),
|
||||||
|
period
|
||||||
|
)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
DROP COLUMN IF EXISTS flock_id;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop newly introduced egg tables
|
||||||
|
DROP TABLE IF EXISTS grading_eggs;
|
||||||
|
DROP TABLE IF EXISTS recording_eggs;
|
||||||
|
|
||||||
|
-- Revert recording_stocks structure
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
|
||||||
|
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
DROP COLUMN IF EXISTS usage_qty,
|
||||||
|
DROP COLUMN IF EXISTS pending_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
ADD COLUMN increase NUMERIC(10,3),
|
||||||
|
ADD COLUMN decrease NUMERIC(10,3),
|
||||||
|
ADD COLUMN usage_amount BIGINT,
|
||||||
|
ADD COLUMN notes VARCHAR;
|
||||||
|
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
|
||||||
|
(increase IS NULL OR increase >= 0) AND
|
||||||
|
(decrease IS NULL OR decrease >= 0) AND
|
||||||
|
(usage_amount IS NULL OR usage_amount >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Revert recording_depletions structure
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_depl_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
ALTER COLUMN qty TYPE BIGINT USING COALESCE(qty, 0)::BIGINT;
|
||||||
|
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
RENAME COLUMN qty TO total;
|
||||||
|
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
ADD COLUMN notes VARCHAR;
|
||||||
|
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
ADD CONSTRAINT chk_recording_depl_total CHECK (total >= 0);
|
||||||
|
|
||||||
|
-- Revert recording_bws structure
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
ALTER COLUMN qty TYPE INT USING COALESCE(qty, 0)::INT;
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
DROP COLUMN IF EXISTS total_weight;
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
RENAME COLUMN avg_weight TO weight;
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
ADD COLUMN notes VARCHAR;
|
||||||
|
|
||||||
|
UPDATE recording_bws
|
||||||
|
SET qty = GREATEST(qty, 1);
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (weight >= 0 AND qty >= 1);
|
||||||
|
|
||||||
|
-- Revert recordings header
|
||||||
|
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock_kandang,
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ALTER COLUMN total_depletion_qty TYPE INT USING COALESCE(total_depletion_qty, 0)::INT,
|
||||||
|
ALTER COLUMN total_chick_qty TYPE BIGINT USING COALESCE(total_chick_qty, 0)::BIGINT;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
RENAME COLUMN total_depletion_qty TO total_depletion;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
RENAME COLUMN total_chick_qty TO total_chick;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD COLUMN record_date DATE,
|
||||||
|
ADD COLUMN status INT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN ontime INT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN daily_depletion_rate NUMERIC(7,3),
|
||||||
|
ADD COLUMN cum_depletion INT;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
RENAME COLUMN project_flock_kandangs_id TO project_flock_id;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT fk_recordings_project_flock
|
||||||
|
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id);
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_status CHECK (status IN (0,1,2,3));
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_ontime CHECK (ontime IN (0,1));
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_nonnegatives CHECK (
|
||||||
|
(total_depletion IS NULL OR total_depletion >= 0) AND
|
||||||
|
(cum_depletion IS NULL OR cum_depletion >= 0) AND
|
||||||
|
(total_chick IS NULL OR total_chick >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||||
|
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value > 0) AND
|
||||||
|
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Ensure new columns carry derived data
|
||||||
|
UPDATE recordings
|
||||||
|
SET record_date = (record_datetime AT TIME ZONE 'Asia/Jakarta')::date
|
||||||
|
WHERE record_date IS NULL;
|
||||||
|
|
||||||
|
-- Restore helper trigger/function and indexes
|
||||||
|
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER recordings_set_record_date_trg
|
||||||
|
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
|
||||||
|
|
||||||
|
CREATE INDEX idx_recordings_flock_datetime
|
||||||
|
ON recordings (project_flock_id, record_datetime);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX uq_recordings_flock_record_date
|
||||||
|
ON recordings (project_flock_id, record_date)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop trigger & helper function tied to record_date before removing the column
|
||||||
|
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
|
||||||
|
DROP FUNCTION IF EXISTS trg_set_record_date();
|
||||||
|
|
||||||
|
-- Drop indexes and constraints that reference legacy columns
|
||||||
|
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
|
||||||
|
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock,
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_status,
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_ontime,
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives;
|
||||||
|
|
||||||
|
-- Align recordings header with the new schema
|
||||||
|
ALTER TABLE recordings
|
||||||
|
RENAME COLUMN project_flock_id TO project_flock_kandangs_id;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP COLUMN IF EXISTS record_date,
|
||||||
|
DROP COLUMN IF EXISTS status,
|
||||||
|
DROP COLUMN IF EXISTS ontime,
|
||||||
|
DROP COLUMN IF EXISTS daily_depletion_rate,
|
||||||
|
DROP COLUMN IF EXISTS cum_depletion;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
RENAME COLUMN total_depletion TO total_depletion_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
RENAME COLUMN total_chick TO total_chick_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ALTER COLUMN total_depletion_qty TYPE NUMERIC(15,3) USING COALESCE(total_depletion_qty, 0)::NUMERIC(15,3),
|
||||||
|
ALTER COLUMN total_chick_qty TYPE NUMERIC(15,3) USING COALESCE(total_chick_qty, 0)::NUMERIC(15,3),
|
||||||
|
ALTER COLUMN cum_intake TYPE INT USING COALESCE(cum_intake, 0)::INT;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT fk_recordings_project_flock_kandang
|
||||||
|
FOREIGN KEY (project_flock_kandangs_id) REFERENCES project_flock_kandangs(id);
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK (
|
||||||
|
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
|
||||||
|
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||||
|
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value >= 0) AND
|
||||||
|
(total_chick_qty IS NULL OR total_chick_qty >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_recordings_flock_datetime
|
||||||
|
ON recordings (project_flock_kandangs_id, record_datetime);
|
||||||
|
|
||||||
|
-- recording_bws reshape
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
RENAME COLUMN weight TO avg_weight;
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
ADD COLUMN total_weight NUMERIC(10,3);
|
||||||
|
|
||||||
|
UPDATE recording_bws
|
||||||
|
SET total_weight = COALESCE(avg_weight, 0) * COALESCE(qty, 0);
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
ALTER COLUMN total_weight SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
DROP COLUMN IF EXISTS notes;
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
|
||||||
|
|
||||||
|
ALTER TABLE recording_bws
|
||||||
|
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (
|
||||||
|
avg_weight >= 0 AND qty >= 0 AND total_weight >= 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- recording_depletions reshape
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
RENAME COLUMN total TO qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
|
||||||
|
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
DROP COLUMN IF EXISTS notes;
|
||||||
|
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_depl_total;
|
||||||
|
|
||||||
|
ALTER TABLE recording_depletions
|
||||||
|
ADD CONSTRAINT chk_recording_depl_qty CHECK (qty >= 0);
|
||||||
|
|
||||||
|
-- recording_stocks reshape
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
|
||||||
|
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
DROP COLUMN IF EXISTS increase,
|
||||||
|
DROP COLUMN IF EXISTS decrease,
|
||||||
|
DROP COLUMN IF EXISTS usage_amount,
|
||||||
|
DROP COLUMN IF EXISTS notes;
|
||||||
|
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
ADD COLUMN usage_qty NUMERIC(15,3),
|
||||||
|
ADD COLUMN pending_qty NUMERIC(15,3);
|
||||||
|
|
||||||
|
ALTER TABLE recording_stocks
|
||||||
|
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
|
||||||
|
(usage_qty IS NULL OR usage_qty >= 0) AND
|
||||||
|
(pending_qty IS NULL OR pending_qty >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- recording_eggs table
|
||||||
|
CREATE TABLE recording_eggs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
qty INT NOT NULL,
|
||||||
|
created_by BIGINT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_eggs_recording
|
||||||
|
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_recording_eggs_product_warehouse
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
|
||||||
|
CONSTRAINT fk_recording_eggs_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||||
|
CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_recording_eggs_recording
|
||||||
|
ON recording_eggs (recording_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_recording_eggs_product
|
||||||
|
ON recording_eggs (product_warehouse_id);
|
||||||
|
|
||||||
|
-- grading_eggs table
|
||||||
|
CREATE TABLE grading_eggs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_egg_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15,3) NOT NULL,
|
||||||
|
grade VARCHAR,
|
||||||
|
created_by BIGINT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT fk_grading_eggs_recording_egg
|
||||||
|
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_grading_eggs_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||||
|
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_grading_eggs_recording_egg
|
||||||
|
ON grading_eggs (recording_egg_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -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 $$;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user