mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf7b3418a5 | |||
| f60564d673 | |||
| 3ada837b8b | |||
| 4ce7611c26 | |||
| e98d0a9fa1 | |||
| 08c8c4a747 | |||
| de6304332b | |||
| f073bcc2c1 | |||
| 4853891191 | |||
| 086184bbaa | |||
| 4161dcfbdd | |||
| d0309f25dd | |||
| 59ebe29ec8 | |||
| 2b6ba3a41d | |||
| bb1e6833f0 | |||
| a536094481 | |||
| c33cc05f72 | |||
| 3f9865d267 | |||
| 822ca0268e | |||
| 16d1358b3a | |||
| e00f168a15 | |||
| 79d488c979 | |||
| 576f8083a3 | |||
| d7c543bc9d | |||
| 4a2a80916f | |||
| 511e5501bb | |||
| 0fbf04fc1d | |||
| 536e76d481 | |||
| 29aa737422 | |||
| 26f2f3ccbf | |||
| 4b147a3be7 | |||
| 7094d90034 | |||
| 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 | |||
| 1fc750efd3 | |||
| 1156b376fc |
+27
-6
@@ -6,24 +6,45 @@ deploy-dev:
|
|||||||
image: alpine:3.20
|
image: alpine:3.20
|
||||||
variables:
|
variables:
|
||||||
DEPLOY_APP: "LTI-MBUGROUP"
|
DEPLOY_APP: "LTI-MBUGROUP"
|
||||||
|
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
||||||
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
GIT_DEPTH: "1"
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- echo "🧰 Installing dependencies..."
|
- echo "🧰 Installing dependencies..."
|
||||||
- apk update && apk add --no-cache openssh git curl
|
- apk update && apk add --no-cache openssh git curl bash
|
||||||
|
|
||||||
|
# Setup SSH di runner
|
||||||
- mkdir -p ~/.ssh
|
- mkdir -p ~/.ssh
|
||||||
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||||
- chmod 600 ~/.ssh/id_rsa
|
- chmod 600 ~/.ssh/id_rsa
|
||||||
- eval $(ssh-agent -s)
|
- eval "$(ssh-agent -s)"
|
||||||
- ssh-add ~/.ssh/id_rsa
|
- ssh-add ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
|
||||||
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
||||||
|
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||||
|
|
||||||
- >
|
- >
|
||||||
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
||||||
cd /home/devops/docker/deployment/development/lti-api &&
|
set -e
|
||||||
git fetch origin development &&
|
|
||||||
git reset --hard origin/development &&
|
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
|
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
|
||||||
"; then
|
"; then
|
||||||
STATUS='success';
|
STATUS='success';
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ go 1.23
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0
|
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/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
|
||||||
@@ -20,17 +26,33 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
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
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
github.com/jackc/pgio v1.0.0 // indirect
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
@@ -51,6 +73,7 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/philhofer/fwd v1.1.2 // indirect
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
@@ -75,4 +98,8 @@ require (
|
|||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.22.5 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
modernc.org/sqlite v1.23.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
@@ -27,12 +65,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||||
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -50,6 +94,8 @@ 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=
|
||||||
@@ -146,6 +192,9 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
@@ -306,4 +355,12 @@ gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
|||||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||||
|
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||||
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||||
|
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
|
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||||
|
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package capabilities
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
permission "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
||||||
@@ -37,8 +37,8 @@ func normalizeAndAllow(perm string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allowed = map[string]struct{}{
|
var allowed = map[string]struct{}{
|
||||||
recordings.PermissionRecordingRead: {},
|
permission.PermissionRecordingRead: {},
|
||||||
recordings.PermissionRecordingCreate: {},
|
permission.PermissionRecordingCreate: {},
|
||||||
recordings.PermissionRecordingUpdate: {},
|
permission.PermissionRecordingUpdate: {},
|
||||||
recordings.PermissionRecordingDelete: {},
|
permission.PermissionRecordingDelete: {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
}
|
||||||
@@ -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,820 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FifoService interface {
|
||||||
|
RegisterStockable(cfg fifo.StockableConfig) error
|
||||||
|
RegisterUsable(cfg fifo.UsableConfig) error
|
||||||
|
|
||||||
|
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
|
||||||
|
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
|
||||||
|
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type fifoService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
logger *logrus.Logger
|
||||||
|
allocations commonRepo.StockAllocationRepository
|
||||||
|
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||||
|
defaultOrderBy []string
|
||||||
|
pendingBatchPerUsable int
|
||||||
|
maxLotsPerStockable int
|
||||||
|
defaultAllocationNotes string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFifoService(
|
||||||
|
db *gorm.DB,
|
||||||
|
allocations commonRepo.StockAllocationRepository,
|
||||||
|
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository,
|
||||||
|
logger *logrus.Logger,
|
||||||
|
) FifoService {
|
||||||
|
if logger == nil {
|
||||||
|
logger = logrus.StandardLogger()
|
||||||
|
}
|
||||||
|
return &fifoService{
|
||||||
|
db: db,
|
||||||
|
logger: logger,
|
||||||
|
allocations: allocations,
|
||||||
|
productWarehouseRepo: productWarehouseRepo,
|
||||||
|
defaultOrderBy: []string{"created_at ASC", "id ASC"},
|
||||||
|
pendingBatchPerUsable: 25,
|
||||||
|
maxLotsPerStockable: 50,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) withTransaction(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
fn func(*gorm.DB) error,
|
||||||
|
) error {
|
||||||
|
if tx != nil {
|
||||||
|
return fn(tx.WithContext(ctx))
|
||||||
|
}
|
||||||
|
return s.db.WithContext(ctx).Transaction(func(inner *gorm.DB) error {
|
||||||
|
return fn(inner)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) txOrDB(tx, db *gorm.DB) *gorm.DB {
|
||||||
|
if tx != nil {
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) RegisterStockable(cfg fifo.StockableConfig) error {
|
||||||
|
return fifo.RegisterStockable(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) RegisterUsable(cfg fifo.UsableConfig) error {
|
||||||
|
return fifo.RegisterUsable(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockReplenishRequest struct {
|
||||||
|
StockableKey fifo.StockableKey
|
||||||
|
StockableID uint
|
||||||
|
ProductWarehouseID uint
|
||||||
|
Quantity float64
|
||||||
|
Note *string
|
||||||
|
Tx *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type PendingResolution struct {
|
||||||
|
UsableKey fifo.UsableKey
|
||||||
|
UsableID uint
|
||||||
|
Quantity float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockReplenishResult struct {
|
||||||
|
AddedQuantity float64
|
||||||
|
PendingResolved []PendingResolution
|
||||||
|
RemainingPending float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockConsumeRequest struct {
|
||||||
|
UsableKey fifo.UsableKey
|
||||||
|
UsableID uint
|
||||||
|
ProductWarehouseID uint
|
||||||
|
Quantity float64
|
||||||
|
AllowPending bool
|
||||||
|
Note *string
|
||||||
|
Tx *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllocationDetail struct {
|
||||||
|
StockableKey fifo.StockableKey
|
||||||
|
StockableID uint
|
||||||
|
Quantity float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockConsumeResult struct {
|
||||||
|
RequestedQuantity float64
|
||||||
|
UsageQuantity float64
|
||||||
|
PendingQuantity float64
|
||||||
|
AddedAllocations []AllocationDetail
|
||||||
|
ReleasedQuantity float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockReleaseRequest struct {
|
||||||
|
UsableKey fifo.UsableKey
|
||||||
|
UsableID uint
|
||||||
|
Reason *string
|
||||||
|
Tx *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
|
||||||
|
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
|
||||||
|
return nil, errors.New("stockable key and id are required")
|
||||||
|
}
|
||||||
|
if req.ProductWarehouseID == 0 {
|
||||||
|
return nil, errors.New("product warehouse id is required")
|
||||||
|
}
|
||||||
|
if req.Quantity <= 0 {
|
||||||
|
return nil, errors.New("quantity must be greater than zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := fifo.Stockable(req.StockableKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("stockable %q is not registered", req.StockableKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &StockReplenishResult{
|
||||||
|
AddedQuantity: req.Quantity,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
|
if err := s.incrementStockableQty(ctx, tx, cfg, req.StockableID, req.Quantity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
||||||
|
req.ProductWarehouseID: req.Quantity,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved, err := s.resolvePendingForWarehouse(ctx, tx, req.ProductWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result.PendingResolved = resolved
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) {
|
||||||
|
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
||||||
|
return nil, errors.New("usable key and id are required")
|
||||||
|
}
|
||||||
|
if req.Quantity < 0 {
|
||||||
|
return nil, errors.New("quantity must be zero or greater")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, ok := fifo.Usable(req.UsableKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &StockConsumeResult{
|
||||||
|
RequestedQuantity: req.Quantity,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
|
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
productWarehouseID := ctxRow.ProductWarehouseID
|
||||||
|
if productWarehouseID == 0 {
|
||||||
|
return fmt.Errorf("usable %q (id: %d) has no product warehouse reference", req.UsableKey, req.UsableID)
|
||||||
|
}
|
||||||
|
if req.ProductWarehouseID != 0 && req.ProductWarehouseID != productWarehouseID {
|
||||||
|
return fmt.Errorf("usable %q (id: %d) references product warehouse %d but %d was provided", req.UsableKey, req.UsableID, productWarehouseID, req.ProductWarehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUsage := ctxRow.UsageQty
|
||||||
|
currentPending := ctxRow.PendingQty
|
||||||
|
currentTotal := currentUsage + currentPending
|
||||||
|
delta := req.Quantity - currentTotal
|
||||||
|
|
||||||
|
var (
|
||||||
|
usageDelta float64
|
||||||
|
pendingDelta float64
|
||||||
|
addedAlloc []AllocationDetail
|
||||||
|
releasedAmount float64
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case delta > 0:
|
||||||
|
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if allocationRes.pending > 0 && !req.AllowPending {
|
||||||
|
return fmt.Errorf("insufficient stock: requested %.3f, allocated %.3f", req.Quantity, currentUsage+allocationRes.allocated)
|
||||||
|
}
|
||||||
|
|
||||||
|
usageDelta += allocationRes.allocated
|
||||||
|
pendingDelta += allocationRes.pending
|
||||||
|
addedAlloc = allocationRes.allocations
|
||||||
|
|
||||||
|
if allocationRes.allocated > 0 {
|
||||||
|
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
||||||
|
productWarehouseID: -allocationRes.allocated,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case delta < 0:
|
||||||
|
reductionTarget := -delta
|
||||||
|
|
||||||
|
if currentPending > 0 {
|
||||||
|
pendingReduction := math.Min(currentPending, reductionTarget)
|
||||||
|
if pendingReduction > 0 {
|
||||||
|
pendingDelta -= pendingReduction
|
||||||
|
reductionTarget -= pendingReduction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reductionTarget > 0 {
|
||||||
|
released, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, reductionTarget)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if released+1e-6 < reductionTarget {
|
||||||
|
return fmt.Errorf("unable to release %.3f from usable %d, only %.3f available", reductionTarget, req.UsableID, released)
|
||||||
|
}
|
||||||
|
usageDelta -= released
|
||||||
|
releasedAmount = released
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// no change
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.AddedAllocations = addedAlloc
|
||||||
|
result.ReleasedQuantity = releasedAmount
|
||||||
|
result.UsageQuantity = currentUsage + usageDelta
|
||||||
|
result.PendingQuantity = currentPending + pendingDelta
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest) error {
|
||||||
|
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
||||||
|
return errors.New("usable key and id are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
|
cfg, ok := fifo.Usable(req.UsableKey)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("usable %q is not registered", req.UsableKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var usageDelta, pendingDelta float64
|
||||||
|
if ctxRow.UsageQty > 0 {
|
||||||
|
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
usageDelta -= ctxRow.UsageQty
|
||||||
|
}
|
||||||
|
if ctxRow.PendingQty > 0 {
|
||||||
|
pendingDelta -= ctxRow.PendingQty
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.allocations.ReleaseByUsable(ctx, req.UsableKey.String(), req.UsableID, req.Reason, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
type usableContextRow struct {
|
||||||
|
ProductWarehouseID uint
|
||||||
|
UsageQty float64
|
||||||
|
PendingQty float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) loadUsableContext(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint) (*usableContextRow, error) {
|
||||||
|
var row usableContextRow
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Select(fmt.Sprintf("%s AS product_warehouse_id, COALESCE(%s,0) AS usage_qty, COALESCE(%s,0) AS pending_qty", cfg.Columns.ProductWarehouseID, cfg.Columns.UsageQuantity, cfg.Columns.PendingQuantity)).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"})
|
||||||
|
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Take(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fmt.Errorf("usable record %d not found", id)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) incrementStockableQty(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
|
||||||
|
column := cfg.Columns.TotalQuantity
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]any{
|
||||||
|
column: gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty),
|
||||||
|
}
|
||||||
|
if cfg.Columns.TotalUsedQuantity != "" {
|
||||||
|
updates[cfg.Columns.TotalUsedQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0)", cfg.Columns.TotalUsedQuantity))
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) incrementStockableUsage(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
|
||||||
|
if qty == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
column := cfg.Columns.TotalUsedQuantity
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.Update(column, gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty)).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type allocationOutcome struct {
|
||||||
|
allocated float64
|
||||||
|
pending float64
|
||||||
|
allocations []AllocationDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
type stockLot struct {
|
||||||
|
StockableKey fifo.StockableKey
|
||||||
|
RecordID uint
|
||||||
|
AvailableQty float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) allocateFromStock(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
productWarehouseID uint,
|
||||||
|
usableKey fifo.UsableKey,
|
||||||
|
usableID uint,
|
||||||
|
requestQty float64,
|
||||||
|
) (*allocationOutcome, error) {
|
||||||
|
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(lots) == 0 {
|
||||||
|
return &allocationOutcome{pending: requestQty}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
remaining = requestQty
|
||||||
|
applied float64
|
||||||
|
allocations []*entities.StockAllocation
|
||||||
|
allocationSummaries []AllocationDetail
|
||||||
|
usageAdjustments = make(map[fifo.StockableKey]map[uint]float64)
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, lot := range lots {
|
||||||
|
if remaining <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if lot.AvailableQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
portion := lot.AvailableQty
|
||||||
|
if portion > remaining {
|
||||||
|
portion = remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
applied += portion
|
||||||
|
remaining -= portion
|
||||||
|
|
||||||
|
allocationSummaries = append(allocationSummaries, AllocationDetail{
|
||||||
|
StockableKey: lot.StockableKey,
|
||||||
|
StockableID: lot.RecordID,
|
||||||
|
Quantity: portion,
|
||||||
|
})
|
||||||
|
|
||||||
|
allocations = append(allocations, &entities.StockAllocation{
|
||||||
|
ProductWarehouseId: productWarehouseID,
|
||||||
|
StockableType: lot.StockableKey.String(),
|
||||||
|
StockableId: lot.RecordID,
|
||||||
|
UsableType: usableKey.String(),
|
||||||
|
UsableId: usableID,
|
||||||
|
Qty: portion,
|
||||||
|
Status: entities.StockAllocationStatusActive,
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, ok := usageAdjustments[lot.StockableKey]; !ok {
|
||||||
|
usageAdjustments[lot.StockableKey] = make(map[uint]float64)
|
||||||
|
}
|
||||||
|
usageAdjustments[lot.StockableKey][lot.RecordID] += portion
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allocations) > 0 {
|
||||||
|
if err := s.allocations.CreateMany(ctx, allocations, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, deltas := range usageAdjustments {
|
||||||
|
cfg, ok := fifo.Stockable(key)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for id, qty := range deltas {
|
||||||
|
if err := s.incrementStockableUsage(ctx, tx, cfg, id, qty); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &allocationOutcome{
|
||||||
|
allocated: applied,
|
||||||
|
pending: remaining,
|
||||||
|
allocations: allocationSummaries,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) {
|
||||||
|
configs := fifo.Stockables()
|
||||||
|
if len(configs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var lots []stockLot
|
||||||
|
for key, cfg := range configs {
|
||||||
|
selectStmt := fmt.Sprintf(
|
||||||
|
"%s AS id, %s AS available_qty, %s AS created_at",
|
||||||
|
cfg.Columns.ID,
|
||||||
|
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||||
|
cfg.Columns.CreatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows []struct {
|
||||||
|
ID uint
|
||||||
|
AvailableQty float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Select(selectStmt).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
|
||||||
|
Where(fmt.Sprintf("%s > %s", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity))
|
||||||
|
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, order := range s.orderClauses(cfg.OrderBy) {
|
||||||
|
query = query.Order(order)
|
||||||
|
}
|
||||||
|
query = query.Limit(s.maxLotsPerStockable)
|
||||||
|
|
||||||
|
if err := query.Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.AvailableQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lots = append(lots, stockLot{
|
||||||
|
StockableKey: key,
|
||||||
|
RecordID: row.ID,
|
||||||
|
AvailableQty: row.AvailableQty,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lots) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(lots, func(i, j int) bool {
|
||||||
|
if lots[i].CreatedAt.Equal(lots[j].CreatedAt) {
|
||||||
|
return lots[i].RecordID < lots[j].RecordID
|
||||||
|
}
|
||||||
|
return lots[i].CreatedAt.Before(lots[j].CreatedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
return lots, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) applyUsableDeltas(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint, usageDelta, pendingDelta float64) error {
|
||||||
|
if usageDelta == 0 && pendingDelta == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]any{}
|
||||||
|
if usageDelta != 0 {
|
||||||
|
updates[cfg.Columns.UsageQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.UsageQuantity), usageDelta)
|
||||||
|
}
|
||||||
|
if pendingDelta != 0 {
|
||||||
|
updates[cfg.Columns.PendingQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.PendingQuantity), pendingDelta)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type pendingCandidate struct {
|
||||||
|
UsableKey fifo.UsableKey
|
||||||
|
Config fifo.UsableConfig
|
||||||
|
UsableID uint
|
||||||
|
Pending float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]PendingResolution, error) {
|
||||||
|
candidates, err := s.fetchPendingCandidates(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolutions []PendingResolution
|
||||||
|
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if candidate.Pending <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if outcome.allocated <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.applyUsableDeltas(ctx, tx, candidate.Config, candidate.UsableID, outcome.allocated, -outcome.allocated); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
||||||
|
productWarehouseID: -outcome.allocated,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutions = append(resolutions, PendingResolution{
|
||||||
|
UsableKey: candidate.UsableKey,
|
||||||
|
UsableID: candidate.UsableID,
|
||||||
|
Quantity: outcome.allocated,
|
||||||
|
})
|
||||||
|
|
||||||
|
if outcome.pending > 0 {
|
||||||
|
// No more stock available for this warehouse at the moment.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolutions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) releaseUsagePortion(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
usableKey fifo.UsableKey,
|
||||||
|
usableID uint,
|
||||||
|
target float64,
|
||||||
|
) (float64, error) {
|
||||||
|
if target <= 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
allocations, err := s.allocations.FindActiveByUsable(ctx, usableKey.String(), usableID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
target := s.txOrDB(tx, db)
|
||||||
|
return target.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(allocations) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
remaining = target
|
||||||
|
totalReleased float64
|
||||||
|
warehouseAdjustments = make(map[uint]float64)
|
||||||
|
stockableAdjustments = make(map[fifo.StockableKey]map[uint]float64)
|
||||||
|
)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for i := len(allocations) - 1; i >= 0 && remaining > 0; i-- {
|
||||||
|
allocation := allocations[i]
|
||||||
|
releaseAmt := allocation.Qty
|
||||||
|
if releaseAmt > remaining {
|
||||||
|
releaseAmt = remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining -= releaseAmt
|
||||||
|
totalReleased += releaseAmt
|
||||||
|
warehouseAdjustments[allocation.ProductWarehouseId] += releaseAmt
|
||||||
|
|
||||||
|
key := fifo.StockableKey(allocation.StockableType)
|
||||||
|
if _, ok := stockableAdjustments[key]; !ok {
|
||||||
|
stockableAdjustments[key] = make(map[uint]float64)
|
||||||
|
}
|
||||||
|
stockableAdjustments[key][allocation.StockableId] += releaseAmt
|
||||||
|
|
||||||
|
if releaseAmt == allocation.Qty {
|
||||||
|
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
|
||||||
|
"status": entities.StockAllocationStatusReleased,
|
||||||
|
"released_at": now,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
|
||||||
|
"quantity": allocation.Qty - releaseAmt,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalReleased == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, deltas := range stockableAdjustments {
|
||||||
|
cfg, ok := fifo.Stockable(key)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for id, qty := range deltas {
|
||||||
|
if err := s.incrementStockableUsage(ctx, tx, cfg, id, -qty); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(warehouseAdjustments) > 0 {
|
||||||
|
if err := s.productWarehouseRepo.AdjustQuantities(ctx, warehouseAdjustments, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.txOrDB(tx, db)
|
||||||
|
}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for warehouseID := range warehouseAdjustments {
|
||||||
|
if _, err := s.resolvePendingForWarehouse(ctx, tx, warehouseID); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalReleased, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]pendingCandidate, error) {
|
||||||
|
configs := fifo.Usables()
|
||||||
|
if len(configs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates []pendingCandidate
|
||||||
|
|
||||||
|
for key, cfg := range configs {
|
||||||
|
selectStmt := fmt.Sprintf(
|
||||||
|
"%s AS id, %s AS pending_qty, %s AS created_at",
|
||||||
|
cfg.Columns.ID,
|
||||||
|
cfg.Columns.PendingQuantity,
|
||||||
|
cfg.Columns.CreatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows []struct {
|
||||||
|
ID uint
|
||||||
|
Pending float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
query := tx.Table(cfg.Table).
|
||||||
|
Select(selectStmt).
|
||||||
|
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
|
||||||
|
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
|
||||||
|
Limit(s.pendingBatchPerUsable)
|
||||||
|
|
||||||
|
if cfg.Scope != nil {
|
||||||
|
query = cfg.Scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, order := range s.orderClauses(cfg.OrderBy) {
|
||||||
|
query = query.Order(order)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.Pending <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidates = append(candidates, pendingCandidate{
|
||||||
|
UsableKey: key,
|
||||||
|
Config: cfg,
|
||||||
|
UsableID: row.ID,
|
||||||
|
Pending: row.Pending,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(candidates, func(i, j int) bool {
|
||||||
|
if candidates[i].CreatedAt.Equal(candidates[j].CreatedAt) {
|
||||||
|
return candidates[i].UsableID < candidates[j].UsableID
|
||||||
|
}
|
||||||
|
return candidates[i].CreatedAt.Before(candidates[j].CreatedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
return candidates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fifoService) orderClauses(custom []string) []string {
|
||||||
|
if len(custom) > 0 {
|
||||||
|
return custom
|
||||||
|
}
|
||||||
|
return s.defaultOrderBy
|
||||||
|
}
|
||||||
@@ -65,6 +65,14 @@ var (
|
|||||||
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() {
|
||||||
@@ -106,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")
|
||||||
|
|||||||
@@ -2,42 +2,42 @@
|
|||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
id_user BIGINT NOT NULL,
|
id_user BIGINT NOT NULL,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
email VARCHAR NOT NULL,
|
email VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
|
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL;
|
CREATE UNIQUE INDEX users_email_unique ON users (email)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- FLAGS
|
-- FLAGS
|
||||||
CREATE TABLE flags (
|
CREATE TABLE flags (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
flagable_id BIGINT NOT NULL,
|
flagable_id BIGINT NOT NULL,
|
||||||
flagable_type VARCHAR(50) NOT NULL,
|
flagable_type VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
updated_at TIMESTAMPTZ DEFAULT NOW ()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX flags_unique_flagable ON flags (
|
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
|
||||||
name,
|
|
||||||
flagable_id,
|
|
||||||
flagable_type
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
||||||
|
|
||||||
-- PRODUCT CATEGORIES
|
-- PRODUCT CATEGORIES
|
||||||
CREATE TABLE product_categories (
|
CREATE TABLE product_categories (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
code VARCHAR(10) NOT NULL,
|
code VARCHAR(10) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -53,9 +53,9 @@ WHERE
|
|||||||
-- UOM
|
-- UOM
|
||||||
CREATE TABLE uoms (
|
CREATE TABLE uoms (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -67,12 +67,12 @@ WHERE
|
|||||||
-- BANKS
|
-- BANKS
|
||||||
CREATE TABLE banks (
|
CREATE TABLE banks (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
alias VARCHAR(5) NOT NULL,
|
alias VARCHAR(5) NOT NULL,
|
||||||
owner VARCHAR,
|
owner VARCHAR(50),
|
||||||
account_number VARCHAR(50) NOT NULL,
|
account_number VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -84,9 +84,9 @@ WHERE
|
|||||||
-- AREAS
|
-- AREAS
|
||||||
CREATE TABLE areas (
|
CREATE TABLE areas (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -98,11 +98,11 @@ WHERE
|
|||||||
-- LOCATIONS
|
-- LOCATIONS
|
||||||
CREATE TABLE locations (
|
CREATE TABLE locations (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -114,11 +114,11 @@ WHERE
|
|||||||
-- KANDANG
|
-- KANDANG
|
||||||
CREATE TABLE kandangs (
|
CREATE TABLE kandangs (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -130,13 +130,13 @@ WHERE
|
|||||||
-- WAREHOUSES
|
-- WAREHOUSES
|
||||||
CREATE TABLE warehouses (
|
CREATE TABLE warehouses (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
location_id BIGINT REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
location_id BIGINT REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -148,16 +148,16 @@ WHERE
|
|||||||
-- CUSTOMERS
|
-- CUSTOMERS
|
||||||
CREATE TABLE customers (
|
CREATE TABLE customers (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
phone VARCHAR(20) NOT NULL,
|
phone VARCHAR(20) NOT NULL,
|
||||||
email VARCHAR NOT NULL,
|
email VARCHAR(50) NOT NULL,
|
||||||
account_number VARCHAR(50) NOT NULL,
|
account_number VARCHAR(50) NOT NULL,
|
||||||
balance NUMERIC(15, 3) DEFAULT 0,
|
balance NUMERIC(15, 3) DEFAULT 0,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -169,10 +169,10 @@ WHERE
|
|||||||
-- NONSTOCK
|
-- NONSTOCK
|
||||||
CREATE TABLE nonstocks (
|
CREATE TABLE nonstocks (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -184,9 +184,9 @@ WHERE
|
|||||||
-- FCR
|
-- FCR
|
||||||
CREATE TABLE fcrs (
|
CREATE TABLE fcrs (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -201,29 +201,29 @@ CREATE TABLE fcr_standards (
|
|||||||
weight NUMERIC(15, 3) NOT NULL,
|
weight NUMERIC(15, 3) NOT NULL,
|
||||||
fcr_number NUMERIC(15, 3) NOT NULL,
|
fcr_number NUMERIC(15, 3) NOT NULL,
|
||||||
mortality NUMERIC(15, 3) NOT NULL,
|
mortality NUMERIC(15, 3) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
-- SUPPLIERS
|
-- SUPPLIERS
|
||||||
CREATE TABLE suppliers (
|
CREATE TABLE suppliers (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
alias VARCHAR(5) NOT NULL,
|
alias VARCHAR(5) NOT NULL,
|
||||||
pic VARCHAR NOT NULL,
|
pic VARCHAR(50) NOT NULL,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
category VARCHAR(20) NOT NULL,
|
category VARCHAR(20) NOT NULL,
|
||||||
hatchery VARCHAR,
|
hatchery VARCHAR(50),
|
||||||
phone VARCHAR(20) NOT NULL,
|
phone VARCHAR(20) NOT NULL,
|
||||||
email VARCHAR NOT NULL,
|
email VARCHAR(50) NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
npwp VARCHAR(50),
|
npwp VARCHAR(50),
|
||||||
account_number VARCHAR(50),
|
account_number VARCHAR(50),
|
||||||
balance NUMERIC(15, 3) DEFAULT 0,
|
balance NUMERIC(15, 3) DEFAULT 0,
|
||||||
due_date INT NOT NULL,
|
due_date INT NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -235,15 +235,15 @@ WHERE
|
|||||||
CREATE TABLE nonstock_suppliers (
|
CREATE TABLE nonstock_suppliers (
|
||||||
nonstock_id BIGINT NOT NULL REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
nonstock_id BIGINT NOT NULL REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
PRIMARY KEY (nonstock_id, supplier_id)
|
PRIMARY KEY (nonstock_id, supplier_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- PRODUCTS
|
-- PRODUCTS
|
||||||
CREATE TABLE products (
|
CREATE TABLE products (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
brand VARCHAR NOT NULL,
|
brand VARCHAR(50) NOT NULL,
|
||||||
sku VARCHAR(100),
|
sku VARCHAR(100),
|
||||||
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
@@ -251,8 +251,8 @@ CREATE TABLE products (
|
|||||||
selling_price NUMERIC(15, 3),
|
selling_price NUMERIC(15, 3),
|
||||||
tax NUMERIC(15, 3),
|
tax NUMERIC(15, 3),
|
||||||
expiry_period INT,
|
expiry_period INT,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -268,15 +268,15 @@ WHERE
|
|||||||
CREATE TABLE product_suppliers (
|
CREATE TABLE product_suppliers (
|
||||||
product_id BIGINT NOT NULL REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
product_id BIGINT NOT NULL REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
PRIMARY KEY (product_id, supplier_id)
|
PRIMARY KEY (product_id, supplier_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- PROJECTS
|
-- PROJECTS
|
||||||
CREATE TABLE projects (
|
CREATE TABLE projects (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
@@ -288,8 +288,8 @@ CREATE TABLE product_warehouses (
|
|||||||
warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
|
warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
|
||||||
quantity INTEGER NOT NULL DEFAULT 0,
|
quantity INTEGER NOT NULL DEFAULT 0,
|
||||||
created_by BIGINT NOT NULL REFERENCES users (id),
|
created_by BIGINT NOT NULL REFERENCES users (id),
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -316,8 +316,8 @@ CREATE TABLE stock_logs (
|
|||||||
note TEXT,
|
note TEXT,
|
||||||
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW (),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
CREATE TABLE expenses (
|
CREATE TABLE expenses (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
reference_number VARCHAR(50) UNIQUE NOT NULL,
|
reference_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
supplier_id BIGINT NULL,
|
supplier_id BIGINT NOT NULL,
|
||||||
category VARCHAR(50) NOT NULL CHECK (
|
category VARCHAR(50) NOT NULL CHECK (
|
||||||
category IN ('BOP', 'NON-BOP')
|
category IN ('BOP', 'NON-BOP')
|
||||||
),
|
),
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
-- ============================
|
||||||
|
-- EXPENSES
|
||||||
|
-- ============================
|
||||||
|
ALTER TABLE expenses DROP COLUMN IF EXISTS grand_total;
|
||||||
|
|
||||||
|
ALTER TABLE expenses RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
ALTER TABLE expenses RENAME COLUMN expense_date TO transaction_date;
|
||||||
|
|
||||||
|
-- ============================
|
||||||
|
-- EXPENSE_REALIZATIONS
|
||||||
|
-- ============================
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
RENAME COLUMN realization_qty TO qty;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
RENAME COLUMN realization_unit_price TO price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
DROP COLUMN IF EXISTS realization_total_price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
DROP COLUMN IF EXISTS realization_date;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations DROP COLUMN IF EXISTS created_by;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
|
|
||||||
|
-- ============================
|
||||||
|
-- EXPENSE_NONSTOCKS
|
||||||
|
-- ============================
|
||||||
|
ALTER TABLE expense_nonstocks RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS total_price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks RENAME COLUMN unit_price TO price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS created_by;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP Table IF EXISTS project_budgets;
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
CREATE TABLE project_budgets (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_id BIGINT NOT NULL,
|
||||||
|
nonstock_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
price NUMERIC(15, 3) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke project_flocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
ADD CONSTRAINT fk_project_budgets_project_flock_id
|
||||||
|
FOREIGN KEY (project_flock_id) REFERENCES project_flocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
-- Tambahkan Foreign Key ke nonstocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
ADD CONSTRAINT fk_project_budgets_nonstock_id
|
||||||
|
FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
-- Index
|
||||||
|
CREATE INDEX idx_project_budgets_project_flock_id ON project_budgets (project_flock_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_project_budgets_nonstock_id ON project_budgets (nonstock_id);
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandangs_closed_at;
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
DROP COLUMN IF EXISTS closed_at;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_flock_kandangs_closed_at
|
||||||
|
ON project_flock_kandangs (closed_at);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE products
|
||||||
|
DROP COLUMN IF EXISTS is_visible;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE products
|
||||||
|
ADD COLUMN IF NOT EXISTS is_visible BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX IF EXISTS documents_documentable_polymorphic;
|
||||||
|
DROP TABLE IF EXISTS documents;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE documents (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
documentable_type VARCHAR(50) NOT NULL,
|
||||||
|
documentable_id BIGINT NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
path VARCHAR(50) NOT NULL,
|
||||||
|
name VARCHAR(50) NOT NULL,
|
||||||
|
ext VARCHAR(50) NOT NULL,
|
||||||
|
size NUMERIC(15, 3) NOT NULL,
|
||||||
|
created_by BIGINT REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX documents_documentable_polymorphic ON documents (documentable_type, documentable_id);
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop new indexes and FK
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_project_flock_kandang_id;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_unique;
|
||||||
|
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_product_warehouses_project_flock_kandang_id,
|
||||||
|
ALTER COLUMN project_flock_kandang_id DROP NOT NULL,
|
||||||
|
DROP COLUMN IF EXISTS project_flock_kandang_id;
|
||||||
|
|
||||||
|
-- Revert qty to integer quantity
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
RENAME COLUMN qty TO quantity;
|
||||||
|
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ALTER COLUMN quantity TYPE INTEGER USING quantity::integer,
|
||||||
|
ALTER COLUMN quantity SET DEFAULT 0,
|
||||||
|
ALTER COLUMN quantity SET NOT NULL;
|
||||||
|
|
||||||
|
-- Restore audit/soft-delete columns
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id),
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- Recreate prior indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_product_warehouses_deleted_at ON product_warehouses (deleted_at);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_product_warehouses_unique
|
||||||
|
ON product_warehouses (product_id, warehouse_id)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop indexes that depend on deleted_at or old uniqueness
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_deleted_at;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_unique;
|
||||||
|
|
||||||
|
-- Add new relation and adjust quantity column
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
|
||||||
|
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
RENAME COLUMN quantity TO qty;
|
||||||
|
|
||||||
|
-- Enforce numeric quantity with precision and default
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ALTER COLUMN qty TYPE NUMERIC(15, 3) USING qty::numeric(15, 3),
|
||||||
|
ALTER COLUMN qty SET DEFAULT 0,
|
||||||
|
ALTER COLUMN qty SET NOT NULL;
|
||||||
|
|
||||||
|
-- Remove audit/soft-delete columns no longer used
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
DROP COLUMN IF EXISTS created_by,
|
||||||
|
DROP COLUMN IF EXISTS created_at,
|
||||||
|
DROP COLUMN IF EXISTS updated_at,
|
||||||
|
DROP COLUMN IF EXISTS deleted_at;
|
||||||
|
|
||||||
|
-- Enforce FK and not-null for project_flock_kandang_id
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ADD CONSTRAINT fk_product_warehouses_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs (id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- New indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_product_warehouses_project_flock_kandang_id
|
||||||
|
ON product_warehouses (project_flock_kandang_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_product_warehouses_unique
|
||||||
|
ON product_warehouses (product_id, warehouse_id, project_flock_kandang_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop new indexes
|
||||||
|
DROP INDEX IF EXISTS stock_logs_loggable_type_loggable_id_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_product_warehouse_id_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_created_by_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_created_at_idx;
|
||||||
|
|
||||||
|
-- Restore obsolete columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
ADD COLUMN IF NOT EXISTS transaction_type VARCHAR(20) DEFAULT '' NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS before_quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS after_quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Rename columns back
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN loggable_type TO log_type;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN loggable_id TO log_id;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN notes TO note;
|
||||||
|
|
||||||
|
-- Drop new columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
DROP COLUMN IF EXISTS increase,
|
||||||
|
DROP COLUMN IF EXISTS decrease;
|
||||||
|
|
||||||
|
-- Restore indexes for old structure
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_log_type_log_id_idx ON stock_logs (log_type, log_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_by_idx ON stock_logs (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_at_idx ON stock_logs (created_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_deleted_at_idx ON stock_logs (deleted_at);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop old indexes tied to removed columns
|
||||||
|
DROP INDEX IF EXISTS stock_logs_log_type_log_id_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_deleted_at_idx;
|
||||||
|
|
||||||
|
-- Rename columns to new naming
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN log_type TO loggable_type;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN log_id TO loggable_id;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
-- Add new increase/decrease columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
ADD COLUMN IF NOT EXISTS increase NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS decrease NUMERIC(15, 3) DEFAULT 0;
|
||||||
|
|
||||||
|
-- Adjust column definitions
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
ALTER COLUMN loggable_type TYPE VARCHAR(50),
|
||||||
|
ALTER COLUMN loggable_type SET NOT NULL,
|
||||||
|
ALTER COLUMN loggable_id SET NOT NULL,
|
||||||
|
ALTER COLUMN increase SET DEFAULT 0,
|
||||||
|
ALTER COLUMN increase SET NOT NULL,
|
||||||
|
ALTER COLUMN decrease SET DEFAULT 0,
|
||||||
|
ALTER COLUMN decrease SET NOT NULL;
|
||||||
|
|
||||||
|
-- Remove obsolete columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
DROP COLUMN IF EXISTS transaction_type,
|
||||||
|
DROP COLUMN IF EXISTS quantity,
|
||||||
|
DROP COLUMN IF EXISTS before_quantity,
|
||||||
|
DROP COLUMN IF EXISTS after_quantity,
|
||||||
|
DROP COLUMN IF EXISTS updated_at,
|
||||||
|
DROP COLUMN IF EXISTS deleted_at;
|
||||||
|
|
||||||
|
-- Recreate indexes for new structure
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_loggable_type_loggable_id_idx ON stock_logs (loggable_type, loggable_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_by_idx ON stock_logs (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_at_idx ON stock_logs (created_at);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Remove grading details from recording_eggs
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP COLUMN IF EXISTS weight;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0);
|
||||||
|
|
||||||
|
-- Restore grading_eggs table for rollback scenarios
|
||||||
|
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;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Remove separate grading table and move grading details into recording_eggs
|
||||||
|
DROP INDEX IF EXISTS idx_grading_eggs_recording_egg;
|
||||||
|
DROP TABLE IF EXISTS grading_eggs;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD COLUMN IF NOT EXISTS weight NUMERIC(10,3);
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
|
||||||
|
qty >= 0 AND (weight IS NULL OR weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_expense_nonstock;
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_project_flock_kandang;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_purchase_items_expense_nonstock_id;
|
||||||
|
DROP INDEX IF EXISTS idx_purchase_items_project_flock_kandang_id;
|
||||||
|
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP COLUMN IF EXISTS expense_nonstock_id,
|
||||||
|
DROP COLUMN IF EXISTS project_flock_kandang_id,
|
||||||
|
ALTER COLUMN vehicle_number DROP NOT NULL,
|
||||||
|
ALTER COLUMN vehicle_number TYPE VARCHAR USING vehicle_number;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN pr_number TYPE VARCHAR USING pr_number,
|
||||||
|
ALTER COLUMN po_number TYPE VARCHAR USING po_number,
|
||||||
|
ALTER COLUMN created_at DROP DEFAULT,
|
||||||
|
ALTER COLUMN updated_at DROP DEFAULT;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ADD COLUMN credit_term INT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN grand_total NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN credit_term DROP DEFAULT,
|
||||||
|
ALTER COLUMN grand_total DROP DEFAULT;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- Adjust purchases table to new purchasing schema
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN pr_number TYPE VARCHAR(50) USING LEFT(pr_number, 50),
|
||||||
|
ALTER COLUMN po_number TYPE VARCHAR(50) USING LEFT(po_number, 50),
|
||||||
|
ALTER COLUMN created_at SET DEFAULT now(),
|
||||||
|
ALTER COLUMN updated_at SET DEFAULT now();
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
DROP COLUMN IF EXISTS credit_term,
|
||||||
|
DROP COLUMN IF EXISTS grand_total;
|
||||||
|
|
||||||
|
-- Bring purchase_items in line with new requirements
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
ADD COLUMN IF NOT EXISTS expense_nonstock_id BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
|
||||||
|
|
||||||
|
UPDATE purchase_items
|
||||||
|
SET vehicle_number = ''
|
||||||
|
WHERE vehicle_number IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
ALTER COLUMN vehicle_number TYPE VARCHAR(10) USING LEFT(vehicle_number, 10),
|
||||||
|
ALTER COLUMN vehicle_number SET NOT NULL;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_expense_nonstock
|
||||||
|
FOREIGN KEY (expense_nonstock_id)
|
||||||
|
REFERENCES expense_nonstocks(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_project_flock_kandang
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_expense_nonstock_id
|
||||||
|
ON purchase_items (expense_nonstock_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_project_flock_kandang_id
|
||||||
|
ON purchase_items (project_flock_kandang_id);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Drop function and sequence for sales order numbers
|
||||||
|
DROP FUNCTION IF EXISTS generate_so_number();
|
||||||
|
DROP SEQUENCE IF EXISTS so_number_seq;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Create sequence for sales order numbers
|
||||||
|
CREATE SEQUENCE so_number_seq START WITH 1 INCREMENT BY 1;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION generate_so_number()
|
||||||
|
RETURNS VARCHAR AS $$
|
||||||
|
DECLARE
|
||||||
|
next_val INTEGER;
|
||||||
|
BEGIN
|
||||||
|
next_val := nextval('so_number_seq');
|
||||||
|
RETURN 'SO-' || LPAD(next_val::TEXT, 5, '0');
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE purchases
|
||||||
|
DROP COLUMN IF EXISTS credit_term;
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE purchases
|
||||||
|
ADD COLUMN IF NOT EXISTS credit_term INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN credit_term DROP DEFAULT;
|
||||||
@@ -910,7 +910,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
|||||||
ProductId: product.Id,
|
ProductId: product.Id,
|
||||||
WarehouseId: warehouse.Id,
|
WarehouseId: warehouse.Id,
|
||||||
Quantity: seed.Quantity,
|
Quantity: seed.Quantity,
|
||||||
CreatedBy: createdBy,
|
// CreatedBy: createdBy,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&productWarehouse).Error; err != nil {
|
if err := tx.Create(&productWarehouse).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Area struct {
|
type Area struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
|
|
||||||
type Bank struct {
|
type Bank struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
|
||||||
Alias string `gorm:"not null;size:5"`
|
Alias string `gorm:"not null;size:5"`
|
||||||
Owner *string `gorm:""`
|
Owner *string `gorm:"type:varchar(50)"`
|
||||||
AccountNumber string `gorm:"not null;size:50"`
|
AccountNumber string `gorm:"not null;size:50"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
|
|
||||||
type Customer struct {
|
type Customer struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
|
||||||
PicId uint `gorm:"not null"`
|
PicId uint `gorm:"not null"`
|
||||||
Type string `gorm:"not null;size:50"`
|
Type string `gorm:"not null;size:50"`
|
||||||
Address string `gorm:"not null"`
|
Address string `gorm:"not null"`
|
||||||
Phone string `gorm:"not null;size:20"`
|
Phone string `gorm:"not null;size:20"`
|
||||||
Email string `gorm:"not null"`
|
Email string `gorm:"type:varchar(50);not null"`
|
||||||
AccountNumber string `gorm:"not null;size:50"`
|
AccountNumber string `gorm:"not null;size:50"`
|
||||||
Balance float64 `gorm:"default:0"`
|
Balance float64 `gorm:"default:0"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
DocumentableType string `gorm:"size:50;not null;index:documents_documentable_polymorphic,priority:1"`
|
||||||
|
DocumentableId uint64 `gorm:"not null;index:documents_documentable_polymorphic,priority:2"`
|
||||||
|
Type string `gorm:"size:50;not null"`
|
||||||
|
Path string `gorm:"size:50;not null"`
|
||||||
|
Name string `gorm:"size:50;not null"`
|
||||||
|
Ext string `gorm:"size:50;not null"`
|
||||||
|
Size float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
CreatedBy *uint `gorm:"index"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
}
|
||||||
@@ -13,18 +13,16 @@ type Expense struct {
|
|||||||
SupplierId uint64 `gorm:""`
|
SupplierId uint64 `gorm:""`
|
||||||
Category string `gorm:"type:varchar(50);not null"`
|
Category string `gorm:"type:varchar(50);not null"`
|
||||||
PoNumber string `gorm:"type:varchar(50)"`
|
PoNumber string `gorm:"type:varchar(50)"`
|
||||||
DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan
|
DocumentPath sql.NullString `gorm:"type:json"`
|
||||||
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi
|
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
|
||||||
RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi
|
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
|
||||||
ExpenseDate time.Time `gorm:"type:date;not null"`
|
TransactionDate time.Time `gorm:"type:date;not null"`
|
||||||
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
Notes string `gorm:"type:text;column:notes"`
|
||||||
Note string `gorm:"type:text"`
|
|
||||||
CreatedBy uint64 `gorm:""`
|
CreatedBy uint64 `gorm:""`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
// Relations
|
|
||||||
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type ExpenseNonstock struct {
|
type ExpenseNonstock struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
ExpenseId *uint64 `gorm:""`
|
ExpenseId *uint64 `gorm:""`
|
||||||
@@ -7,14 +11,13 @@ type ExpenseNonstock struct {
|
|||||||
KandangId *uint64 `gorm:""`
|
KandangId *uint64 `gorm:""`
|
||||||
NonstockId *uint64 `gorm:""`
|
NonstockId *uint64 `gorm:""`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
|
Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
|
||||||
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
|
Notes string `gorm:"type:text;column:notes"`
|
||||||
Note string `gorm:"type:text"`
|
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
||||||
|
|
||||||
// Relations
|
|
||||||
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
|
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
||||||
Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
Realization *ExpenseRealization `gorm:"foreignKey:Id;references:ExpenseNonstockId"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,10 @@ import (
|
|||||||
type ExpenseRealization struct {
|
type ExpenseRealization struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
ExpenseNonstockId *uint64 `gorm:""`
|
ExpenseNonstockId *uint64 `gorm:""`
|
||||||
RealizationQty float64 `gorm:"type:numeric(15,3);not null"`
|
Qty float64 `gorm:"type:numeric(15,3);not null;"`
|
||||||
RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"`
|
Price float64 `gorm:"type:numeric(15,3);not null;"`
|
||||||
RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"`
|
Notes string `gorm:"type:text;"`
|
||||||
RealizationDate time.Time `gorm:"type:date;not null"`
|
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
||||||
Note *string `gorm:"type:text"`
|
|
||||||
CreatedBy *uint64 `gorm:""`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Fcr struct {
|
type Fcr struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const (
|
|||||||
|
|
||||||
type Flag struct {
|
type Flag struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:flags_unique_flagable"`
|
Name string `gorm:"type:varchar(50);size:50;not null;uniqueIndex:flags_unique_flagable"`
|
||||||
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"`
|
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"`
|
||||||
FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
|
FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Kandang struct {
|
type Kandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
||||||
Status string `gorm:"type:varchar(50);not null"`
|
Status string `gorm:"type:varchar(50);not null"`
|
||||||
LocationId uint `gorm:"not null"`
|
LocationId uint `gorm:"not null"`
|
||||||
Capacity float64 `gorm:"not null"`
|
Capacity float64 `gorm:"not null"`
|
||||||
@@ -20,5 +20,6 @@ type Kandang struct {
|
|||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||||
|
Warehouses []Warehouse `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Location struct {
|
type Location struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
|
||||||
Address string `gorm:"not null"`
|
Address string `gorm:"not null"`
|
||||||
AreaId uint `gorm:"not null"`
|
AreaId uint `gorm:"not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Nonstock struct {
|
type Nonstock struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
|
||||||
UomId uint `gorm:"not null"`
|
UomId uint `gorm:"not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type ProductCategory struct {
|
type ProductCategory struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
|
||||||
Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"`
|
Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
type Product struct {
|
type Product struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
|
||||||
Brand string `gorm:"not null"`
|
Brand string `gorm:"type:varchar(50);not null"`
|
||||||
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
|
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
|
||||||
UomId uint `gorm:"not null"`
|
UomId uint `gorm:"not null"`
|
||||||
ProductCategoryId uint `gorm:"not null"`
|
ProductCategoryId uint `gorm:"not null"`
|
||||||
@@ -21,10 +21,12 @@ type Product struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
IsVisible bool `gorm:"column:is_visible;default:true"`
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
||||||
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
||||||
ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
|
ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
|
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
|
||||||
|
ProductWarehouses []ProductWarehouse `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProductWarehouse struct {
|
type ProductWarehouse struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
ProductId uint `gorm:"not null"`
|
ProductId uint `gorm:"column:product_id;not null"`
|
||||||
WarehouseId uint `gorm:"not null"`
|
WarehouseId uint `gorm:"column:warehouse_id;not null"`
|
||||||
Quantity float64 `gorm:"default:0"`
|
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Quantity float64 `gorm:"column:qty;type:numeric(15,3);default:0"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
StockLogs []StockLog `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectBudget struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
ProjectFlockId uint `gorm:"not null"`
|
||||||
|
NonstockId uint `gorm:"not null"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
|
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
||||||
|
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ type ProjectFlock struct {
|
|||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
||||||
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||||
|
Budgets []ProjectBudget `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||||
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type ProjectFlockKandang struct {
|
|||||||
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
Period int `gorm:"not null"`
|
Period int `gorm:"not null"`
|
||||||
|
ClosedAt *time.Time `gorm:"index"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
|
|||||||
@@ -5,19 +5,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Purchase struct {
|
type Purchase struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
PrNumber string `gorm:"not null"`
|
PrNumber string `gorm:"not null"`
|
||||||
PoNumber *string
|
PoNumber *string
|
||||||
PoDate *time.Time
|
PoDate *time.Time
|
||||||
SupplierId uint64 `gorm:"not null"`
|
SupplierId uint `gorm:"not null"`
|
||||||
CreditTerm *int
|
CreditTerm int `gorm:"column:credit_term;not null;default:0"`
|
||||||
DueDate *time.Time
|
DueDate *time.Time
|
||||||
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
|
||||||
Notes *string
|
Notes *string
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
CreatedBy uint64 `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseItem struct {
|
type PurchaseItem struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
PurchaseId uint64 `gorm:"not null"`
|
PurchaseId uint `gorm:"not null"`
|
||||||
ProductId uint64 `gorm:"not null"`
|
ProductId uint `gorm:"not null"`
|
||||||
WarehouseId uint64 `gorm:"not null"`
|
WarehouseId uint `gorm:"not null"`
|
||||||
ProductWarehouseId *uint64
|
ProductWarehouseId *uint
|
||||||
|
ProjectFlockKandangId *uint
|
||||||
ReceivedDate *time.Time
|
ReceivedDate *time.Time
|
||||||
TravelNumber *string
|
TravelNumber *string
|
||||||
TravelNumberDocs *string
|
TravelNumberDocs *string
|
||||||
@@ -19,8 +20,10 @@ type PurchaseItem struct {
|
|||||||
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
ExpenseNonstockId *uint64
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
|
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||||
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
|
|||||||
@@ -7,24 +7,11 @@ type RecordingEgg struct {
|
|||||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
Qty int `gorm:"column:qty;not null"`
|
Qty int `gorm:"column:qty;not null"`
|
||||||
|
Weight *float64 `gorm:"column:weight"`
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GradingEgg struct {
|
|
||||||
Id uint `gorm:"primaryKey"`
|
|
||||||
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
|
|
||||||
Qty float64 `gorm:"column:qty;not null"`
|
|
||||||
Grade string `gorm:"column:grade;type:varchar(50)"`
|
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
|
|
||||||
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StockAllocationStatusPending = "PENDING"
|
||||||
|
StockAllocationStatusActive = "ACTIVE"
|
||||||
|
StockAllocationStatusReleased = "RELEASED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StockAllocation links a usable record (consumption) with an incoming stock record.
|
||||||
|
// The combination lets us trace FIFO deductions while keeping each module focused on its own fields.
|
||||||
|
type StockAllocation struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
ProductWarehouseId uint `gorm:"not null;index"`
|
||||||
|
StockableType string `gorm:"size:100;not null;index:stock_allocations_lookup,priority:1"`
|
||||||
|
StockableId uint `gorm:"not null;index:stock_allocations_lookup,priority:2"`
|
||||||
|
UsableType string `gorm:"size:100;not null;index:stock_allocations_usage_lookup,priority:1"`
|
||||||
|
UsableId uint `gorm:"not null;index:stock_allocations_usage_lookup,priority:2"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Status string `gorm:"size:20;not null;default:ACTIVE"`
|
||||||
|
Note *string `gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
ReleasedAt *time.Time `gorm:"index"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
import "time"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LogTypeAdjustment = "ADJUSTMENT"
|
LogTypeAdjustment = "ADJUSTMENT"
|
||||||
@@ -18,18 +14,17 @@ const (
|
|||||||
|
|
||||||
type StockLog struct {
|
type StockLog struct {
|
||||||
Id uint `gorm:"primaryKey;column:id"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
TransactionType string `gorm:"type:varchar(20);not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
||||||
Quantity float64 `gorm:"type:numeric(15,3);not null"`
|
CreatedBy uint `gorm:"column:created_by;not null;index"`
|
||||||
BeforeQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
AfterQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
Increase float64 `gorm:"column:increase;type:numeric(15,3);default:0"`
|
||||||
LogType string `gorm:"type:varchar(50);not null;index:stock_logs_flaggable_lookup,priority:1"`
|
Decrease float64 `gorm:"column:decrease;type:numeric(15,3);default:0"`
|
||||||
LogId uint `gorm:"not null;index:stock_logs_flaggable_lookup,priority:2"`
|
|
||||||
Note string `gorm:"type:text"`
|
LoggableType string `gorm:"column:loggable_type;type:varchar(50);not null"`
|
||||||
ProductWarehouseId uint `gorm:"not null;index"`
|
LoggableId uint `gorm:"column:loggable_id;not null"`
|
||||||
CreatedBy uint `gorm:"index"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Notes string `gorm:"column:notes;type:text"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
|
||||||
|
|
||||||
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import (
|
|||||||
|
|
||||||
type Supplier struct {
|
type Supplier struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
|
||||||
Alias string `gorm:"not null;size:5"`
|
Alias string `gorm:"not null;size:5"`
|
||||||
Pic string `gorm:"not null"`
|
Pic string `gorm:"type:varchar(50);not null"`
|
||||||
Type string `gorm:"not null;size:50"`
|
Type string `gorm:"not null;size:50"`
|
||||||
Category string `gorm:"not null;size:20"`
|
Category string `gorm:"not null;size:20"`
|
||||||
Hatchery *string `gorm:"size:255"`
|
Hatchery *string `gorm:"type:varchar(50)"`
|
||||||
Phone string `gorm:"not null;size:20"`
|
Phone string `gorm:"not null;size:20"`
|
||||||
Email string `gorm:"not null"`
|
Email string `gorm:"type:varchar(50);not null"`
|
||||||
Address string `gorm:"not null"`
|
Address string `gorm:"not null"`
|
||||||
Npwp *string `gorm:"size:50"`
|
Npwp *string `gorm:"size:50"`
|
||||||
AccountNumber *string `gorm:"size:50"`
|
AccountNumber *string `gorm:"size:50"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Uom struct {
|
type Uom struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"type:varchar(50);not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
IdUser int64 `gorm:"uniqueIndex"`
|
IdUser int64 `gorm:"uniqueIndex"`
|
||||||
Email string `gorm:"uniqueIndex"`
|
Email string `gorm:"type:varchar(50);uniqueIndex"`
|
||||||
Name string `gorm:"not null"`
|
Name string `gorm:"type:varchar(50);not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Warehouse struct {
|
type Warehouse struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null"`
|
Name string `gorm:"type:varchar(50);not null"`
|
||||||
Type string `gorm:"not null"`
|
Type string `gorm:"not null"`
|
||||||
AreaId uint `gorm:"not null"`
|
AreaId uint `gorm:"not null"`
|
||||||
LocationId *uint
|
LocationId *uint
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -91,7 +90,6 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl
|
|||||||
|
|
||||||
c.Locals(authContextLocalsKey, ctx)
|
c.Locals(authContextLocalsKey, ctx)
|
||||||
c.Locals(authUserLocalsKey, user)
|
c.Locals(authUserLocalsKey, user)
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,6 +103,14 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||||
|
user, ok := AuthenticatedUser(c)
|
||||||
|
if !ok || user == nil || user.Id == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
return user.Id, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AuthDetails returns the full authentication context (token, claims, user).
|
// AuthDetails returns the full authentication context (token, claims, user).
|
||||||
func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) {
|
func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) {
|
||||||
value := c.Locals(authContextLocalsKey)
|
value := c.Locals(authContextLocalsKey)
|
||||||
@@ -191,3 +197,71 @@ func hasAllScopes(have, required []string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
||||||
|
func RequirePermissions(perms ...string) fiber.Handler {
|
||||||
|
required := canonicalPermissions(perms)
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
if len(required) == 0 {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
userPerms := ctx.permissionSet()
|
||||||
|
if len(userPerms) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, perm := range required {
|
||||||
|
if _, has := userPerms[perm]; !has {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPermission reports whether the current request context includes the given permission.
|
||||||
|
func HasPermission(c *fiber.Ctx, perm string) bool {
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
perm = canonicalPermission(perm)
|
||||||
|
if perm == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, has := ctx.permissionSet()[perm]
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthContext) permissionSet() map[string]struct{} {
|
||||||
|
if a == nil || a.Permissions == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.Permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermissions(perms []string) []string {
|
||||||
|
out := make([]string, 0, len(perms))
|
||||||
|
seen := make(map[string]struct{}, len(perms))
|
||||||
|
for _, perm := range perms {
|
||||||
|
if canonical := canonicalPermission(perm); canonical != "" {
|
||||||
|
if _, ok := seen[canonical]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[canonical] = struct{}{}
|
||||||
|
out = append(out, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermission(perm string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(perm))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,75 +1,14 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
//project-flock
|
||||||
"strings"
|
const (
|
||||||
|
PermissionProjectFlockClosing = "lti:project-flock:closing"
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
//recording
|
||||||
func RequirePermissions(perms ...string) fiber.Handler {
|
const (
|
||||||
required := canonicalPermissions(perms)
|
PermissionRecordingRead = "recording.index"
|
||||||
return func(c *fiber.Ctx) error {
|
PermissionRecordingCreate = "recording.create"
|
||||||
if len(required) == 0 {
|
PermissionRecordingUpdate = "recording.update"
|
||||||
return c.Next()
|
PermissionRecordingDelete = "recording.delete"
|
||||||
}
|
)
|
||||||
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
|
||||||
}
|
|
||||||
|
|
||||||
userPerms := ctx.permissionSet()
|
|
||||||
if len(userPerms) == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, perm := range required {
|
|
||||||
if _, has := userPerms[perm]; !has {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPermission reports whether the current request context includes the given permission.
|
|
||||||
func HasPermission(c *fiber.Ctx, perm string) bool {
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
perm = canonicalPermission(perm)
|
|
||||||
if perm == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, has := ctx.permissionSet()[perm]
|
|
||||||
return has
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AuthContext) permissionSet() map[string]struct{} {
|
|
||||||
if a == nil || a.Permissions == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return a.Permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermissions(perms []string) []string {
|
|
||||||
out := make([]string, 0, len(perms))
|
|
||||||
seen := make(map[string]struct{}, len(perms))
|
|
||||||
for _, perm := range perms {
|
|
||||||
if canonical := canonicalPermission(perm); canonical != "" {
|
|
||||||
if _, ok := seen[canonical]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[canonical] = struct{}{}
|
|
||||||
out = append(out, canonical)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermission(perm string) string {
|
|
||||||
return strings.ToLower(strings.TrimSpace(perm))
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package approvals
|
|||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
|
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
|
||||||
_ = u
|
_ = u
|
||||||
ctrl := controller.NewApprovalController(s)
|
ctrl := controller.NewApprovalController(s)
|
||||||
|
|
||||||
route := v1.Group("/approvals")
|
route := v1.Group("/approvals")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClosingController struct {
|
||||||
|
ClosingService service.ClosingService
|
||||||
|
SapronakService service.SapronakService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
||||||
|
return &ClosingController{
|
||||||
|
ClosingService: closingService,
|
||||||
|
SapronakService: sapronakService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ClosingService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ClosingListItemDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved closing projects list successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetProjectFlockByID(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved closing information successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("projectFlockId")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved project information successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("project_flock_id")
|
||||||
|
|
||||||
|
projectFlockID, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get closing penjualan successfully",
|
||||||
|
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("project_flock_id")
|
||||||
|
|
||||||
|
projectFlockID, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get overhead successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("projectFlockId")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &validation.ClosingSapronakQuery{
|
||||||
|
Type: strings.ToLower(c.Query("type")),
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ClosingService.GetClosingSapronak(c, uint(id), query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ClosingSapronakItemDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved closing report (sapronak) successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("project_flock_id")
|
||||||
|
flag := c.Query("flag", "")
|
||||||
|
|
||||||
|
projectID, err := strconv.Atoi(param)
|
||||||
|
if err != nil || projectID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.SapronakService.GetSapronakByProject(c, uint(projectID), flag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := dto.ToSapronakProjectAggregatedFromReports(result, flag)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get perhitungan sapronak per project successfully",
|
||||||
|
Data: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
|
||||||
|
projectParam := c.Params("project_flock_id")
|
||||||
|
kandangParam := c.Params("project_flock_kandang_id")
|
||||||
|
flag := c.Query("flag", "")
|
||||||
|
|
||||||
|
projectID, err := strconv.Atoi(projectParam)
|
||||||
|
if err != nil || projectID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
|
}
|
||||||
|
pfkID, err := strconv.Atoi(kandangParam)
|
||||||
|
if err != nil || pfkID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID), flag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := dto.ToSapronakProjectAggregatedFromReport(result, flag)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get perhitungan sapronak per kandang successfully",
|
||||||
|
Data: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type ClosingRelationDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingDetailDTO struct {
|
||||||
|
ClosingListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingListItemDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
LocationID uint `json:"location_id"`
|
||||||
|
LocationName string `json:"location_name"`
|
||||||
|
ProjectCategory string `json:"project_category"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
ClosingDate string `json:"closing_date"`
|
||||||
|
ShedLabel string `json:"shed_label"`
|
||||||
|
ShedCount int `json:"shed_count"`
|
||||||
|
SalesPaidAmount int64 `json:"sales_paid_amount"`
|
||||||
|
SalesRemainingAmount int64 `json:"sales_remaining_amount"`
|
||||||
|
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||||
|
ProjectStatus string `json:"project_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingSummaryDTO struct {
|
||||||
|
FlockID uint `json:"flock_id"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
// JenisProduk string `json:"jenis_produk"`
|
||||||
|
// LabelPopulasi string `json:"label_populasi"`
|
||||||
|
Population int `json:"population"`
|
||||||
|
PopulationFormatted string `json:"population_formatted"`
|
||||||
|
ProjectType string `json:"project_type"`
|
||||||
|
ActiveHouseCount int `json:"active_house_count"`
|
||||||
|
ActiveHouseLabel string `json:"active_house_label"`
|
||||||
|
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||||
|
// StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
||||||
|
StatusProject string `json:"project_status"`
|
||||||
|
StatusClosing string `json:"closing_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
||||||
|
history := project.KandangHistory
|
||||||
|
|
||||||
|
period := maxPeriod(history)
|
||||||
|
kandangCount := len(history)
|
||||||
|
population := sumPopulation(history)
|
||||||
|
populationInt := int(population)
|
||||||
|
|
||||||
|
return ClosingSummaryDTO{
|
||||||
|
FlockID: project.Id,
|
||||||
|
Period: period,
|
||||||
|
// JenisProduk: project.Category,
|
||||||
|
// LabelPopulasi: "",
|
||||||
|
Population: populationInt,
|
||||||
|
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||||
|
ProjectType: project.Category,
|
||||||
|
ActiveHouseCount: kandangCount,
|
||||||
|
ActiveHouseLabel: fmt.Sprintf("%d Kandang", kandangCount),
|
||||||
|
SalesPaymentStatus: "Tempo",
|
||||||
|
// StatusPembayaranMitra: "",
|
||||||
|
StatusProject: statusProject,
|
||||||
|
StatusClosing: statusClosing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToClosingListItemDTO(project entity.ProjectFlock, projectStatus string) ClosingListItemDTO {
|
||||||
|
shedCount := len(project.KandangHistory)
|
||||||
|
|
||||||
|
return ClosingListItemDTO{
|
||||||
|
Id: project.Id,
|
||||||
|
LocationID: project.LocationId,
|
||||||
|
LocationName: project.Location.Name,
|
||||||
|
ProjectCategory: project.Category,
|
||||||
|
Period: maxPeriod(project.KandangHistory),
|
||||||
|
ClosingDate: "17-Nov-2025",
|
||||||
|
ShedLabel: fmt.Sprintf("%d Kandang", shedCount),
|
||||||
|
ShedCount: shedCount,
|
||||||
|
SalesPaidAmount: 21993726,
|
||||||
|
SalesRemainingAmount: 11075919,
|
||||||
|
SalesPaymentStatus: "Lunas",
|
||||||
|
ProjectStatus: projectStatus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxPeriod(history []entity.ProjectFlockKandang) int {
|
||||||
|
max := 0
|
||||||
|
for _, h := range history {
|
||||||
|
if h.Period > max {
|
||||||
|
max = h.Period
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumPopulation(history []entity.ProjectFlockKandang) float64 {
|
||||||
|
var total float64
|
||||||
|
for _, h := range history {
|
||||||
|
for _, chickin := range h.Chickins {
|
||||||
|
total += chickin.UsageQty + chickin.PendingUsageQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
|
||||||
|
return ClosingRelationDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToClosingListDTO(e entity.ProjectFlock) ClosingListDTO {
|
||||||
|
var createdUser *userDTO.UserRelationDTO
|
||||||
|
if e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClosingListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToClosingListDTOs(e []entity.ProjectFlock) []ClosingListDTO {
|
||||||
|
result := make([]ClosingListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToClosingListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToClosingDetailDTO(e entity.ProjectFlock) ClosingDetailDTO {
|
||||||
|
return ClosingDetailDTO{
|
||||||
|
ClosingListDTO: ToClosingListDTO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === Response DTO ===
|
||||||
|
type SalesDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
RealizationDate time.Time `json:"realization_date"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
DoNumber string `json:"do_number"`
|
||||||
|
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||||
|
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
|
PaymentStatus string `json:"payment_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PenjualanRealisasiResponseDTO struct {
|
||||||
|
ProjectType string `json:"project_type"`
|
||||||
|
FlockId uint `json:"flock_id"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
Sales []SalesDTO `json:"sales"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||||
|
|
||||||
|
// todo: usia ayam masih dummy
|
||||||
|
age := 0
|
||||||
|
|
||||||
|
var product *productDTO.ProductRelationDTO
|
||||||
|
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||||
|
mapped := productDTO.ToProductRelationDTO(e.MarketingProduct.ProductWarehouse.Product)
|
||||||
|
product = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer *customerDTO.CustomerRelationDTO
|
||||||
|
if e.MarketingProduct.Marketing.Customer.Id != 0 {
|
||||||
|
mapped := customerDTO.ToCustomerRelationDTO(e.MarketingProduct.Marketing.Customer)
|
||||||
|
customer = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandang *kandangDTO.KandangRelationDTO
|
||||||
|
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang)
|
||||||
|
kandang = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
|
||||||
|
|
||||||
|
return SalesDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
RealizationDate: *e.DeliveryDate,
|
||||||
|
Age: age,
|
||||||
|
DoNumber: doNumber,
|
||||||
|
Product: product,
|
||||||
|
Customer: customer,
|
||||||
|
Qty: e.Qty,
|
||||||
|
Weight: e.TotalWeight,
|
||||||
|
AvgWeight: e.AvgWeight,
|
||||||
|
Price: e.UnitPrice,
|
||||||
|
TotalPrice: e.TotalPrice,
|
||||||
|
Kandang: kandang,
|
||||||
|
PaymentStatus: "Paid",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
||||||
|
result := make([]SalesDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToSalesDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||||
|
period := extractPeriodFromRealisasi(e)
|
||||||
|
return PenjualanRealisasiResponseDTO{
|
||||||
|
ProjectType: projectType,
|
||||||
|
FlockId: projectFlockID,
|
||||||
|
Period: period,
|
||||||
|
Sales: ToSalesDTOs(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int {
|
||||||
|
if len(realisasi) > 0 {
|
||||||
|
for _, item := range realisasi {
|
||||||
|
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
||||||
|
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type OverheadDTO struct {
|
||||||
|
ItemName string `json:"item_name"`
|
||||||
|
UOMName string `json:"uom_name"`
|
||||||
|
BudgetQuantity float64 `json:"budget_quantity"`
|
||||||
|
BudgetUnitPrice float64 `json:"budget_unit_price"`
|
||||||
|
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||||
|
ActualDate string `json:"actual_date"`
|
||||||
|
ActualQuantity float64 `json:"actual_quantity"`
|
||||||
|
ActualUnitPrice float64 `json:"actual_unit_price"`
|
||||||
|
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||||
|
CostPerBird float64 `json:"cost_per_bird"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TotalDTO struct {
|
||||||
|
BudgetQuantity float64 `json:"budget_quantity"`
|
||||||
|
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||||
|
ActualQuantity float64 `json:"actual_quantity"`
|
||||||
|
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||||
|
CostPerBird float64 `json:"cost_per_bird"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverheadListDTO struct {
|
||||||
|
Total TotalDTO `json:"total"`
|
||||||
|
Overheads []OverheadDTO `json:"overheads"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseRealization) OverheadDTO {
|
||||||
|
if budget == nil && realization == nil {
|
||||||
|
return OverheadDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemName, itemUOM string
|
||||||
|
if budget != nil {
|
||||||
|
itemName, itemUOM = getItemInfo(budget.Nonstock)
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemName == "" && realization != nil && realization.ExpenseNonstock != nil {
|
||||||
|
itemName, itemUOM = getItemInfo(realization.ExpenseNonstock.Nonstock)
|
||||||
|
}
|
||||||
|
|
||||||
|
dto := OverheadDTO{
|
||||||
|
ItemName: itemName,
|
||||||
|
UOMName: itemUOM,
|
||||||
|
}
|
||||||
|
|
||||||
|
if budget != nil {
|
||||||
|
dto.BudgetQuantity = budget.Qty
|
||||||
|
dto.BudgetUnitPrice = budget.Price
|
||||||
|
dto.BudgetTotalAmount = calculateTotal(budget.Qty, budget.Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
if realization != nil {
|
||||||
|
dto.ActualQuantity = realization.Qty
|
||||||
|
dto.ActualUnitPrice = realization.Price
|
||||||
|
dto.ActualTotalAmount = calculateTotal(realization.Qty, realization.Price)
|
||||||
|
dto.ActualDate = formatRealizationDate(realization)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO {
|
||||||
|
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||||
|
latestDateByNonstockID := make(map[uint]string)
|
||||||
|
|
||||||
|
for i := range budgets {
|
||||||
|
nonstockID := budgets[i].NonstockId
|
||||||
|
if overheadsByNonstockID[nonstockID] == nil {
|
||||||
|
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
||||||
|
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||||
|
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range realizations {
|
||||||
|
if realizations[i].ExpenseNonstock == nil || realizations[i].ExpenseNonstock.NonstockId == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nonstockID := uint(*realizations[i].ExpenseNonstock.NonstockId)
|
||||||
|
if overheadsByNonstockID[nonstockID] == nil {
|
||||||
|
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
|
||||||
|
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||||
|
|
||||||
|
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
||||||
|
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
||||||
|
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||||
|
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||||
|
}
|
||||||
|
|
||||||
|
realizationDateStr := formatRealizationDate(&realizations[i])
|
||||||
|
if realizationDateStr != "" {
|
||||||
|
if latestDateByNonstockID[nonstockID] == "" || realizationDateStr > latestDateByNonstockID[nonstockID] {
|
||||||
|
latestDateByNonstockID[nonstockID] = realizationDateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalBudgetQuantity, totalBudgetAmount, totalActualQuantity, totalActualAmount float64
|
||||||
|
overheadItems := make([]OverheadDTO, 0, len(overheadsByNonstockID))
|
||||||
|
|
||||||
|
for nonstockID, overhead := range overheadsByNonstockID {
|
||||||
|
overhead.ActualDate = latestDateByNonstockID[nonstockID]
|
||||||
|
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty)
|
||||||
|
|
||||||
|
if overhead.ActualQuantity > 0 {
|
||||||
|
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBudgetQuantity += overhead.BudgetQuantity
|
||||||
|
totalBudgetAmount += overhead.BudgetTotalAmount
|
||||||
|
totalActualQuantity += overhead.ActualQuantity
|
||||||
|
totalActualAmount += overhead.ActualTotalAmount
|
||||||
|
|
||||||
|
overheadItems = append(overheadItems, *overhead)
|
||||||
|
}
|
||||||
|
|
||||||
|
return OverheadListDTO{
|
||||||
|
Total: TotalDTO{
|
||||||
|
BudgetQuantity: totalBudgetQuantity,
|
||||||
|
BudgetTotalAmount: totalBudgetAmount,
|
||||||
|
ActualQuantity: totalActualQuantity,
|
||||||
|
ActualTotalAmount: totalActualAmount,
|
||||||
|
CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty),
|
||||||
|
},
|
||||||
|
Overheads: overheadItems,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helper Functions ===
|
||||||
|
|
||||||
|
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
||||||
|
if nonstock != nil && nonstock.Id != 0 {
|
||||||
|
return nonstock.Name, nonstock.Uom.Name
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateTotal(qty, price float64) float64 {
|
||||||
|
return qty * price
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 {
|
||||||
|
if totalChickinQty > 0 {
|
||||||
|
return totalPrice / totalChickinQty
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRealizationDate(realization *entity.ExpenseRealization) string {
|
||||||
|
if realization != nil && realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil {
|
||||||
|
if !realization.ExpenseNonstock.Expense.RealizationDate.IsZero() {
|
||||||
|
return realization.ExpenseNonstock.Expense.RealizationDate.Format("2006-01-02T15:04:05Z07:00")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SapronakDetailDTO struct {
|
||||||
|
ProductID uint `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
Tanggal *time.Time `json:"tanggal,omitempty"`
|
||||||
|
NoReferensi string `json:"no_referensi,omitempty"`
|
||||||
|
JenisTransaksi string `json:"jenis_transaksi,omitempty"`
|
||||||
|
QtyMasuk float64 `json:"qty_masuk"`
|
||||||
|
QtyKeluar float64 `json:"qty_keluar"`
|
||||||
|
Harga float64 `json:"harga"`
|
||||||
|
Nilai float64 `json:"nilai"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakGroupDTO struct {
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
Items []SapronakDetailDTO `json:"items"`
|
||||||
|
TotalMasuk float64 `json:"total_masuk"`
|
||||||
|
TotalKeluar float64 `json:"total_keluar"`
|
||||||
|
SaldoAkhir float64 `json:"saldo_akhir"`
|
||||||
|
TotalNilai float64 `json:"total_nilai"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakItemDTO struct {
|
||||||
|
ProductID uint `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
IncomingQty float64 `json:"incoming_qty"`
|
||||||
|
IncomingValue float64 `json:"incoming_value"`
|
||||||
|
UsageQty float64 `json:"usage_qty"`
|
||||||
|
UsageValue float64 `json:"usage_value"`
|
||||||
|
RemainingQty float64 `json:"remaining_qty"`
|
||||||
|
AveragePrice float64 `json:"average_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakReportDTO struct {
|
||||||
|
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
||||||
|
ProjectFlockID uint `json:"project_flock_id"`
|
||||||
|
ProjectName string `json:"project_name"`
|
||||||
|
KandangID uint `json:"kandang_id"`
|
||||||
|
KandangName string `json:"kandang_name"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
StartDate *time.Time `json:"start_date,omitempty"`
|
||||||
|
EndDate *time.Time `json:"end_date,omitempty"`
|
||||||
|
TotalIncomingValue float64 `json:"total_incoming_value"`
|
||||||
|
TotalUsageValue float64 `json:"total_usage_value"`
|
||||||
|
Items []SapronakItemDTO `json:"items"`
|
||||||
|
Groups []SapronakGroupDTO `json:"groups,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified view for project-level sapronak response
|
||||||
|
type SapronakCategoryRowDTO struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
ReferenceNumber string `json:"reference_number"`
|
||||||
|
QtyIn float64 `json:"qty_in"`
|
||||||
|
QtyOut float64 `json:"qty_out"`
|
||||||
|
QtyUsed float64 `json:"qty_used"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ProductCategory string `json:"product_category"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakCategoryTotalDTO struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
QtyIn float64 `json:"qty_in"`
|
||||||
|
QtyOut float64 `json:"qty_out"`
|
||||||
|
QtyUsed float64 `json:"qty_used"`
|
||||||
|
AvgUnitPrice float64 `json:"avg_unit_price"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakCategoryDTO struct {
|
||||||
|
Rows []SapronakCategoryRowDTO `json:"rows"`
|
||||||
|
Total SapronakCategoryTotalDTO `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakProjectAggregatedDTO struct {
|
||||||
|
Doc *SapronakCategoryDTO `json:"doc,omitempty"`
|
||||||
|
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
|
||||||
|
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
|
||||||
|
Pullet *SapronakCategoryDTO `json:"pullet,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingSapronakItemDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
ReferenceNumber string `json:"reference_number"`
|
||||||
|
TransactionType string `json:"transaction_type"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ProductCategory string `json:"product_category"`
|
||||||
|
ProductSubCategory string `json:"product_sub_category"`
|
||||||
|
SourceWarehouse string `json:"source_warehouse"`
|
||||||
|
DestinationWarehouse string `json:"destination_warehouse,omitempty"`
|
||||||
|
// Destination string `json:"destination,omitempty"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Unit string `json:"unit"`
|
||||||
|
FormattedQuantity string `json:"formatted_quantity"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
SortDate time.Time `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingSapronakDTO struct {
|
||||||
|
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
|
||||||
|
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions for Aggregated Sapronak Response ===
|
||||||
|
|
||||||
|
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||||
|
result := SapronakProjectAggregatedDTO{}
|
||||||
|
|
||||||
|
if len(reports) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
rep := reports[0]
|
||||||
|
return ToSapronakProjectAggregatedFromReport(&rep, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||||
|
result := SapronakProjectAggregatedDTO{}
|
||||||
|
|
||||||
|
if report == nil {
|
||||||
|
report = &SapronakReportDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := strings.ToUpper(strings.TrimSpace(flag))
|
||||||
|
|
||||||
|
byFlag := map[string]**SapronakCategoryDTO{}
|
||||||
|
if filter == "" || filter == "DOC" {
|
||||||
|
result.Doc = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["DOC"] = &result.Doc
|
||||||
|
}
|
||||||
|
if filter == "" || filter == "OVK" {
|
||||||
|
result.Ovk = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["OVK"] = &result.Ovk
|
||||||
|
}
|
||||||
|
if filter == "" || filter == "PAKAN" {
|
||||||
|
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["PAKAN"] = &result.Pakan
|
||||||
|
}
|
||||||
|
if filter == "" || filter == "PULLET" {
|
||||||
|
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["PULLET"] = &result.Pullet
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate := func(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format("02-Jan-2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range report.Groups {
|
||||||
|
flagKey := strings.ToUpper(group.Flag)
|
||||||
|
ptr := byFlag[flagKey]
|
||||||
|
if ptr == nil || *ptr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
target := *ptr
|
||||||
|
|
||||||
|
rowIndexByProduct := make(map[string]int)
|
||||||
|
|
||||||
|
getOrCreateRow := func(productKey string, base SapronakCategoryRowDTO) *SapronakCategoryRowDTO {
|
||||||
|
if idx, ok := rowIndexByProduct[productKey]; ok {
|
||||||
|
return &target.Rows[idx]
|
||||||
|
}
|
||||||
|
target.Rows = append(target.Rows, base)
|
||||||
|
idx := len(target.Rows) - 1
|
||||||
|
rowIndexByProduct[productKey] = idx
|
||||||
|
return &target.Rows[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, item := range group.Items {
|
||||||
|
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
|
||||||
|
baseRow := SapronakCategoryRowDTO{
|
||||||
|
ID: idx + 1,
|
||||||
|
Date: formatDate(item.Tanggal),
|
||||||
|
ReferenceNumber: item.NoReferensi,
|
||||||
|
Description: item.ProductName,
|
||||||
|
ProductCategory: item.ProductName,
|
||||||
|
UnitPrice: item.Harga,
|
||||||
|
Notes: "-",
|
||||||
|
}
|
||||||
|
|
||||||
|
row := getOrCreateRow(productKey, baseRow)
|
||||||
|
|
||||||
|
switch strings.ToLower(item.JenisTransaksi) {
|
||||||
|
case "pembelian", "adjustment masuk", "mutasi masuk":
|
||||||
|
row.QtyIn += item.QtyMasuk
|
||||||
|
row.TotalAmount += item.Nilai
|
||||||
|
case "pemakaian", "adjustment keluar":
|
||||||
|
row.QtyUsed += item.QtyKeluar
|
||||||
|
case "mutasi keluar":
|
||||||
|
row.QtyOut += item.QtyKeluar
|
||||||
|
default:
|
||||||
|
row.QtyIn += item.QtyMasuk
|
||||||
|
row.TotalAmount += item.Nilai
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.QtyIn > 0 {
|
||||||
|
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range target.Rows {
|
||||||
|
target.Rows[i].ID = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTotals := func(cat *SapronakCategoryDTO, label string) {
|
||||||
|
if cat == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var qtyIn, qtyOut, qtyUsed, total float64
|
||||||
|
for _, r := range cat.Rows {
|
||||||
|
qtyIn += r.QtyIn
|
||||||
|
qtyOut += r.QtyOut
|
||||||
|
qtyUsed += r.QtyUsed
|
||||||
|
total += r.TotalAmount
|
||||||
|
}
|
||||||
|
avg := 0.0
|
||||||
|
if qtyIn > 0 {
|
||||||
|
avg = total / qtyIn
|
||||||
|
}
|
||||||
|
cat.Total = SapronakCategoryTotalDTO{
|
||||||
|
Label: label,
|
||||||
|
QtyIn: qtyIn,
|
||||||
|
QtyOut: qtyOut,
|
||||||
|
QtyUsed: qtyUsed,
|
||||||
|
AvgUnitPrice: avg,
|
||||||
|
TotalAmount: total,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTotals(result.Doc, "TOTAL DOC")
|
||||||
|
buildTotals(result.Ovk, "TOTAL OVK")
|
||||||
|
buildTotals(result.Pakan, "TOTAL PAKAN")
|
||||||
|
buildTotals(result.Pullet, "TOTAL PULLET")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package closings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
|
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
|
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClosingModule struct{}
|
||||||
|
|
||||||
|
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
closingRepo := rClosing.NewClosingRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||||
|
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
|
||||||
|
marketingRepo := rMarketings.NewMarketingRepository(db)
|
||||||
|
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
||||||
|
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
|
||||||
|
chickinRepo := rChickin.NewChickinRepository(db)
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
|
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
|
||||||
|
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
ClosingRoutes(router, userService, closingService, sapronakService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,600 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClosingRepository interface {
|
||||||
|
repository.BaseRepository[entity.ProjectFlock]
|
||||||
|
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||||
|
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
||||||
|
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||||
|
FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||||
|
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.ProjectFlock]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClosingRepository(db *gorm.DB) ClosingRepository {
|
||||||
|
return &ClosingRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakRow struct {
|
||||||
|
Id uint64 `gorm:"column:id"`
|
||||||
|
SortDate time.Time `gorm:"column:sort_date"`
|
||||||
|
DateText string `gorm:"column:date_text"`
|
||||||
|
ReferenceNumber string `gorm:"column:reference_number"`
|
||||||
|
TransactionType string `gorm:"column:transaction_type"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
ProductCategory string `gorm:"column:product_category"`
|
||||||
|
ProductSubCategory string `gorm:"column:product_sub_category"`
|
||||||
|
SourceWarehouse string `gorm:"column:source_warehouse"`
|
||||||
|
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
||||||
|
Destination string `gorm:"column:destination"`
|
||||||
|
Quantity float64 `gorm:"column:quantity"`
|
||||||
|
Unit string `gorm:"column:unit"`
|
||||||
|
Notes string `gorm:"column:notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakQueryParams struct {
|
||||||
|
Type string
|
||||||
|
WarehouseIDs []uint
|
||||||
|
ProjectFlockKandangIDs []uint
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
||||||
|
db := r.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
unionParts []string
|
||||||
|
args []any
|
||||||
|
)
|
||||||
|
|
||||||
|
switch params.Type {
|
||||||
|
case validation.SapronakTypeIncoming:
|
||||||
|
if len(params.WarehouseIDs) == 0 {
|
||||||
|
return []SapronakRow{}, 0, nil
|
||||||
|
}
|
||||||
|
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
||||||
|
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||||
|
case validation.SapronakTypeOutgoing:
|
||||||
|
if len(params.WarehouseIDs) > 0 {
|
||||||
|
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
||||||
|
args = append(args, params.WarehouseIDs)
|
||||||
|
}
|
||||||
|
if len(params.ProjectFlockKandangIDs) > 0 {
|
||||||
|
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
||||||
|
args = append(args, params.ProjectFlockKandangIDs)
|
||||||
|
}
|
||||||
|
if len(unionParts) == 0 {
|
||||||
|
return []SapronakRow{}, 0, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, 0, fmt.Errorf("invalid sapronak type: %s", params.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||||
|
|
||||||
|
var totalResults int64
|
||||||
|
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
|
||||||
|
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
|
||||||
|
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
|
||||||
|
|
||||||
|
var rows []SapronakRow
|
||||||
|
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, totalResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sapronakIncomingPurchasesSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(pi.id AS BIGINT) AS id,
|
||||||
|
COALESCE(pi.received_date, '1970-01-01') AS sort_date,
|
||||||
|
COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
|
||||||
|
COALESCE(p.po_number, '') AS reference_number,
|
||||||
|
'Purchase' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
'External Supplier' AS source_warehouse,
|
||||||
|
w.name AS destination_warehouse,
|
||||||
|
'' AS destination,
|
||||||
|
pi.total_qty AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
COALESCE(p.notes, '') AS notes
|
||||||
|
FROM purchase_items pi
|
||||||
|
JOIN purchases p ON p.id = pi.purchase_id
|
||||||
|
JOIN products prod ON prod.id = pi.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
JOIN warehouses w ON w.id = pi.warehouse_id
|
||||||
|
WHERE pi.warehouse_id IN ?
|
||||||
|
`
|
||||||
|
|
||||||
|
sapronakIncomingTransfersSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(st.id AS BIGINT) AS id,
|
||||||
|
st.transfer_date AS sort_date,
|
||||||
|
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||||
|
st.movement_number AS reference_number,
|
||||||
|
'Internal Transfer In' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
COALESCE(fw.name, '') AS source_warehouse,
|
||||||
|
COALESCE(tw.name, '') AS destination_warehouse,
|
||||||
|
'' AS destination,
|
||||||
|
std.quantity AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
'Stock Refill' AS notes
|
||||||
|
FROM stock_transfer_details std
|
||||||
|
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||||
|
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||||
|
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||||
|
JOIN products prod ON prod.id = std.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
WHERE st.to_warehouse_id IN ?
|
||||||
|
`
|
||||||
|
|
||||||
|
sapronakOutgoingTransfersSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(st.id AS BIGINT) AS id,
|
||||||
|
st.transfer_date AS sort_date,
|
||||||
|
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||||
|
st.movement_number AS reference_number,
|
||||||
|
'Internal Transfer Out' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
COALESCE(fw.name, '') AS source_warehouse,
|
||||||
|
'' AS destination_warehouse,
|
||||||
|
COALESCE(tw.name, '') AS destination,
|
||||||
|
std.quantity AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
'Transfer to other unit' AS notes
|
||||||
|
FROM stock_transfer_details std
|
||||||
|
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||||
|
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||||
|
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||||
|
JOIN products prod ON prod.id = std.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
WHERE st.from_warehouse_id IN ?
|
||||||
|
`
|
||||||
|
|
||||||
|
sapronakOutgoingMarketingsSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(mp.id AS BIGINT) AS id,
|
||||||
|
m.so_date AS sort_date,
|
||||||
|
TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text,
|
||||||
|
m.so_number AS reference_number,
|
||||||
|
'Trading Sales' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
w.name AS source_warehouse,
|
||||||
|
'' AS destination_warehouse,
|
||||||
|
'RETAIL CUSTOMER' AS destination,
|
||||||
|
mp.qty AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
m.notes AS notes
|
||||||
|
FROM marketing_products mp
|
||||||
|
JOIN marketings m ON m.id = mp.marketing_id
|
||||||
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||||
|
JOIN products prod ON prod.id = pw.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||||
|
WHERE pw.project_flock_kandang_id IN ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
type SapronakIncomingRow struct {
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
Flag string
|
||||||
|
Qty float64
|
||||||
|
Value float64
|
||||||
|
DefaultPrice float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakUsageRow struct {
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
Flag string
|
||||||
|
Qty float64
|
||||||
|
DefaultPrice float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakDetailRow struct {
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
Flag string
|
||||||
|
Date *time.Time
|
||||||
|
Reference string
|
||||||
|
QtyIn float64
|
||||||
|
QtyOut float64
|
||||||
|
Price float64
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }
|
||||||
|
|
||||||
|
func applyJoins(db *gorm.DB, joins ...string) *gorm.DB {
|
||||||
|
for _, j := range joins {
|
||||||
|
if strings.TrimSpace(j) != "" {
|
||||||
|
db = db.Joins(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func sapronakFlags(flags ...utils.FlagType) []string {
|
||||||
|
out := make([]string, len(flags))
|
||||||
|
for i, f := range flags {
|
||||||
|
out[i] = string(f)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
|
||||||
|
sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
|
||||||
|
sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
|
||||||
|
)
|
||||||
|
|
||||||
|
func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
|
||||||
|
m := make(map[uint][]SapronakDetailRow)
|
||||||
|
for _, row := range rows {
|
||||||
|
m[row.ProductID] = append(m[row.ProductID], row)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanAndGroupDetails(db *gorm.DB) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
rows := make([]SapronakDetailRow, 0)
|
||||||
|
if err := db.Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return groupSapronakDetails(rows), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Usage (summary + details)
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) usageQuery(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) *gorm.DB {
|
||||||
|
db := r.withCtx(ctx).Table(table).Select(`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
COALESCE(SUM(usage_qty), 0) AS qty,
|
||||||
|
COALESCE(p.product_price, 0) AS default_price
|
||||||
|
`)
|
||||||
|
db = applyJoins(db, joins...)
|
||||||
|
return db.
|
||||||
|
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Where(where, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) fetchSapronakUsage(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) ([]SapronakUsageRow, error) {
|
||||||
|
rows := make([]SapronakUsageRow, 0)
|
||||||
|
db := r.usageQuery(ctx, table, pwJoinCond, joins, where, args...)
|
||||||
|
if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) detailQuery(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
selectSQL string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) *gorm.DB {
|
||||||
|
db := r.withCtx(ctx).
|
||||||
|
Table(table).
|
||||||
|
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct)
|
||||||
|
|
||||||
|
db = applyJoins(db, joins...)
|
||||||
|
return db.Select(selectSQL).Where(where, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) fetchSapronakDetails(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
selectSQL string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return scanAndGroupDetails(r.detailQuery(ctx, table, pwJoinCond, joins, selectSQL, where, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||||
|
if pfkID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return r.fetchSapronakUsage(
|
||||||
|
ctx,
|
||||||
|
"recording_stocks rs",
|
||||||
|
"pw.id = rs.product_warehouse_id",
|
||||||
|
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
|
||||||
|
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsUsage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||||
|
if pfkID == 0 {
|
||||||
|
return []SapronakUsageRow{}, nil
|
||||||
|
}
|
||||||
|
return r.fetchSapronakUsage(
|
||||||
|
ctx,
|
||||||
|
"project_chickins pc",
|
||||||
|
"pw.id = pc.product_warehouse_id",
|
||||||
|
nil,
|
||||||
|
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsChickin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return r.fetchSapronakDetails(
|
||||||
|
ctx,
|
||||||
|
"recording_stocks rs",
|
||||||
|
"pw.id = rs.product_warehouse_id",
|
||||||
|
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"}, // penting: supaya alias r valid
|
||||||
|
`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
r.record_datetime AS date,
|
||||||
|
CAST(r.id AS TEXT) AS reference,
|
||||||
|
0 AS qty_in,
|
||||||
|
COALESCE(rs.usage_qty,0) AS qty_out,
|
||||||
|
COALESCE(p.product_price,0) AS price
|
||||||
|
`,
|
||||||
|
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsUsage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return r.fetchSapronakDetails(
|
||||||
|
ctx,
|
||||||
|
"project_chickins pc",
|
||||||
|
"pw.id = pc.product_warehouse_id",
|
||||||
|
nil,
|
||||||
|
`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
pc.chick_in_date AS date,
|
||||||
|
CAST(pc.id AS TEXT) AS reference,
|
||||||
|
0 AS qty_in,
|
||||||
|
COALESCE(pc.usage_qty,0) AS qty_out,
|
||||||
|
COALESCE(p.product_price,0) AS price
|
||||||
|
`,
|
||||||
|
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsChickin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
|
||||||
|
return r.withCtx(ctx).
|
||||||
|
Table("purchase_items AS pi").
|
||||||
|
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
|
||||||
|
Joins("JOIN products p ON p.id = pi.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
|
Where("pi.received_date IS NOT NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
|
||||||
|
rows := make([]SapronakIncomingRow, 0)
|
||||||
|
db := r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||||
|
pi.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
COALESCE(SUM(pi.total_qty), 0) AS qty,
|
||||||
|
COALESCE(SUM(pi.total_qty * pi.price), 0) AS value,
|
||||||
|
COALESCE(p.product_price, 0) AS default_price
|
||||||
|
`)
|
||||||
|
if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return scanAndGroupDetails(
|
||||||
|
r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||||
|
pi.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
pi.received_date AS date,
|
||||||
|
COALESCE(po.po_number, '') AS reference,
|
||||||
|
COALESCE(pi.total_qty,0) AS qty_in,
|
||||||
|
0 AS qty_out,
|
||||||
|
COALESCE(pi.price,0) AS price
|
||||||
|
`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stockLogSapronakRow struct {
|
||||||
|
ID uint `gorm:"column:id"`
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
Flag string `gorm:"column:flag"`
|
||||||
|
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||||
|
Increase float64 `gorm:"column:increase"`
|
||||||
|
Decrease float64 `gorm:"column:decrease"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
MovementNumber string `gorm:"column:movement_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID uint, logType any, withMovement bool) ([]stockLogSapronakRow, error) {
|
||||||
|
rows := make([]stockLogSapronakRow, 0)
|
||||||
|
|
||||||
|
movementSelect := "'' AS movement_number"
|
||||||
|
joins := []string{}
|
||||||
|
if withMovement {
|
||||||
|
movementSelect = "COALESCE(st.movement_number,'') AS movement_number"
|
||||||
|
joins = append(joins, "JOIN stock_transfers st ON st.id = sl.loggable_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := r.withCtx(ctx).
|
||||||
|
Table("stock_logs sl").
|
||||||
|
Select(`
|
||||||
|
sl.id AS id,
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
sl.created_at AS created_at,
|
||||||
|
COALESCE(sl.increase,0) AS increase,
|
||||||
|
COALESCE(sl.decrease,0) AS decrease,
|
||||||
|
COALESCE(p.product_price,0) AS price,
|
||||||
|
` + movementSelect + `
|
||||||
|
`).
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
|
||||||
|
|
||||||
|
db = applyJoins(db, joins...)
|
||||||
|
|
||||||
|
if err := db.
|
||||||
|
Where("sl.loggable_type = ?", logType).
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) string) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow) {
|
||||||
|
in := make(map[uint][]SapronakDetailRow)
|
||||||
|
out := make(map[uint][]SapronakDetailRow)
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
base := SapronakDetailRow{
|
||||||
|
ProductID: row.ProductID,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
Flag: row.Flag,
|
||||||
|
Date: row.CreatedAt,
|
||||||
|
Reference: refFn(row),
|
||||||
|
Price: row.Price,
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.Increase > 0 {
|
||||||
|
d := base
|
||||||
|
d.QtyIn = row.Increase
|
||||||
|
in[row.ProductID] = append(in[row.ProductID], d)
|
||||||
|
}
|
||||||
|
if row.Decrease > 0 {
|
||||||
|
d := base
|
||||||
|
d.QtyOut = row.Decrease
|
||||||
|
out[row.ProductID] = append(out[row.ProductID], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return in, out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||||
|
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) })
|
||||||
|
return in, out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||||
|
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
|
||||||
|
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("TRF-%d", row.ID)
|
||||||
|
})
|
||||||
|
return in, out, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package closings
|
||||||
|
|
||||||
|
import (
|
||||||
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/controllers"
|
||||||
|
closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
||||||
|
ctrl := controller.NewClosingController(s, sapronakSvc)
|
||||||
|
|
||||||
|
route := v1.Group("/closings")
|
||||||
|
|
||||||
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
||||||
|
route.Get("/:project_flock_id/overhead", ctrl.GetOverhead)
|
||||||
|
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang)
|
||||||
|
route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject)
|
||||||
|
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
||||||
|
route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak)
|
||||||
|
}
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
|
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClosingService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
||||||
|
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||||
|
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
|
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||||
|
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
||||||
|
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type closingService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.ClosingRepository
|
||||||
|
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||||
|
MarketingRepo marketingRepository.MarketingRepository
|
||||||
|
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||||
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
|
||||||
|
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
|
||||||
|
ChickinRepo chickinRepository.ProjectChickinRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService {
|
||||||
|
return &closingService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
|
MarketingRepo: marketingRepo,
|
||||||
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
|
ApprovalSvc: approvalSvc,
|
||||||
|
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||||
|
ProjectBudgetRepo: projectBudgetRepo,
|
||||||
|
ChickinRepo: chickinRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("CreatedUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.withRelations(db).
|
||||||
|
Preload("Location").
|
||||||
|
Preload("KandangHistory").
|
||||||
|
Preload("KandangHistory.Chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = s.withClosingRelations(db)
|
||||||
|
if params.Search != "" {
|
||||||
|
return db.Where("flock_name LIKE ?", "%"+params.Search+"%")
|
||||||
|
}
|
||||||
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get closings: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]dto.ClosingListItemDTO, 0, len(closings))
|
||||||
|
for _, closing := range closings {
|
||||||
|
statusProject, _, err := s.getApprovalStatuses(c.Context(), closing.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", closing.Id, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, dto.ToClosingListItemDTO(closing, statusProject))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
||||||
|
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return projectFlock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
|
||||||
|
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("MarketingProduct").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||||
|
Preload("MarketingProduct.Marketing").
|
||||||
|
Preload("MarketingProduct.Marketing.Customer").
|
||||||
|
Order("marketing_delivery_products.delivery_date DESC")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(realisasi) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found")
|
||||||
|
}
|
||||||
|
return realisasi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get project flock %d for closing summary: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := dto.ToClosingSummaryDTO(*project, statusProject, statusClosing)
|
||||||
|
|
||||||
|
return &summary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if params == nil {
|
||||||
|
params = &validation.ClosingSapronakQuery{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Page == 0 {
|
||||||
|
params.Page = 1
|
||||||
|
}
|
||||||
|
if params.Limit == 0 {
|
||||||
|
params.Limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectFlockKandangIDs []uint
|
||||||
|
if params.Type == validation.SapronakTypeOutgoing {
|
||||||
|
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
rows, totalResults, err := s.Repository.GetSapronak(c.Context(), repository.SapronakQueryParams{
|
||||||
|
Type: params.Type,
|
||||||
|
WarehouseIDs: warehouseIDs,
|
||||||
|
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||||
|
Limit: params.Limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak data")
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.ClosingSapronakItemDTO, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
dateStr := row.DateText
|
||||||
|
if dateStr == "" && !row.SortDate.IsZero() {
|
||||||
|
dateStr = row.SortDate.Format("02-Jan-2006")
|
||||||
|
}
|
||||||
|
items = append(items, dto.ClosingSapronakItemDTO{
|
||||||
|
Id: row.Id,
|
||||||
|
Date: dateStr,
|
||||||
|
ReferenceNumber: row.ReferenceNumber,
|
||||||
|
TransactionType: row.TransactionType,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
ProductCategory: row.ProductCategory,
|
||||||
|
ProductSubCategory: row.ProductSubCategory,
|
||||||
|
SourceWarehouse: row.SourceWarehouse,
|
||||||
|
DestinationWarehouse: row.DestinationWarehouse,
|
||||||
|
// Destination: row.Destination,
|
||||||
|
Quantity: row.Quantity,
|
||||||
|
Unit: row.Unit,
|
||||||
|
FormattedQuantity: formatQuantity(row.Quantity, row.Unit),
|
||||||
|
Notes: row.Notes,
|
||||||
|
SortDate: row.SortDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, totalResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
|
var kandangIDs []uint
|
||||||
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
if err := db.Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Pluck("kandang_id", &kandangIDs).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(kandangIDs) == 0 {
|
||||||
|
return []uint{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var warehouses []entity.Warehouse
|
||||||
|
if err := db.Where("kandang_id IN ?", kandangIDs).Find(&warehouses).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unique := make(map[uint]struct{})
|
||||||
|
for _, warehouse := range warehouses {
|
||||||
|
unique[warehouse.Id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]uint, 0, len(unique))
|
||||||
|
for id := range unique {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
|
var ids []uint
|
||||||
|
err := s.Repository.DB().WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Pluck("id", &ids).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatQuantity(qty float64, uom string) string {
|
||||||
|
qtyStr := strconv.FormatFloat(qty, 'f', -1, 64)
|
||||||
|
if uom == "" {
|
||||||
|
return qtyStr
|
||||||
|
}
|
||||||
|
return qtyStr + " " + uom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) {
|
||||||
|
if s.ApprovalSvc == nil {
|
||||||
|
return "", "Belum Selesai", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
minStep uint16
|
||||||
|
statusProject string
|
||||||
|
completed int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, rec := range records {
|
||||||
|
if minStep == 0 || rec.StepNumber < minStep {
|
||||||
|
minStep = rec.StepNumber
|
||||||
|
statusProject = rec.StepName
|
||||||
|
}
|
||||||
|
if rec.StepNumber == uint16(utils.ProjectFlockStepAktif) {
|
||||||
|
completed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusProject == "" && minStep > 0 {
|
||||||
|
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, approvalutils.ApprovalStep(minStep)); ok {
|
||||||
|
statusProject = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusClosing := "Belum Selesai"
|
||||||
|
switch {
|
||||||
|
case len(records) == 0 || completed == 0:
|
||||||
|
statusClosing = "Belum Selesai"
|
||||||
|
case completed < len(records):
|
||||||
|
statusClosing = "Sebagian"
|
||||||
|
default:
|
||||||
|
statusClosing = "Selesai"
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusProject, statusClosing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
|
||||||
|
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalChickinQty float64
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
totalChickinQty += chickin.UsageQty
|
||||||
|
}
|
||||||
|
|
||||||
|
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty)
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,681 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SapronakService interface {
|
||||||
|
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
|
||||||
|
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sapronakService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.ClosingRepository
|
||||||
|
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSapronakService(
|
||||||
|
repo repository.ClosingRepository,
|
||||||
|
pfkRepo projectflockRepository.ProjectFlockKandangRepository,
|
||||||
|
validate *validator.Validate,
|
||||||
|
) SapronakService {
|
||||||
|
return &sapronakService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
ProjectFlockKandangRepo: pfkRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
||||||
|
}
|
||||||
|
reports, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||||
|
ProjectFlockID: projectFlockID,
|
||||||
|
Status: "all",
|
||||||
|
Flag: flag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(reports) <= 1 {
|
||||||
|
return reports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
combined := s.combineSapronakReports(reports, projectFlockID)
|
||||||
|
return []dto.SapronakReportDTO{combined}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error) {
|
||||||
|
if projectFlockID == 0 || pfkID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||||
|
ProjectFlockID: projectFlockID,
|
||||||
|
ProjectFlockKandangID: pfkID,
|
||||||
|
Status: "all",
|
||||||
|
Flag: flag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, res := range results {
|
||||||
|
if res.ProjectFlockID == projectFlockID && res.ProjectFlockKandangID == pfkID {
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||||
|
pfks, err := s.loadProjectFlockKandangs(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(pfks) == 0 {
|
||||||
|
return []dto.SapronakReportDTO{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filterStatus := strings.ToLower(strings.TrimSpace(params.Status))
|
||||||
|
if filterStatus == "" {
|
||||||
|
filterStatus = "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]dto.SapronakReportDTO, 0, len(pfks))
|
||||||
|
for _, pfk := range pfks {
|
||||||
|
status := "closing"
|
||||||
|
if pfk.ClosedAt == nil {
|
||||||
|
status = "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||||
|
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, dto.SapronakReportDTO{
|
||||||
|
ProjectFlockKandangID: pfk.Id,
|
||||||
|
ProjectFlockID: pfk.ProjectFlockId,
|
||||||
|
ProjectName: pfk.ProjectFlock.FlockName,
|
||||||
|
KandangID: pfk.KandangId,
|
||||||
|
KandangName: pfk.Kandang.Name,
|
||||||
|
Period: pfk.Period,
|
||||||
|
Status: status,
|
||||||
|
StartDate: nil,
|
||||||
|
EndDate: nil,
|
||||||
|
TotalIncomingValue: totalIncoming,
|
||||||
|
TotalUsageValue: totalUsage,
|
||||||
|
Items: items,
|
||||||
|
Groups: groups,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||||
|
db := s.ProjectFlockKandangRepo.DB().WithContext(ctx).
|
||||||
|
Preload("ProjectFlock").
|
||||||
|
Preload("Kandang").
|
||||||
|
Preload("Chickins")
|
||||||
|
|
||||||
|
if params != nil {
|
||||||
|
if params.ProjectFlockID > 0 {
|
||||||
|
db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockID)
|
||||||
|
}
|
||||||
|
if params.KandangID > 0 {
|
||||||
|
db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangID)
|
||||||
|
}
|
||||||
|
if params.ProjectFlockKandangID > 0 {
|
||||||
|
db = db.Where("project_flock_kandangs.id = ?", params.ProjectFlockKandangID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pfks []entity.ProjectFlockKandang
|
||||||
|
if err := db.Find(&pfks).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandangs")
|
||||||
|
}
|
||||||
|
return pfks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO, projectID uint) dto.SapronakReportDTO {
|
||||||
|
if len(reports) == 0 {
|
||||||
|
return dto.SapronakReportDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
totalIncoming float64
|
||||||
|
totalUsage float64
|
||||||
|
projectName = reports[0].ProjectName
|
||||||
|
)
|
||||||
|
|
||||||
|
itemMap := make(map[uint]dto.SapronakItemDTO)
|
||||||
|
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||||
|
|
||||||
|
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||||
|
if g, ok := groupMap[flag]; ok {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||||
|
return groupMap[flag]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range reports {
|
||||||
|
totalIncoming += r.TotalIncomingValue
|
||||||
|
totalUsage += r.TotalUsageValue
|
||||||
|
|
||||||
|
for _, it := range r.Items {
|
||||||
|
cur := itemMap[it.ProductID]
|
||||||
|
if cur.ProductID == 0 {
|
||||||
|
cur.ProductID = it.ProductID
|
||||||
|
cur.ProductName = it.ProductName
|
||||||
|
cur.Flag = it.Flag
|
||||||
|
}
|
||||||
|
cur.IncomingQty += it.IncomingQty
|
||||||
|
cur.IncomingValue += it.IncomingValue
|
||||||
|
cur.UsageQty += it.UsageQty
|
||||||
|
cur.UsageValue += it.UsageValue
|
||||||
|
if cur.IncomingQty >= cur.UsageQty {
|
||||||
|
cur.RemainingQty = cur.IncomingQty - cur.UsageQty
|
||||||
|
} else {
|
||||||
|
cur.RemainingQty = 0
|
||||||
|
}
|
||||||
|
if cur.IncomingQty > 0 {
|
||||||
|
cur.AveragePrice = cur.IncomingValue / cur.IncomingQty
|
||||||
|
} else {
|
||||||
|
cur.AveragePrice = it.AveragePrice
|
||||||
|
}
|
||||||
|
itemMap[it.ProductID] = cur
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, g := range r.Groups {
|
||||||
|
agg := ensureGroup(g.Flag)
|
||||||
|
agg.TotalMasuk += g.TotalMasuk
|
||||||
|
agg.TotalKeluar += g.TotalKeluar
|
||||||
|
agg.SaldoAkhir += g.SaldoAkhir
|
||||||
|
agg.TotalNilai += g.TotalNilai
|
||||||
|
agg.Items = append(agg.Items, g.Items...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||||
|
for _, it := range itemMap {
|
||||||
|
items = append(items, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||||
|
for _, g := range groupMap {
|
||||||
|
groups = append(groups, *g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto.SapronakReportDTO{
|
||||||
|
ProjectFlockID: projectID,
|
||||||
|
ProjectName: projectName,
|
||||||
|
Status: "combined",
|
||||||
|
StartDate: nil,
|
||||||
|
TotalIncomingValue: totalIncoming,
|
||||||
|
TotalUsageValue: totalUsage,
|
||||||
|
Items: items,
|
||||||
|
Groups: groups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapIncomingUsage(incomingRows []repository.SapronakIncomingRow, usageRows []repository.SapronakUsageRow) (map[uint]repository.SapronakIncomingRow, map[uint]repository.SapronakUsageRow) {
|
||||||
|
incoming := make(map[uint]repository.SapronakIncomingRow, len(incomingRows))
|
||||||
|
for _, row := range incomingRows {
|
||||||
|
incoming[row.ProductID] = row
|
||||||
|
}
|
||||||
|
usage := make(map[uint]repository.SapronakUsageRow, len(usageRows))
|
||||||
|
for _, row := range usageRows {
|
||||||
|
usage[row.ProductID] = row
|
||||||
|
}
|
||||||
|
return incoming, usage
|
||||||
|
}
|
||||||
|
|
||||||
|
type sapronakDetailMaps struct {
|
||||||
|
Incoming map[uint][]dto.SapronakDetailDTO
|
||||||
|
Usage map[uint][]dto.SapronakDetailDTO
|
||||||
|
AdjIncoming map[uint][]dto.SapronakDetailDTO
|
||||||
|
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
||||||
|
TransferIn map[uint][]dto.SapronakDetailDTO
|
||||||
|
TransferOut map[uint][]dto.SapronakDetailDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSapronakDetails(
|
||||||
|
incomingRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
usageRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
adjIncomingRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
transferInRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
transferOutRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
) sapronakDetailMaps {
|
||||||
|
result := sapronakDetailMaps{
|
||||||
|
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
Usage: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
AdjIncoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
}
|
||||||
|
|
||||||
|
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
||||||
|
for pid, rows := range src {
|
||||||
|
for _, r := range rows {
|
||||||
|
d := dto.SapronakDetailDTO{
|
||||||
|
ProductID: r.ProductID,
|
||||||
|
ProductName: r.ProductName,
|
||||||
|
Flag: r.Flag,
|
||||||
|
Tanggal: r.Date,
|
||||||
|
NoReferensi: r.Reference,
|
||||||
|
JenisTransaksi: jenis,
|
||||||
|
Harga: r.Price,
|
||||||
|
}
|
||||||
|
if masuk {
|
||||||
|
d.QtyMasuk = r.QtyIn
|
||||||
|
d.Nilai = r.QtyIn * r.Price
|
||||||
|
} else {
|
||||||
|
d.QtyKeluar = r.QtyOut
|
||||||
|
d.Nilai = r.QtyOut * r.Price
|
||||||
|
}
|
||||||
|
target[pid] = append(target[pid], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRows(result.Incoming, incomingRows, "Pembelian", true)
|
||||||
|
addRows(result.Usage, usageRows, "Pemakaian", false)
|
||||||
|
addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true)
|
||||||
|
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||||
|
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||||
|
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||||
|
// For sapronak closing report we intentionally ignore date range
|
||||||
|
// and aggregate all historical transactions for the kandang/project.
|
||||||
|
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
usageRows, err := s.Repository.FetchSapronakUsage(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
chickinUsageRows, err := s.Repository.FetchSapronakChickinUsage(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
usageDetailsRows, err := s.Repository.FetchSapronakUsageDetails(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
chickinUsageDetailsRows, err := s.Repository.FetchSapronakChickinUsageDetails(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
transIncomingRows, transOutgoingRows, err := s.Repository.FetchSapronakTransfers(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
||||||
|
matchesFlag := func(f string) bool {
|
||||||
|
if filterFlag == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.ToUpper(f) == filterFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
// For project flocks with category GROWING, pullet usage from chickin
|
||||||
|
// should not be counted yet. Only when category is LAYING we allow
|
||||||
|
// pullet usage to contribute to qty_used.
|
||||||
|
isLaying := strings.EqualFold(string(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying))
|
||||||
|
|
||||||
|
if !isLaying {
|
||||||
|
filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
|
||||||
|
for _, row := range chickinUsageRows {
|
||||||
|
if strings.ToUpper(row.Flag) == "DOC" {
|
||||||
|
filteredUsage = append(filteredUsage, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chickinUsageRows = filteredUsage
|
||||||
|
|
||||||
|
filteredDetail := make(map[uint][]repository.SapronakDetailRow, len(chickinUsageDetailsRows))
|
||||||
|
for pid, rows := range chickinUsageDetailsRows {
|
||||||
|
for _, d := range rows {
|
||||||
|
if strings.ToUpper(d.Flag) == "DOC" {
|
||||||
|
filteredDetail[pid] = append(filteredDetail[pid], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chickinUsageDetailsRows = filteredDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsageRows := append(usageRows, chickinUsageRows...)
|
||||||
|
incoming, usage := mapIncomingUsage(incomingRows, allUsageRows)
|
||||||
|
itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage))
|
||||||
|
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||||
|
|
||||||
|
for pid, rows := range chickinUsageDetailsRows {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
||||||
|
}
|
||||||
|
|
||||||
|
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
|
||||||
|
incomingDetails := detailMaps.Incoming
|
||||||
|
usageDetails := detailMaps.Usage
|
||||||
|
adjIncoming := detailMaps.AdjIncoming
|
||||||
|
adjOutgoing := detailMaps.AdjOutgoing
|
||||||
|
transIncoming := detailMaps.TransferIn
|
||||||
|
transOutgoing := detailMaps.TransferOut
|
||||||
|
|
||||||
|
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||||
|
if g, ok := groupMap[flag]; ok {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||||
|
return groupMap[flag]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range incoming {
|
||||||
|
if !matchesFlag(row.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
avgPrice := row.DefaultPrice
|
||||||
|
if row.Qty > 0 && row.Value > 0 {
|
||||||
|
avgPrice = row.Value / row.Qty
|
||||||
|
}
|
||||||
|
|
||||||
|
itemMap[row.ProductID] = dto.SapronakItemDTO{
|
||||||
|
ProductID: row.ProductID,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
Flag: row.Flag,
|
||||||
|
IncomingQty: row.Qty,
|
||||||
|
IncomingValue: row.Value,
|
||||||
|
RemainingQty: row.Qty,
|
||||||
|
AveragePrice: avgPrice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range usage {
|
||||||
|
if !matchesFlag(row.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[row.ProductID]
|
||||||
|
price := existing.AveragePrice
|
||||||
|
if price == 0 {
|
||||||
|
price = row.DefaultPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
usageValue := row.Qty * price
|
||||||
|
|
||||||
|
existing.ProductID = row.ProductID
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = row.ProductName
|
||||||
|
}
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = row.Flag
|
||||||
|
}
|
||||||
|
existing.AveragePrice = price
|
||||||
|
existing.UsageQty += row.Qty
|
||||||
|
existing.UsageValue += usageValue
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
itemMap[row.ProductID] = existing
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjIncoming {
|
||||||
|
for _, d := range details {
|
||||||
|
if !matchesFlag(d.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[productID]
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = d.Flag
|
||||||
|
}
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = d.ProductName
|
||||||
|
}
|
||||||
|
existing.IncomingQty += d.QtyMasuk
|
||||||
|
existing.IncomingValue += d.Nilai
|
||||||
|
if existing.IncomingQty > 0 {
|
||||||
|
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||||
|
}
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
itemMap[productID] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjOutgoing {
|
||||||
|
for _, d := range details {
|
||||||
|
if !matchesFlag(d.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[productID]
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = d.Flag
|
||||||
|
}
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = d.ProductName
|
||||||
|
}
|
||||||
|
existing.UsageQty += d.QtyKeluar
|
||||||
|
existing.UsageValue += d.Nilai
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
itemMap[productID] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range transIncoming {
|
||||||
|
for _, d := range details {
|
||||||
|
if !matchesFlag(d.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[productID]
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = d.Flag
|
||||||
|
}
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = d.ProductName
|
||||||
|
}
|
||||||
|
existing.IncomingQty += d.QtyMasuk
|
||||||
|
existing.IncomingValue += d.Nilai
|
||||||
|
if existing.IncomingQty > 0 {
|
||||||
|
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||||
|
}
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
itemMap[productID] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||||
|
var totalIncoming, totalUsage float64
|
||||||
|
for _, item := range itemMap {
|
||||||
|
totalIncoming += item.IncomingValue
|
||||||
|
totalUsage += item.UsageValue
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range incomingDetails {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalMasuk += d.QtyMasuk
|
||||||
|
group.TotalNilai += d.Nilai
|
||||||
|
group.SaldoAkhir += d.QtyMasuk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjIncoming {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalMasuk += d.QtyMasuk
|
||||||
|
group.TotalNilai += d.Nilai
|
||||||
|
group.SaldoAkhir += d.QtyMasuk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range usageDetails {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjOutgoing {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range transIncoming {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalMasuk += d.QtyMasuk
|
||||||
|
group.TotalNilai += d.Nilai
|
||||||
|
group.SaldoAkhir += d.QtyMasuk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range transOutgoing {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||||
|
for _, g := range groupMap {
|
||||||
|
groups = append(groups, *g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, groups, totalIncoming, totalUsage, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SapronakTypeIncoming = "incoming"
|
||||||
|
SapronakTypeOutgoing = "outgoing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClosingSapronakQuery struct {
|
||||||
|
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type CountSapronakQuery struct {
|
||||||
|
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
|
||||||
|
KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||||
|
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
|
||||||
|
Status string `query:"status" validate:"omitempty,oneof=active closing all"`
|
||||||
|
Flag string `query:"flag" validate:"omitempty,oneof=DOC OVK PAKAN PULLET doc ovk pakan pullet"`
|
||||||
|
}
|
||||||
@@ -96,30 +96,30 @@ func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
req.Documents = form.File["documents"]
|
req.Documents = form.File["documents"]
|
||||||
|
|
||||||
costPerKandangJSON := c.FormValue("cost_per_kandangs")
|
expenseNonstocksJSON := c.FormValue("expense_nonstocks")
|
||||||
if costPerKandangJSON != "" {
|
if expenseNonstocksJSON != "" {
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(costPerKandangJSON), &req.CostPerKandangs); err != nil {
|
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &req.ExpenseNonstocks); err != nil {
|
||||||
|
|
||||||
var singleCostPerKandang validation.CostPerKandang
|
var singleExpenseNonstock validation.ExpenseNonstock
|
||||||
if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil {
|
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &singleExpenseNonstock); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandangs JSON: %v", err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if singleCostPerKandang.KandangID == 0 {
|
if singleExpenseNonstock.KandangID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required")
|
return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang}
|
req.ExpenseNonstocks = []validation.ExpenseNonstock{singleExpenseNonstock}
|
||||||
} else {
|
} else {
|
||||||
for i, costPerKandang := range req.CostPerKandangs {
|
for i, expenseNonstock := range req.ExpenseNonstocks {
|
||||||
if costPerKandang.KandangID == 0 {
|
if expenseNonstock.KandangID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandangs[%d]", i))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Field cost_per_kandangs is required")
|
return fiber.NewError(fiber.StatusBadRequest, "Field expense_nonstocks is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ExpenseService.CreateOne(c, req)
|
result, err := u.ExpenseService.CreateOne(c, req)
|
||||||
@@ -151,24 +151,40 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Documents = form.File["documents"]
|
req.Documents = form.File["documents"]
|
||||||
if transactionDate := c.FormValue("transaction_date"); transactionDate != "" {
|
|
||||||
|
transactionDate := c.FormValue("transaction_date")
|
||||||
|
if transactionDate != "" {
|
||||||
req.TransactionDate = &transactionDate
|
req.TransactionDate = &transactionDate
|
||||||
}
|
}
|
||||||
|
|
||||||
costPerKandangJSON := c.FormValue("cost_per_kandang")
|
categoryVal := c.FormValue("category")
|
||||||
if costPerKandangJSON != "" {
|
if categoryVal != "" {
|
||||||
var costPerKandang []validation.CostPerKandang
|
req.Category = &categoryVal
|
||||||
if err := json.Unmarshal([]byte(costPerKandangJSON), &costPerKandang); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, costPerKandang := range costPerKandang {
|
supplierIDVal := c.FormValue("supplier_id")
|
||||||
if costPerKandang.KandangID == 0 {
|
if supplierIDVal != "" {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandang[%d]", i))
|
supplierID, err := strconv.ParseUint(supplierIDVal, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format")
|
||||||
|
}
|
||||||
|
req.SupplierID = &supplierID
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseNonstocksJSON := c.FormValue("expense_nonstocks")
|
||||||
|
if expenseNonstocksJSON != "" {
|
||||||
|
var expenseNonstocks []validation.ExpenseNonstock
|
||||||
|
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &expenseNonstocks); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, expenseNonstock := range expenseNonstocks {
|
||||||
|
if expenseNonstock.KandangID == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.CostPerKandang = &costPerKandang
|
req.ExpenseNonstocks = &expenseNonstocks
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ExpenseService.UpdateOne(c, req, uint(id))
|
result, err := u.ExpenseService.UpdateOne(c, req, uint(id))
|
||||||
@@ -300,13 +316,18 @@ func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
req.Documents = form.File["documents"]
|
req.Documents = form.File["documents"]
|
||||||
|
|
||||||
req.RealizationDate = c.FormValue("realization_date")
|
realizationDate := c.FormValue("realization_date")
|
||||||
|
if realizationDate != "" {
|
||||||
|
req.RealizationDate = &realizationDate
|
||||||
|
}
|
||||||
|
|
||||||
realizationsJSON := c.FormValue("realizations")
|
realizationsJSON := c.FormValue("realizations")
|
||||||
if realizationsJSON != "" {
|
if realizationsJSON != "" {
|
||||||
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
|
var realizations []validation.RealizationItem
|
||||||
|
if err := json.Unmarshal([]byte(realizationsJSON), &realizations); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
|
||||||
}
|
}
|
||||||
|
req.Realizations = &realizations
|
||||||
}
|
}
|
||||||
|
|
||||||
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
|
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import (
|
|||||||
type ExpenseRelationDTO struct {
|
type ExpenseRelationDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
PoNumber string `json:"po_number"`
|
PoNumber string `json:"po_number"`
|
||||||
ExpenseDate time.Time `json:"expense_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
GrandTotal float64 `json:"grand_total"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseBaseDTO struct {
|
type ExpenseBaseDTO struct {
|
||||||
@@ -28,8 +27,7 @@ type ExpenseBaseDTO struct {
|
|||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
|
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
|
||||||
RealizationDate *time.Time `json:"realization_date,omitempty"`
|
RealizationDate *time.Time `json:"realization_date,omitempty"`
|
||||||
ExpenseDate time.Time `json:"expense_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
GrandTotal float64 `json:"grand_total"`
|
|
||||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,20 +54,25 @@ type ExpenseDetailDTO struct {
|
|||||||
|
|
||||||
type ExpenseNonstockDTO struct {
|
type ExpenseNonstockDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
|
ExpenseId *uint64 `json:"expense_id,omitempty"`
|
||||||
|
ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"`
|
||||||
|
KandangId *uint64 `json:"kandang_id,omitempty"`
|
||||||
|
NonstockId *uint64 `json:"nonstock_id,omitempty"`
|
||||||
Qty float64 `json:"qty"`
|
Qty float64 `json:"qty"`
|
||||||
UnitPrice float64 `json:"unit_price"`
|
Price float64 `json:"price"`
|
||||||
TotalPrice float64 `json:"total_price"`
|
Notes string `json:"notes"`
|
||||||
Note *string `json:"note,omitempty"`
|
|
||||||
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRealizationDTO struct {
|
type ExpenseRealizationDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
|
ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"`
|
||||||
Qty float64 `json:"qty"`
|
Qty float64 `json:"qty"`
|
||||||
UnitPrice float64 `json:"unit_price"`
|
Price float64 `json:"price"`
|
||||||
TotalPrice float64 `json:"total_price"`
|
Notes string `json:"notes"`
|
||||||
Note *string `json:"note,omitempty"`
|
|
||||||
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KandangGroupDTO struct {
|
type KandangGroupDTO struct {
|
||||||
@@ -91,8 +94,7 @@ func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO {
|
|||||||
return ExpenseRelationDTO{
|
return ExpenseRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
PoNumber: e.PoNumber,
|
PoNumber: e.PoNumber,
|
||||||
ExpenseDate: e.ExpenseDate,
|
TransactionDate: e.TransactionDate,
|
||||||
GrandTotal: e.GrandTotal,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +126,7 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
|
|||||||
Category: e.Category,
|
Category: e.Category,
|
||||||
Supplier: supplier,
|
Supplier: supplier,
|
||||||
RealizationDate: realizationDate,
|
RealizationDate: realizationDate,
|
||||||
ExpenseDate: e.ExpenseDate,
|
TransactionDate: e.TransactionDate,
|
||||||
GrandTotal: e.GrandTotal,
|
|
||||||
Location: location,
|
Location: location,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,10 +193,9 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
|||||||
|
|
||||||
for _, ns := range e.Nonstocks {
|
for _, ns := range e.Nonstocks {
|
||||||
pengajuanDTO := ToExpenseNonstockDTO(ns)
|
pengajuanDTO := ToExpenseNonstockDTO(ns)
|
||||||
|
|
||||||
pengajuans = append(pengajuans, pengajuanDTO)
|
pengajuans = append(pengajuans, pengajuanDTO)
|
||||||
|
|
||||||
if ns.Realization != nil && ns.Realization.Id != 0 {
|
if ns.Realization != nil {
|
||||||
var nonstock *nonstockDTO.NonstockRelationDTO
|
var nonstock *nonstockDTO.NonstockRelationDTO
|
||||||
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
|
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
|
||||||
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
|
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
|
||||||
@@ -204,11 +204,12 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
|||||||
|
|
||||||
realisasiDTO := ExpenseRealizationDTO{
|
realisasiDTO := ExpenseRealizationDTO{
|
||||||
Id: ns.Realization.Id,
|
Id: ns.Realization.Id,
|
||||||
Qty: ns.Realization.RealizationQty,
|
ExpenseNonstockId: ns.Realization.ExpenseNonstockId,
|
||||||
UnitPrice: ns.Realization.RealizationUnitPrice,
|
Qty: ns.Realization.Qty,
|
||||||
TotalPrice: ns.Realization.RealizationTotalPrice,
|
Price: ns.Realization.Price,
|
||||||
Note: ns.Realization.Note,
|
Notes: ns.Realization.Notes,
|
||||||
Nonstock: nonstock,
|
Nonstock: nonstock,
|
||||||
|
CreatedAt: ns.Realization.CreatedAt,
|
||||||
}
|
}
|
||||||
realisasi = append(realisasi, realisasiDTO)
|
realisasi = append(realisasi, realisasiDTO)
|
||||||
}
|
}
|
||||||
@@ -217,12 +218,12 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
|||||||
|
|
||||||
var totalPengajuan float64
|
var totalPengajuan float64
|
||||||
for _, p := range pengajuans {
|
for _, p := range pengajuans {
|
||||||
totalPengajuan += p.TotalPrice
|
totalPengajuan += p.Qty * p.Price
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalRealisasi float64
|
var totalRealisasi float64
|
||||||
for _, r := range realisasi {
|
for _, r := range realisasi {
|
||||||
totalRealisasi += r.TotalPrice
|
totalRealisasi += r.Qty * r.Price
|
||||||
}
|
}
|
||||||
kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks)
|
kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks)
|
||||||
|
|
||||||
@@ -249,11 +250,15 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO {
|
|||||||
|
|
||||||
return ExpenseNonstockDTO{
|
return ExpenseNonstockDTO{
|
||||||
Id: ns.Id,
|
Id: ns.Id,
|
||||||
|
ExpenseId: ns.ExpenseId,
|
||||||
|
ProjectFlockKandangId: ns.ProjectFlockKandangId,
|
||||||
|
KandangId: ns.KandangId,
|
||||||
|
NonstockId: ns.NonstockId,
|
||||||
Qty: ns.Qty,
|
Qty: ns.Qty,
|
||||||
UnitPrice: ns.UnitPrice,
|
Price: ns.Price,
|
||||||
TotalPrice: ns.TotalPrice,
|
Notes: ns.Notes,
|
||||||
Note: &ns.Note,
|
|
||||||
Nonstock: nonstock,
|
Nonstock: nonstock,
|
||||||
|
CreatedAt: ns.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,13 +269,15 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali
|
|||||||
var kandangId uint64
|
var kandangId uint64
|
||||||
var kandangName string
|
var kandangName string
|
||||||
|
|
||||||
|
if p.KandangId != nil {
|
||||||
|
kandangId = *p.KandangId
|
||||||
for _, ns := range nonstocks {
|
for _, ns := range nonstocks {
|
||||||
if ns.Id == p.Id && ns.Kandang != nil {
|
if ns.Id == p.Id && ns.Kandang != nil {
|
||||||
kandangId = uint64(ns.Kandang.Id)
|
|
||||||
kandangName = ns.Kandang.Name
|
kandangName = ns.Kandang.Name
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if kandangId > 0 {
|
if kandangId > 0 {
|
||||||
if kandangMap[kandangId] == nil {
|
if kandangMap[kandangId] == nil {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExpenseRepository interface {
|
type ExpenseRepository interface {
|
||||||
repository.BaseRepository[entity.Expense]
|
repository.BaseRepository[entity.Expense]
|
||||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
GetNextSequence(ctx context.Context) (int, error)
|
GetNextSequence(ctx context.Context) (int, error)
|
||||||
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
|
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
|
||||||
|
WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB
|
||||||
|
CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRepositoryImpl struct {
|
type ExpenseRepositoryImpl struct {
|
||||||
@@ -25,8 +29,8 @@ func NewExpenseRepository(db *gorm.DB) ExpenseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
|
func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
return repository.Exists[entity.Expense](ctx, r.DB(), uint(id))
|
return repository.Exists[entity.Expense](ctx, r.DB(), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) {
|
func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) {
|
||||||
@@ -49,3 +53,57 @@ func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64)
|
|||||||
}
|
}
|
||||||
return &expense, nil
|
return &expense, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if pfkID == 0 && kandangID == 0 {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
q := db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id")
|
||||||
|
if pfkID > 0 && kandangID > 0 {
|
||||||
|
return q.Where("expense_nonstocks.project_flock_kandang_id = ? OR expense_nonstocks.kandang_id = ?", pfkID, kandangID)
|
||||||
|
}
|
||||||
|
if pfkID > 0 {
|
||||||
|
return q.Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID)
|
||||||
|
}
|
||||||
|
return q.Where("expense_nonstocks.kandang_id = ?", kandangID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) {
|
||||||
|
if pfkID == 0 && kandangID == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []uint64
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Table("expenses").
|
||||||
|
Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)).
|
||||||
|
Group("expenses.id").Where("expenses.deleted_at IS NULL").
|
||||||
|
Pluck("expenses.id", &ids).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var unfinished int64
|
||||||
|
for _, id := range ids {
|
||||||
|
var latest entity.Approval
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("approvals").
|
||||||
|
Where("approvable_type = ? AND approvable_id = ?", utils.ApprovalWorkflowExpense.String(), id).
|
||||||
|
Order("action_at DESC").
|
||||||
|
Limit(1).
|
||||||
|
First(&latest).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if isFinished != nil {
|
||||||
|
if !isFinished(&latest) {
|
||||||
|
unfinished++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unfinished, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type ExpenseRealizationRepository interface {
|
|||||||
repository.BaseRepository[entity.ExpenseRealization]
|
repository.BaseRepository[entity.ExpenseRealization]
|
||||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
IdExists(ctx context.Context, id uint64) (bool, error)
|
||||||
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
||||||
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRealizationRepositoryImpl struct {
|
type ExpenseRealizationRepositoryImpl struct {
|
||||||
@@ -30,11 +31,22 @@ func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint
|
|||||||
|
|
||||||
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
|
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
|
||||||
var realization entity.ExpenseRealization
|
var realization entity.ExpenseRealization
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).Where("expense_nonstock_id = ?", expenseNonstockID).First(&realization).Error
|
||||||
Where("expense_nonstock_id = ?", expenseNonstockID).
|
return &realization, err
|
||||||
First(&realization).Error
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error) {
|
||||||
}
|
var realizations []entity.ExpenseRealization
|
||||||
return &realization, nil
|
err := r.DB().WithContext(ctx).
|
||||||
|
Preload("ExpenseNonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock.Uom").
|
||||||
|
Preload("ExpenseNonstock.Expense").
|
||||||
|
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
||||||
|
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
|
||||||
|
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
||||||
|
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||||
|
Where("expenses.category = ?", "BOP").
|
||||||
|
Find(&realizations).Error
|
||||||
|
return realizations, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package expenses
|
package expenses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/controllers"
|
||||||
expense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
expense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,7 +13,9 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
|
|||||||
ctrl := controller.NewExpenseController(s)
|
ctrl := controller.NewExpenseController(s)
|
||||||
|
|
||||||
route := v1.Group("/expenses")
|
route := v1.Group("/expenses")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
|
// route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"time"
|
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||||
@@ -148,8 +148,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, costPerKandang := range req.CostPerKandangs {
|
for _, expenseNonstock := range req.ExpenseNonstocks {
|
||||||
for _, costItem := range costPerKandang.CostItems {
|
for _, costItem := range expenseNonstock.CostItems {
|
||||||
nonstockId := uint(costItem.NonstockID)
|
nonstockId := uint(costItem.NonstockID)
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
@@ -184,26 +184,22 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction)
|
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction)
|
||||||
|
|
||||||
referenceNumber, err := s.generateReferenceNumber(dbTransaction)
|
referenceNumber, err := GenerateExpenseReferenceNumber(c.Context(), expenseRepoTx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
||||||
}
|
}
|
||||||
|
|
||||||
var grandTotal float64
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
for _, costPerKandang := range req.CostPerKandangs {
|
if err != nil {
|
||||||
for _, costItem := range costPerKandang.CostItems {
|
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
grandTotal += costItem.TotalCost
|
|
||||||
}
|
}
|
||||||
}
|
createdBy := uint64(actorID)
|
||||||
|
|
||||||
createdBy := uint64(1) //todo get from auth
|
|
||||||
expense = &entity.Expense{
|
expense = &entity.Expense{
|
||||||
ReferenceNumber: referenceNumber,
|
ReferenceNumber: referenceNumber,
|
||||||
PoNumber: req.PoNumber,
|
PoNumber: req.PoNumber,
|
||||||
Category: req.Category,
|
Category: req.Category,
|
||||||
SupplierId: req.SupplierID,
|
SupplierId: req.SupplierID,
|
||||||
ExpenseDate: expenseDate,
|
TransactionDate: expenseDate,
|
||||||
GrandTotal: grandTotal,
|
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,15 +207,15 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.CostPerKandangs) > 0 {
|
if len(req.ExpenseNonstocks) > 0 {
|
||||||
|
|
||||||
for _, costPerKandang := range req.CostPerKandangs {
|
for _, expenseNonstock := range req.ExpenseNonstocks {
|
||||||
|
|
||||||
var projectFlockKandangId *uint64
|
var projectFlockKandangId *uint64
|
||||||
|
|
||||||
if req.Category == "BOP" {
|
if req.Category == "BOP" {
|
||||||
|
|
||||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(costPerKandang.KandangID))
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
||||||
@@ -230,16 +226,16 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
projectFlockKandangId = &id
|
projectFlockKandangId = &id
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, costItem := range costPerKandang.CostItems {
|
for _, costItem := range expenseNonstock.CostItems {
|
||||||
|
|
||||||
nonstockId := costItem.NonstockID
|
nonstockId := costItem.NonstockID
|
||||||
var kandangId *uint64
|
var kandangId *uint64
|
||||||
if req.Category == "NON-BOP" {
|
if req.Category == "NON-BOP" {
|
||||||
id := uint64(costPerKandang.KandangID)
|
id := uint64(expenseNonstock.KandangID)
|
||||||
kandangId = &id
|
kandangId = &id
|
||||||
} else if req.Category == "BOP" {
|
} else if req.Category == "BOP" {
|
||||||
if projectFlockKandangId != nil {
|
if projectFlockKandangId != nil {
|
||||||
kandangId = &costPerKandang.KandangID
|
kandangId = &expenseNonstock.KandangID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,8 +245,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
KandangId: kandangId,
|
KandangId: kandangId,
|
||||||
NonstockId: &nonstockId,
|
NonstockId: &nonstockId,
|
||||||
Qty: costItem.Quantity,
|
Qty: costItem.Quantity,
|
||||||
TotalPrice: costItem.TotalCost,
|
Price: costItem.Price,
|
||||||
Note: costItem.Notes,
|
Notes: costItem.Notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
|
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
|
||||||
@@ -302,9 +298,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -328,10 +322,27 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format")
|
||||||
}
|
}
|
||||||
updateBody["expense_date"] = expenseDate
|
updateBody["transaction_date"] = expenseDate
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updateBody) == 0 && req.CostPerKandang == nil && len(req.Documents) == 0 {
|
if req.Category != nil {
|
||||||
|
updateBody["category"] = *req.Category
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.SupplierID != nil {
|
||||||
|
supplierID := uint(*req.SupplierID)
|
||||||
|
supplierExistsFunc := func(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id)
|
||||||
|
}
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: supplierExistsFunc},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updateBody["supplier_id"] = *req.SupplierID
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateBody) == 0 && req.ExpenseNonstocks == nil && len(req.Documents) == 0 {
|
||||||
|
|
||||||
responseDTO, err := s.GetOne(c, id)
|
responseDTO, err := s.GetOne(c, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -346,6 +357,24 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
||||||
|
|
||||||
|
currentExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
categoryChanged := false
|
||||||
|
var newCategory string
|
||||||
|
if req.Category != nil && *req.Category != currentExpense.Category {
|
||||||
|
categoryChanged = true
|
||||||
|
newCategory = *req.Category
|
||||||
|
}
|
||||||
|
|
||||||
if len(updateBody) > 0 {
|
if len(updateBody) > 0 {
|
||||||
if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -355,41 +384,82 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.CostPerKandang != nil {
|
if categoryChanged {
|
||||||
|
if currentExpense.Category == "BOP" && newCategory == "NON-BOP" {
|
||||||
|
|
||||||
if err := tx.Where("expense_id = ?", id).Delete(&entity.ExpenseNonstock{}).Error; err != nil {
|
var existingExpenseNonstocks []entity.ExpenseNonstock
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense items")
|
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks")
|
||||||
}
|
}
|
||||||
|
|
||||||
var grandTotal float64
|
for _, ens := range existingExpenseNonstocks {
|
||||||
for _, cpk := range *req.CostPerKandang {
|
updateData := map[string]interface{}{
|
||||||
for _, costItem := range cpk.CostItems {
|
"project_flock_kandang_id": nil,
|
||||||
grandTotal += costItem.TotalCost
|
}
|
||||||
|
if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" {
|
||||||
|
|
||||||
|
var existingExpenseNonstocks []entity.ExpenseNonstock
|
||||||
|
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
|
||||||
|
for _, ens := range existingExpenseNonstocks {
|
||||||
|
if ens.KandangId != nil {
|
||||||
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
|
||||||
|
if err != nil {
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang")
|
||||||
|
}
|
||||||
|
projectFlockKandangId := uint64(projectFlockKandang.Id)
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"project_flock_kandang_id": projectFlockKandangId,
|
||||||
|
}
|
||||||
|
if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{
|
if req.ExpenseNonstocks != nil {
|
||||||
"grand_total": grandTotal,
|
|
||||||
}, nil); err != nil {
|
|
||||||
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense grand total")
|
var existingExpenseNonstocks []entity.ExpenseNonstock
|
||||||
|
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks for deletion")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cpk := range *req.CostPerKandang {
|
for _, ens := range existingExpenseNonstocks {
|
||||||
var projectFlockKandangId *uint64
|
if err := expenseNonstockRepoTx.DeleteOne(c.Context(), uint(ens.Id)); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete expense nonstock")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expense, err := expenseRepoTx.GetByID(c.Context(), id, nil)
|
updatedExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get updated expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
if expense.Category == "BOP" {
|
for _, expenseNonstock := range *req.ExpenseNonstocks {
|
||||||
|
var projectFlockKandangId *uint64
|
||||||
|
|
||||||
|
if updatedExpense.Category == "BOP" {
|
||||||
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
|
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
|
||||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID))
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
||||||
@@ -400,7 +470,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
projectFlockKandangId = &id
|
projectFlockKandangId = &id
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, costItem := range cpk.CostItems {
|
for _, costItem := range expenseNonstock.CostItems {
|
||||||
|
|
||||||
nonstockId := uint(costItem.NonstockID)
|
nonstockId := uint(costItem.NonstockID)
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
@@ -410,13 +480,12 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var kandangId *uint64
|
var kandangId *uint64
|
||||||
if expense.Category == "NON-BOP" {
|
if updatedExpense.Category == "NON-BOP" {
|
||||||
id := uint64(cpk.KandangID)
|
id := uint64(expenseNonstock.KandangID)
|
||||||
kandangId = &id
|
kandangId = &id
|
||||||
} else if expense.Category == "BOP" {
|
} else if updatedExpense.Category == "BOP" {
|
||||||
|
|
||||||
if projectFlockKandangId != nil {
|
if projectFlockKandangId != nil {
|
||||||
kandangId = &cpk.KandangID
|
kandangId = &expenseNonstock.KandangID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,8 +496,8 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
KandangId: kandangId,
|
KandangId: kandangId,
|
||||||
NonstockId: &costItem.NonstockID,
|
NonstockId: &costItem.NonstockID,
|
||||||
Qty: costItem.Quantity,
|
Qty: costItem.Quantity,
|
||||||
TotalPrice: costItem.TotalCost,
|
Price: costItem.Price,
|
||||||
Note: costItem.Notes,
|
Notes: costItem.Notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
|
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
|
||||||
@@ -438,7 +507,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
||||||
|
|
||||||
approvalAction := entity.ApprovalActionUpdated
|
approvalAction := entity.ApprovalActionUpdated
|
||||||
@@ -481,13 +553,25 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get expense for ID %d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||||
@@ -506,9 +590,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -518,7 +600,19 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
|
||||||
}
|
}
|
||||||
|
|
||||||
createdBy := uint64(1) // TODO: replace with authenticated user id
|
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
@@ -544,12 +638,13 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
|
|
||||||
realization := &entity.ExpenseRealization{
|
realization := &entity.ExpenseRealization{
|
||||||
ExpenseNonstockId: &expenseNonstockID,
|
ExpenseNonstockId: &expenseNonstockID,
|
||||||
RealizationQty: realizationItem.Qty,
|
Qty: realizationItem.Qty,
|
||||||
RealizationUnitPrice: realizationItem.UnitPrice,
|
Price: realizationItem.Price,
|
||||||
RealizationTotalPrice: realizationItem.TotalPrice,
|
Notes: "",
|
||||||
RealizationDate: realizationDate,
|
}
|
||||||
Note: realizationItem.Notes,
|
|
||||||
CreatedBy: &createdBy,
|
if realizationItem.Notes != nil {
|
||||||
|
realization.Notes = *realizationItem.Notes
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil {
|
if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil {
|
||||||
@@ -576,7 +671,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
expenseID,
|
expenseID,
|
||||||
utils.ExpenseStepRealisasi,
|
utils.ExpenseStepRealisasi,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
uint(createdBy),
|
uint(1), // TODO: replace with authenticated user id
|
||||||
nil); err != nil {
|
nil); err != nil {
|
||||||
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
||||||
@@ -597,14 +692,15 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) {
|
func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
|
||||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -652,45 +748,51 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (
|
|||||||
|
|
||||||
func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) {
|
func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
s.Log.Errorf("Validation failed for UpdateRealization: %+v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
||||||
}
|
}
|
||||||
|
|
||||||
if latestApproval != nil && latestApproval.StepNumber != uint16(utils.ExpenseStepRealisasi) {
|
if latestApproval != nil && (latestApproval.StepNumber < uint16(utils.ExpenseStepRealisasi)) {
|
||||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest,
|
return nil, fiber.NewError(fiber.StatusBadRequest,
|
||||||
fmt.Sprintf("Cannot update realization at %s step. Must be at Realisasi step", currentStepName))
|
fmt.Sprintf("tidak bisa update realisasi pada step %s. Harus pada step Realisasi atau selesai", currentStepName))
|
||||||
}
|
}
|
||||||
|
|
||||||
var realizationDate *time.Time
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
if req.RealizationDate != "" {
|
|
||||||
parsedDate, err := utils.ParseDateString(req.RealizationDate)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
|
|
||||||
}
|
|
||||||
realizationDate = &parsedDate
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
|
||||||
|
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
|
realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
|
||||||
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
||||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
|
|
||||||
for _, realizationItem := range req.Realizations {
|
// Check if only updating documents
|
||||||
|
updateDataOnly := req.Realizations == nil && len(req.Documents) > 0
|
||||||
|
|
||||||
|
if req.Realizations != nil {
|
||||||
|
for _, realizationItem := range *req.Realizations {
|
||||||
|
|
||||||
expenseNonstockID := realizationItem.ExpenseNonstockID
|
expenseNonstockID := realizationItem.ExpenseNonstockID
|
||||||
|
|
||||||
@@ -709,14 +811,12 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateData := map[string]interface{}{
|
updateData := map[string]interface{}{
|
||||||
"realization_qty": realizationItem.Qty,
|
"qty": realizationItem.Qty,
|
||||||
"realization_unit_price": realizationItem.UnitPrice,
|
"price": realizationItem.Price,
|
||||||
"realization_total_price": realizationItem.TotalPrice,
|
|
||||||
"realization_date": *realizationDate,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if realizationItem.Notes != nil {
|
if realizationItem.Notes != nil {
|
||||||
updateData["note"] = *realizationItem.Notes
|
updateData["notes"] = *realizationItem.Notes
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil {
|
if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil {
|
||||||
@@ -725,11 +825,13 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{
|
}
|
||||||
"realization_date": *realizationDate,
|
|
||||||
}, nil); err != nil {
|
if req.RealizationDate != nil {
|
||||||
|
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{"realization_date": *req.RealizationDate}, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(req.Documents) > 0 {
|
if len(req.Documents) > 0 {
|
||||||
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
||||||
@@ -737,9 +839,28 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !updateDataOnly && *latestApproval.Action == entity.ApprovalActionUpdated {
|
||||||
|
actorID := uint(1) // TODO: replace with authenticated user id
|
||||||
|
approvalAction := entity.ApprovalActionUpdated
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowExpense,
|
||||||
|
expenseID,
|
||||||
|
utils.ExpenseStepRealisasi,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "gagal update realisasi expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
responseDTO, err := s.GetOne(c, expenseID)
|
responseDTO, err := s.GetOne(c, expenseID)
|
||||||
@@ -825,9 +946,7 @@ func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx reposito
|
|||||||
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
|
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(ctx.Context(),
|
if err := commonSvc.EnsureRelations(ctx.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -898,20 +1017,21 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "No expense IDs provided")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "No expense IDs provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
|
||||||
var results []expenseDto.ExpenseDetailDTO
|
var results []expenseDto.ExpenseDetailDTO
|
||||||
|
|
||||||
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
|
|
||||||
for _, id := range req.ApprovableIds {
|
for _, id := range req.ApprovableIds {
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -950,6 +1070,21 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
} else {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
||||||
}
|
}
|
||||||
|
if approvalAction == entity.ApprovalActionApproved {
|
||||||
|
expense, err := expenseRepoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
@@ -996,17 +1131,6 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *expenseService) generateReferenceNumber(ctx *gorm.DB) (string, error) {
|
|
||||||
|
|
||||||
sequence, err := s.Repository.GetNextSequence(ctx.Statement.Context)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
refNum := fmt.Sprintf("BOP-LTI-%05d", sequence)
|
|
||||||
|
|
||||||
return refNum, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) {
|
func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) {
|
||||||
|
|
||||||
expenseRepoTx := repository.NewExpenseRepository(ctx)
|
expenseRepoTx := repository.NewExpenseRepository(ctx)
|
||||||
@@ -1030,13 +1154,45 @@ func (s *expenseService) validateExpenseNonstockRelation(ctx *fiber.Ctx, expense
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func actorIDFromContext(c *fiber.Ctx) (uint, error) {
|
func (s *expenseService) ensureProjectFlockNotClosedForExpense(
|
||||||
// user, ok := authmiddleware.AuthenticatedUser(c)
|
ctx context.Context,
|
||||||
// if !ok || user == nil || user.Id == 0 {
|
expense *entity.Expense,
|
||||||
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
) error {
|
||||||
// }
|
// Kalau repo belum di-wire atau expense kosong → gak usah ngecek apa-apa
|
||||||
// return user.Id, nil
|
if s.ProjectFlockKandangRepo == nil || expense == nil {
|
||||||
// }
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// return user.Id, nil
|
seen := make(map[uint]struct{})
|
||||||
// }
|
|
||||||
|
for _, ens := range expense.Nonstocks {
|
||||||
|
// Field ini pointer, bisa nil
|
||||||
|
if ens.ProjectFlockKandangId == nil || *ens.ProjectFlockKandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pfkID := uint(*ens.ProjectFlockKandangId)
|
||||||
|
if _, ok := seen[pfkID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[pfkID] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, pfkID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Project flock %d tidak ditemukan", pfkID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to validate project flock %d for expense %d: %+v", pfkID, expense.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
// ❗ RULE: kalau ClosedAt tidak nil → project sudah closing
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateExpenseReferenceNumber builds a new reference number using the expense sequence.
|
||||||
|
func GenerateExpenseReferenceNumber(ctx context.Context, repo expenseRepo.ExpenseRepository) (string, error) {
|
||||||
|
sequence, err := repo.GetNextSequence(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("BOP-LTI-%05d", sequence), nil
|
||||||
|
}
|
||||||
@@ -10,10 +10,10 @@ type Create struct {
|
|||||||
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
|
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
|
||||||
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
|
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" json:"cost_per_kandangs" validate:"required,min=1,dive"`
|
ExpenseNonstocks []ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CostPerKandang struct {
|
type ExpenseNonstock struct {
|
||||||
KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"`
|
KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"`
|
||||||
CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"`
|
CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
@@ -21,14 +21,16 @@ type CostPerKandang struct {
|
|||||||
type CostItem struct {
|
type CostItem struct {
|
||||||
NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"`
|
NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"`
|
||||||
Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"`
|
Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"`
|
||||||
TotalCost float64 `form:"total_cost" json:"total_cost" validate:"required,gt=0"`
|
Price float64 `form:"price" json:"price" validate:"required,gt=0"`
|
||||||
Notes string `form:"notes" json:"notes" validate:"required,max=500"`
|
Notes string `form:"notes" json:"notes" validate:"required,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
|
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
CostPerKandang *[]CostPerKandang `form:"cost_per_kandang" json:"cost_per_kandang" validate:"omitempty,min=1,dive"`
|
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||||
|
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
|
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
@@ -44,16 +46,15 @@ type CreateRealization struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateRealization struct {
|
type UpdateRealization struct {
|
||||||
RealizationDate string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
|
RealizationDate *string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
|
Realizations *[]RealizationItem `form:"realizations" json:"realizations" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RealizationItem struct {
|
type RealizationItem struct {
|
||||||
ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"`
|
ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"`
|
||||||
Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"`
|
Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"`
|
||||||
UnitPrice float64 `form:"unit_price" json:"unit_price" validate:"required,gt=0"`
|
Price float64 `form:"price" json:"price" validate:"required,gt=0"`
|
||||||
TotalPrice float64 `form:"total_price" json:"total_price" validate:"required,gt=0"`
|
|
||||||
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
|
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,11 +105,11 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
|
|||||||
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
||||||
return AdjustmentRelationDTO{
|
return AdjustmentRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
TransactionType: e.TransactionType,
|
// TransactionType: e.LoggableType,
|
||||||
Quantity: e.Quantity,
|
// Quantity: e.Q,
|
||||||
BeforeQuantity: e.BeforeQuantity,
|
// BeforeQuantity: e.BeforeQuantity,
|
||||||
AfterQuantity: e.AfterQuantity,
|
// AfterQuantity: e.AfterQuantity,
|
||||||
Note: e.Note,
|
Note: e.Notes,
|
||||||
ProductWarehouseId: e.ProductWarehouseId,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
||||||
}
|
}
|
||||||
@@ -136,6 +136,6 @@ func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
|
|||||||
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
|
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
|
||||||
return AdjustmentDetailDTO{
|
return AdjustmentDetailDTO{
|
||||||
AdjustmentListDTO: ToAdjustmentListDTO(e),
|
AdjustmentListDTO: ToAdjustmentListDTO(e),
|
||||||
UpdatedAt: e.UpdatedAt,
|
// UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
)
|
)
|
||||||
@@ -21,10 +21,11 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
|||||||
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
productRepo := rproduct.NewProductRepository(db)
|
productRepo := rproduct.NewProductRepository(db)
|
||||||
|
|
||||||
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
|
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate, projectFlockKandangRepo)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
AdjustmentRoutes(router, userService, adjustmentService)
|
AdjustmentRoutes(router, userService, adjustmentService)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package adjustments
|
package adjustments
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers"
|
||||||
adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
|
adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,7 +13,7 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme
|
|||||||
ctrl := controller.NewAdjustmentController(s)
|
ctrl := controller.NewAdjustmentController(s)
|
||||||
|
|
||||||
route := v1.Group("/adjustments")
|
route := v1.Group("/adjustments")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// Standard CRUD routes following master data pattern
|
// Standard CRUD routes following master data pattern
|
||||||
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
|
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
|
||||||
route.Post("/", ctrl.Adjustment) // Create adjustment
|
route.Post("/", ctrl.Adjustment) // Create adjustment
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
|
|
||||||
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
|
||||||
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
|
||||||
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
|
||||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
|
||||||
|
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdjustmentService interface {
|
type AdjustmentService interface {
|
||||||
@@ -33,9 +35,10 @@ type adjustmentService struct {
|
|||||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
||||||
ProductRepo productRepo.ProductRepository
|
ProductRepo productRepo.ProductRepository
|
||||||
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService {
|
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) AdjustmentService {
|
||||||
return &adjustmentService{
|
return &adjustmentService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -43,6 +46,7 @@ func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRe
|
|||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
ProductRepo: productRepo,
|
ProductRepo: productRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +70,7 @@ func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if stockLog.LogType != entity.LogTypeAdjustment {
|
if stockLog.LoggableType != entity.LogTypeAdjustment {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +82,10 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err := common.EnsureRelations(c.Context(),
|
if err := common.EnsureRelations(c.Context(),
|
||||||
common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists},
|
common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists},
|
||||||
common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists},
|
common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists},
|
||||||
@@ -102,12 +109,16 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
}
|
}
|
||||||
if !isProductWarehouseExist {
|
if !isProductWarehouseExist {
|
||||||
|
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
newPW := &entity.ProductWarehouse{
|
newPW := &entity.ProductWarehouse{
|
||||||
ProductId: uint(req.ProductID),
|
ProductId: uint(req.ProductID),
|
||||||
WarehouseId: uint(req.WarehouseID),
|
WarehouseId: uint(req.WarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
CreatedBy: 1, // TODO: should Get from auth middleware
|
ProjectFlockKandangId: &projectFlockKandangID,
|
||||||
|
// CreatedBy: 1, // TODO: should Get from auth middleware
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
||||||
@@ -117,6 +128,23 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||||
|
ctx,
|
||||||
|
uint(req.ProductID),
|
||||||
|
uint(req.WarehouseID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get product warehouse for project flock check: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
ctx,
|
||||||
|
s.StockLogsRepository.DB(),
|
||||||
|
[]uint{pw.Id},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,25 +153,23 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
afterQuantity := productWarehouse.Quantity
|
afterQuantity := productWarehouse.Quantity
|
||||||
|
newLog := &entity.StockLog{
|
||||||
|
// TransactionType: transactionType,
|
||||||
|
LoggableType: entity.LogTypeAdjustment,
|
||||||
|
LoggableId: 0,
|
||||||
|
Notes: req.Note,
|
||||||
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
|
CreatedBy: actorID, // TODO: should Get from auth middleware
|
||||||
|
}
|
||||||
if transactionType == entity.TransactionTypeIncrease {
|
if transactionType == entity.TransactionTypeIncrease {
|
||||||
afterQuantity += req.Quantity
|
afterQuantity += req.Quantity
|
||||||
|
newLog.Increase = afterQuantity
|
||||||
} else {
|
} else {
|
||||||
if productWarehouse.Quantity < req.Quantity {
|
if productWarehouse.Quantity < req.Quantity {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
|
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
|
||||||
}
|
}
|
||||||
afterQuantity -= req.Quantity
|
afterQuantity -= req.Quantity
|
||||||
}
|
newLog.Decrease = afterQuantity
|
||||||
|
|
||||||
newLog := &entity.StockLog{
|
|
||||||
TransactionType: transactionType,
|
|
||||||
Quantity: req.Quantity,
|
|
||||||
BeforeQuantity: productWarehouse.Quantity,
|
|
||||||
AfterQuantity: afterQuantity,
|
|
||||||
LogType: entity.LogTypeAdjustment,
|
|
||||||
LogId: 0,
|
|
||||||
Note: req.Note,
|
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
|
||||||
CreatedBy: 1, // TODO: should Get from auth middleware
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
||||||
@@ -169,6 +195,32 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return s.GetOne(c, createdLogId)
|
return s.GetOne(c, createdLogId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||||
|
}
|
||||||
|
|
||||||
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint(projectFlockKandang.Id), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
|
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
|
||||||
if err := s.Validate.Struct(query); err != nil {
|
if err := s.Validate.Struct(query); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -197,7 +249,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
|||||||
|
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
|
|
||||||
db = db.Where("log_type = ?", entity.LogTypeAdjustment)
|
db = db.Where("loggable_type = ?", entity.LogTypeAdjustment)
|
||||||
|
|
||||||
if query.TransactionType != "" {
|
if query.TransactionType != "" {
|
||||||
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||||
|
|||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MarketingDeliveryProductRepository interface {
|
|
||||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
|
||||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
|
||||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarketingDeliveryProductRepositoryImpl struct {
|
|
||||||
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
|
|
||||||
return &MarketingDeliveryProductRepositoryImpl{
|
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
|
|
||||||
var deliveryProduct entity.MarketingDeliveryProduct
|
|
||||||
if err := r.DB().WithContext(ctx).Where("marketing_product_id = ?", marketingProductID).First(&deliveryProduct).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &deliveryProduct, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
|
||||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
|
||||||
|
|
||||||
// Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct
|
|
||||||
// Filter: hanya ambil yang sudah memiliki delivery_date (delivery date tidak null)
|
|
||||||
if err := r.DB().WithContext(ctx).
|
|
||||||
Preload("MarketingProduct").
|
|
||||||
Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id").
|
|
||||||
Where("mp.marketing_id = ?", marketingId).
|
|
||||||
Where("marketing_delivery_products.delivery_date IS NOT NULL").
|
|
||||||
Order("marketing_delivery_products.id ASC").
|
|
||||||
Find(&deliveryProducts).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return deliveryProducts, nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
// entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductStockController struct {
|
||||||
|
ProductStockService service.ProductStockService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductStockController(productStockService service.ProductStockService) *ProductStockController {
|
||||||
|
return &ProductStockController{
|
||||||
|
ProductStockService: productStockService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProductStockController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ProductStockService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ProductStockListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all productStocks successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToProductStockListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProductStockController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := u.ProductStockService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved product successfully",
|
||||||
|
Data: dto.ToProductStockDetailDTO(*res),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||||
|
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
|
||||||
|
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type ProductStockRelationDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductStockListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Brand string `json:"brand"`
|
||||||
|
Sku *string `json:"sku,omitempty"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
|
SellingPrice *float64 `json:"selling_price,omitempty"`
|
||||||
|
Tax *float64 `json:"tax,omitempty"`
|
||||||
|
ExpiryPeriod *int `json:"expiry_period,omitempty"`
|
||||||
|
Flags []string `json:"flags"`
|
||||||
|
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||||
|
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Suppliers []SupplierDTO `json:"suppliers,omitempty"`
|
||||||
|
ProductWarehouses []ProductWarehouseDTO `json:"product_warehouses,omitempty"`
|
||||||
|
TotalStock float64 `json:"total_stock"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductStockDetailDTO struct {
|
||||||
|
ProductStockListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupplierDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductWarehouseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
ProductId uint `json:"product_id"`
|
||||||
|
WarehouseId uint `json:"warehouse_id"`
|
||||||
|
WarehouseName string `json:"warehouse_name"`
|
||||||
|
Location *locationDTO.LocationRelationDTO `json:"location"`
|
||||||
|
CurrentStock float64 `json:"current_stock"`
|
||||||
|
StockLogs []StockLogDetailDTO `json:"stock_logs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockLogDetailDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Increase float64 `json:"increase"`
|
||||||
|
Decrease float64 `json:"decrease"`
|
||||||
|
LoggableType string `json:"loggable_type"`
|
||||||
|
LoggableId uint `json:"loggable_id"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
CreatedBy uint `json:"created_by"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
func ToProductStockListDTO(e entity.Product) ProductStockListDTO {
|
||||||
|
var createdUser *userDTO.UserRelationDTO
|
||||||
|
if e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryRef *productCategoryDTO.ProductCategoryRelationDTO
|
||||||
|
if e.ProductCategory.Id != 0 {
|
||||||
|
mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory)
|
||||||
|
categoryRef = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := make([]string, len(e.Flags))
|
||||||
|
for i, f := range e.Flags {
|
||||||
|
flags[i] = f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var uomRef *uomDTO.UomRelationDTO
|
||||||
|
if e.Uom.Id != 0 {
|
||||||
|
mapped := uomDTO.ToUomRelationDTO(e.Uom)
|
||||||
|
uomRef = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProductStockListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Name: e.Name,
|
||||||
|
Flags: flags,
|
||||||
|
Uom: uomRef,
|
||||||
|
Brand: e.Brand,
|
||||||
|
Sku: e.Sku,
|
||||||
|
ProductPrice: e.ProductPrice,
|
||||||
|
SellingPrice: e.SellingPrice,
|
||||||
|
Tax: e.Tax,
|
||||||
|
ExpiryPeriod: e.ExpiryPeriod,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
ProductCategory: categoryRef,
|
||||||
|
Suppliers: mapSupplierDTOs(e.ProductSuppliers),
|
||||||
|
TotalStock: calculateTotalStock(e.ProductWarehouses),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProductStockListDTOs(e []entity.Product) []ProductStockListDTO {
|
||||||
|
result := make([]ProductStockListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToProductStockListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProductStockDetailDTO(e entity.Product) ProductStockDetailDTO {
|
||||||
|
base := ToProductStockListDTO(e)
|
||||||
|
base.ProductWarehouses = mapProductWarehouseDTOs(e.ProductWarehouses)
|
||||||
|
|
||||||
|
return ProductStockDetailDTO{
|
||||||
|
ProductStockListDTO: base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
func mapSupplierDTOs(src []entity.ProductSupplier) []SupplierDTO {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]SupplierDTO, 0, len(src))
|
||||||
|
for _, ps := range src {
|
||||||
|
if ps.Supplier.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, SupplierDTO{
|
||||||
|
Id: ps.Supplier.Id,
|
||||||
|
Name: ps.Supplier.Name,
|
||||||
|
Alias: ps.Supplier.Alias,
|
||||||
|
Category: ps.Supplier.Category,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapProductWarehouseDTOs(src []entity.ProductWarehouse) []ProductWarehouseDTO {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return []ProductWarehouseDTO{}
|
||||||
|
}
|
||||||
|
result := make([]ProductWarehouseDTO, 0, len(src))
|
||||||
|
for _, pw := range src {
|
||||||
|
dto := ProductWarehouseDTO{
|
||||||
|
Id: pw.Id,
|
||||||
|
ProductId: pw.ProductId,
|
||||||
|
WarehouseId: pw.WarehouseId,
|
||||||
|
CurrentStock: pw.Quantity,
|
||||||
|
StockLogs: mapStockLogs(pw.StockLogs),
|
||||||
|
}
|
||||||
|
if pw.Warehouse.Id != 0 {
|
||||||
|
dto.WarehouseName = pw.Warehouse.Name
|
||||||
|
if pw.Warehouse.Location != nil {
|
||||||
|
mapped := locationDTO.ToLocationRelationDTO(*pw.Warehouse.Location)
|
||||||
|
dto.Location = &mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, dto)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapStockLogs(src []entity.StockLog) []StockLogDetailDTO {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return []StockLogDetailDTO{}
|
||||||
|
}
|
||||||
|
result := make([]StockLogDetailDTO, 0, len(src))
|
||||||
|
for _, log := range src {
|
||||||
|
var notes *string
|
||||||
|
if log.Notes != "" {
|
||||||
|
n := log.Notes
|
||||||
|
notes = &n
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, StockLogDetailDTO{
|
||||||
|
Id: log.Id,
|
||||||
|
Increase: log.Increase,
|
||||||
|
Decrease: log.Decrease,
|
||||||
|
LoggableType: log.LoggableType,
|
||||||
|
LoggableId: log.LoggableId,
|
||||||
|
Notes: notes,
|
||||||
|
ProductWarehouseId: log.ProductWarehouseId,
|
||||||
|
CreatedBy: log.CreatedBy,
|
||||||
|
CreatedUser: mapCreatedUser(log.CreatedUser),
|
||||||
|
CreatedAt: log.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapCreatedUser(user *entity.User) *userDTO.UserRelationDTO {
|
||||||
|
if user == nil || user.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mapped := userDTO.ToUserRelationDTO(*user)
|
||||||
|
return &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateTotalStock(productWarehouses []entity.ProductWarehouse) float64 {
|
||||||
|
var total float64
|
||||||
|
for _, pw := range productWarehouses {
|
||||||
|
total += pw.Quantity
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package productStocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
sProductStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
|
||||||
|
|
||||||
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductStockModule struct{}
|
||||||
|
|
||||||
|
func (ProductStockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
productRepo := rProduct.NewProductRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
productStockService := sProductStock.NewProductStockService(productRepo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
ProductStockRoutes(router, userService, productStockService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
// "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
// "gorm.io/gorm"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// type ProductStockRepository interface {
|
||||||
|
// repository.BaseRepository[entity.ProductStock]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type ProductStockRepositoryImpl struct {
|
||||||
|
// *repository.BaseRepositoryImpl[entity.ProductStock]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func NewProductStockRepository(db *gorm.DB) ProductStockRepository {
|
||||||
|
// return &ProductStockRepositoryImpl{
|
||||||
|
// BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductStock](db),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package productStocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/controllers"
|
||||||
|
productStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProductStockRoutes(v1 fiber.Router, u user.UserService, s productStock.ProductStockService) {
|
||||||
|
ctrl := controller.NewProductStockController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/product-stocks")
|
||||||
|
|
||||||
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user