mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-21 05:45:44 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a6b3121f6 | |||
| 92901bc60c | |||
| 2ea2e1ddf3 | |||
| 8f4548971e | |||
| d940580152 | |||
| c8052f4cb5 | |||
| b8dca3c25e | |||
| c885fba4ef | |||
| 4837ed4255 | |||
| d740a3e26e | |||
| 0766cfeeb2 | |||
| 8e89f9fad0 |
@@ -3,7 +3,7 @@ root = "."
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
|
cmd = "go build -o ./tmp/main ./cmd/api"
|
||||||
bin = "tmp/main"
|
bin = "tmp/main"
|
||||||
full_bin = "APP_ENV=dev ./tmp/main"
|
full_bin = "APP_ENV=dev ./tmp/main"
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ deploy-dev:
|
|||||||
|
|
||||||
only:
|
only:
|
||||||
- development
|
- development
|
||||||
|
- devops-ec2
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
name: development
|
name: development
|
||||||
@@ -4,17 +4,11 @@ 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
|
||||||
@@ -26,33 +20,17 @@ 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
|
||||||
@@ -73,7 +51,6 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/philhofer/fwd v1.1.2 // indirect
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
@@ -98,8 +75,4 @@ require (
|
|||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
|
||||||
modernc.org/memory v1.5.0 // indirect
|
|
||||||
modernc.org/sqlite v1.23.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,44 +2,6 @@ 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=
|
||||||
@@ -65,18 +27,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -94,8 +50,6 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
@@ -192,9 +146,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
@@ -355,12 +306,4 @@ gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
|||||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
|
||||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
|
||||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
|
||||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
|
||||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
|
||||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
|
||||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
|
||||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package capabilities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
||||||
|
// to toggle features. Only permissions recognized by the application are exposed.
|
||||||
|
func FromPermissions(perms []string) map[string]bool {
|
||||||
|
if len(perms) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]bool)
|
||||||
|
for _, perm := range perms {
|
||||||
|
if key, ok := normalizeAndAllow(perm); ok {
|
||||||
|
out[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAndAllow(perm string) (string, bool) {
|
||||||
|
perm = strings.ToLower(strings.TrimSpace(perm))
|
||||||
|
if perm == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if _, ok := allowed[perm]; !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return perm, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowed = map[string]struct{}{
|
||||||
|
recordings.PermissionRecordingRead: {},
|
||||||
|
recordings.PermissionRecordingCreate: {},
|
||||||
|
recordings.PermissionRecordingUpdate: {},
|
||||||
|
recordings.PermissionRecordingDelete: {},
|
||||||
|
}
|
||||||
@@ -84,9 +84,8 @@ func (r *approvalRepositoryImpl) LatestByTargets(
|
|||||||
result := make(map[uint]entity.Approval, len(approvableIDs))
|
result := make(map[uint]entity.Approval, len(approvableIDs))
|
||||||
|
|
||||||
q := r.DB().WithContext(ctx).
|
q := r.DB().WithContext(ctx).
|
||||||
Select("DISTINCT ON (approvable_id) *").
|
|
||||||
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
|
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
|
||||||
Order("approvable_id, action_at DESC")
|
Order("action_at DESC")
|
||||||
|
|
||||||
if modifier != nil {
|
if modifier != nil {
|
||||||
q = modifier(q)
|
q = modifier(q)
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -10,59 +9,45 @@ import (
|
|||||||
|
|
||||||
// Exists reports whether a record with the given ID exists for type T.
|
// Exists reports whether a record with the given ID exists for type T.
|
||||||
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
|
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
|
||||||
var marker int
|
var count int64
|
||||||
err := db.WithContext(ctx).
|
if err := db.WithContext(ctx).
|
||||||
Model(new(T)).
|
Model(new(T)).
|
||||||
Select("1").
|
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Limit(1).
|
Count(&count).Error; err != nil {
|
||||||
Take(&marker).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
|
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
|
||||||
|
var count int64
|
||||||
q := db.WithContext(ctx).
|
q := db.WithContext(ctx).
|
||||||
Model(new(T)).
|
Model(new(T)).
|
||||||
Select("1").
|
|
||||||
Where("name = ?", name).
|
Where("name = ?", name).
|
||||||
Where("deleted_at IS NULL")
|
Where("deleted_at IS NULL")
|
||||||
if excludeID != nil {
|
if excludeID != nil {
|
||||||
q = q.Where("id <> ?", *excludeID)
|
q = q.Where("id <> ?", *excludeID)
|
||||||
}
|
}
|
||||||
var marker int
|
if err := q.Count(&count).Error; err != nil {
|
||||||
if err := q.Limit(1).Take(&marker).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
|
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
|
||||||
if field == "" {
|
if field == "" {
|
||||||
return false, fmt.Errorf("field is required")
|
return false, fmt.Errorf("field is required")
|
||||||
}
|
}
|
||||||
|
var count int64
|
||||||
q := db.WithContext(ctx).
|
q := db.WithContext(ctx).
|
||||||
Model(new(T)).
|
Model(new(T)).
|
||||||
Select("1").
|
|
||||||
Where(fmt.Sprintf("%s = ?", field), value).
|
Where(fmt.Sprintf("%s = ?", field), value).
|
||||||
Where("deleted_at IS NULL")
|
Where("deleted_at IS NULL")
|
||||||
if excludeID != nil {
|
if excludeID != nil {
|
||||||
q = q.Where("id <> ?", *excludeID)
|
q = q.Where("id <> ?", *excludeID)
|
||||||
}
|
}
|
||||||
var marker int
|
if err := q.Count(&count).Error; err != nil {
|
||||||
if err := q.Limit(1).Take(&marker).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StockAllocationRepository interface {
|
|
||||||
BaseRepository[entity.StockAllocation]
|
|
||||||
FindActiveByUsable(ctx context.Context, usableType string, usableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.StockAllocation, error)
|
|
||||||
ReleaseByUsable(ctx context.Context, usableType string, usableID uint, note *string, modifier func(*gorm.DB) *gorm.DB) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type StockAllocationRepositoryImpl struct {
|
|
||||||
*BaseRepositoryImpl[entity.StockAllocation]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStockAllocationRepository(db *gorm.DB) StockAllocationRepository {
|
|
||||||
return &StockAllocationRepositoryImpl{
|
|
||||||
BaseRepositoryImpl: NewBaseRepository[entity.StockAllocation](db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StockAllocationRepositoryImpl) FindActiveByUsable(
|
|
||||||
ctx context.Context,
|
|
||||||
usableType string,
|
|
||||||
usableID uint,
|
|
||||||
modifier func(*gorm.DB) *gorm.DB,
|
|
||||||
) ([]entity.StockAllocation, error) {
|
|
||||||
var allocations []entity.StockAllocation
|
|
||||||
|
|
||||||
q := r.DB().WithContext(ctx).
|
|
||||||
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
|
|
||||||
|
|
||||||
if modifier != nil {
|
|
||||||
q = modifier(q)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := q.Order("created_at ASC").Find(&allocations).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return allocations, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StockAllocationRepositoryImpl) ReleaseByUsable(
|
|
||||||
ctx context.Context,
|
|
||||||
usableType string,
|
|
||||||
usableID uint,
|
|
||||||
note *string,
|
|
||||||
modifier func(*gorm.DB) *gorm.DB,
|
|
||||||
) error {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
updates := map[string]any{
|
|
||||||
"status": entity.StockAllocationStatusReleased,
|
|
||||||
"released_at": now,
|
|
||||||
}
|
|
||||||
if note != nil {
|
|
||||||
updates["note"] = *note
|
|
||||||
}
|
|
||||||
|
|
||||||
baseDB := r.DB()
|
|
||||||
if modifier != nil {
|
|
||||||
baseDB = modifier(baseDB)
|
|
||||||
}
|
|
||||||
|
|
||||||
q := baseDB.WithContext(ctx).
|
|
||||||
Model(&entity.StockAllocation{}).
|
|
||||||
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
|
|
||||||
|
|
||||||
return q.Updates(updates).Error
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,833 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FifoService interface {
|
|
||||||
RegisterStockable(cfg fifo.StockableConfig) error
|
|
||||||
RegisterUsable(cfg fifo.UsableConfig) error
|
|
||||||
|
|
||||||
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
|
|
||||||
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
|
|
||||||
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type fifoService struct {
|
|
||||||
db *gorm.DB
|
|
||||||
logger *logrus.Logger
|
|
||||||
allocations commonRepo.StockAllocationRepository
|
|
||||||
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
|
||||||
defaultOrderBy []string
|
|
||||||
pendingBatchPerUsable int
|
|
||||||
maxLotsPerStockable int
|
|
||||||
defaultAllocationNotes string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFifoService(
|
|
||||||
db *gorm.DB,
|
|
||||||
allocations commonRepo.StockAllocationRepository,
|
|
||||||
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository,
|
|
||||||
logger *logrus.Logger,
|
|
||||||
) FifoService {
|
|
||||||
if logger == nil {
|
|
||||||
logger = logrus.StandardLogger()
|
|
||||||
}
|
|
||||||
return &fifoService{
|
|
||||||
db: db,
|
|
||||||
logger: logger,
|
|
||||||
allocations: allocations,
|
|
||||||
productWarehouseRepo: productWarehouseRepo,
|
|
||||||
defaultOrderBy: []string{"created_at ASC", "id ASC"},
|
|
||||||
pendingBatchPerUsable: 25,
|
|
||||||
maxLotsPerStockable: 50,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) withTransaction(
|
|
||||||
ctx context.Context,
|
|
||||||
tx *gorm.DB,
|
|
||||||
fn func(*gorm.DB) error,
|
|
||||||
) error {
|
|
||||||
if tx != nil {
|
|
||||||
return fn(tx.WithContext(ctx))
|
|
||||||
}
|
|
||||||
return s.db.WithContext(ctx).Transaction(func(inner *gorm.DB) error {
|
|
||||||
return fn(inner)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) txOrDB(tx, db *gorm.DB) *gorm.DB {
|
|
||||||
if tx != nil {
|
|
||||||
return tx
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) RegisterStockable(cfg fifo.StockableConfig) error {
|
|
||||||
return fifo.RegisterStockable(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) RegisterUsable(cfg fifo.UsableConfig) error {
|
|
||||||
return fifo.RegisterUsable(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StockReplenishRequest struct {
|
|
||||||
StockableKey fifo.StockableKey
|
|
||||||
StockableID uint
|
|
||||||
ProductWarehouseID uint
|
|
||||||
Quantity float64
|
|
||||||
Note *string
|
|
||||||
Tx *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
type PendingResolution struct {
|
|
||||||
UsableKey fifo.UsableKey
|
|
||||||
UsableID uint
|
|
||||||
Quantity float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type StockReplenishResult struct {
|
|
||||||
AddedQuantity float64
|
|
||||||
PendingResolved []PendingResolution
|
|
||||||
RemainingPending float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type StockConsumeRequest struct {
|
|
||||||
UsableKey fifo.UsableKey
|
|
||||||
UsableID uint
|
|
||||||
ProductWarehouseID uint
|
|
||||||
Quantity float64
|
|
||||||
AllowPending bool
|
|
||||||
Note *string
|
|
||||||
Tx *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
type AllocationDetail struct {
|
|
||||||
StockableKey fifo.StockableKey
|
|
||||||
StockableID uint
|
|
||||||
Quantity float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type StockConsumeResult struct {
|
|
||||||
RequestedQuantity float64
|
|
||||||
UsageQuantity float64
|
|
||||||
PendingQuantity float64
|
|
||||||
AddedAllocations []AllocationDetail
|
|
||||||
ReleasedQuantity float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type StockReleaseRequest struct {
|
|
||||||
UsableKey fifo.UsableKey
|
|
||||||
UsableID uint
|
|
||||||
Reason *string
|
|
||||||
Tx *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
|
|
||||||
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
|
|
||||||
return nil, errors.New("stockable key and id are required")
|
|
||||||
}
|
|
||||||
if req.ProductWarehouseID == 0 {
|
|
||||||
return nil, errors.New("product warehouse id is required")
|
|
||||||
}
|
|
||||||
if req.Quantity <= 0 {
|
|
||||||
return nil, errors.New("quantity must be greater than zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, ok := fifo.Stockable(req.StockableKey)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("stockable %q is not registered", req.StockableKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &StockReplenishResult{
|
|
||||||
AddedQuantity: req.Quantity,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
|
||||||
if err := s.incrementStockableQty(ctx, tx, cfg, req.StockableID, req.Quantity); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
|
||||||
req.ProductWarehouseID: req.Quantity,
|
|
||||||
}, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return s.txOrDB(tx, db)
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved, err := s.resolvePendingForWarehouse(ctx, tx, req.ProductWarehouseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
result.PendingResolved = resolved
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) {
|
|
||||||
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
|
||||||
return nil, errors.New("usable key and id are required")
|
|
||||||
}
|
|
||||||
if req.Quantity < 0 {
|
|
||||||
return nil, errors.New("quantity must be zero or greater")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, ok := fifo.Usable(req.UsableKey)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &StockConsumeResult{
|
|
||||||
RequestedQuantity: req.Quantity,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
|
||||||
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
productWarehouseID := ctxRow.ProductWarehouseID
|
|
||||||
if productWarehouseID == 0 {
|
|
||||||
return fmt.Errorf("usable %q (id: %d) has no product warehouse reference", req.UsableKey, req.UsableID)
|
|
||||||
}
|
|
||||||
if req.ProductWarehouseID != 0 && req.ProductWarehouseID != productWarehouseID {
|
|
||||||
return fmt.Errorf("usable %q (id: %d) references product warehouse %d but %d was provided", req.UsableKey, req.UsableID, productWarehouseID, req.ProductWarehouseID)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUsage := ctxRow.UsageQty
|
|
||||||
currentPending := ctxRow.PendingQty
|
|
||||||
currentTotal := currentUsage + currentPending
|
|
||||||
delta := req.Quantity - currentTotal
|
|
||||||
|
|
||||||
var (
|
|
||||||
usageDelta float64
|
|
||||||
pendingDelta float64
|
|
||||||
addedAlloc []AllocationDetail
|
|
||||||
releasedAmount float64
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case delta > 0:
|
|
||||||
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if allocationRes.pending > 0 && !req.AllowPending {
|
|
||||||
return fmt.Errorf("insufficient stock: requested %.3f, allocated %.3f", req.Quantity, currentUsage+allocationRes.allocated)
|
|
||||||
}
|
|
||||||
|
|
||||||
usageDelta += allocationRes.allocated
|
|
||||||
pendingDelta += allocationRes.pending
|
|
||||||
addedAlloc = allocationRes.allocations
|
|
||||||
|
|
||||||
if allocationRes.allocated > 0 {
|
|
||||||
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
|
||||||
productWarehouseID: -allocationRes.allocated,
|
|
||||||
}, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return s.txOrDB(tx, db)
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case delta < 0:
|
|
||||||
reductionTarget := -delta
|
|
||||||
|
|
||||||
if currentPending > 0 {
|
|
||||||
pendingReduction := math.Min(currentPending, reductionTarget)
|
|
||||||
if pendingReduction > 0 {
|
|
||||||
pendingDelta -= pendingReduction
|
|
||||||
reductionTarget -= pendingReduction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if reductionTarget > 0 {
|
|
||||||
released, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, reductionTarget)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if released+1e-6 < reductionTarget {
|
|
||||||
return fmt.Errorf("unable to release %.3f from usable %d, only %.3f available", reductionTarget, req.UsableID, released)
|
|
||||||
}
|
|
||||||
usageDelta -= released
|
|
||||||
releasedAmount = released
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// no change
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
result.AddedAllocations = addedAlloc
|
|
||||||
result.ReleasedQuantity = releasedAmount
|
|
||||||
result.UsageQuantity = currentUsage + usageDelta
|
|
||||||
result.PendingQuantity = currentPending + pendingDelta
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest) error {
|
|
||||||
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
|
||||||
return errors.New("usable key and id are required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
|
||||||
cfg, ok := fifo.Usable(req.UsableKey)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("usable %q is not registered", req.UsableKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var usageDelta, pendingDelta float64
|
|
||||||
if ctxRow.UsageQty > 0 {
|
|
||||||
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
usageDelta -= ctxRow.UsageQty
|
|
||||||
}
|
|
||||||
if ctxRow.PendingQty > 0 {
|
|
||||||
pendingDelta -= ctxRow.PendingQty
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.allocations.ReleaseByUsable(ctx, req.UsableKey.String(), req.UsableID, req.Reason, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return s.txOrDB(tx, db)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- helpers ---
|
|
||||||
|
|
||||||
type usableContextRow struct {
|
|
||||||
ProductWarehouseID uint
|
|
||||||
UsageQty float64
|
|
||||||
PendingQty float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) loadUsableContext(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint) (*usableContextRow, error) {
|
|
||||||
var row usableContextRow
|
|
||||||
|
|
||||||
query := tx.Table(cfg.Table).
|
|
||||||
Select(fmt.Sprintf("%s AS product_warehouse_id, COALESCE(%s,0) AS usage_qty, COALESCE(%s,0) AS pending_qty", cfg.Columns.ProductWarehouseID, cfg.Columns.UsageQuantity, cfg.Columns.PendingQuantity)).
|
|
||||||
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id).
|
|
||||||
Clauses(clause.Locking{Strength: "UPDATE"})
|
|
||||||
|
|
||||||
if cfg.Scope != nil {
|
|
||||||
query = cfg.Scope(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Take(&row).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fmt.Errorf("usable record %d not found", id)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &row, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) incrementStockableQty(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
|
|
||||||
column := cfg.Columns.TotalQuantity
|
|
||||||
|
|
||||||
query := tx.Table(cfg.Table).
|
|
||||||
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
|
|
||||||
if cfg.Scope != nil {
|
|
||||||
query = cfg.Scope(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
updates := map[string]any{
|
|
||||||
column: gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty),
|
|
||||||
}
|
|
||||||
if cfg.Columns.TotalUsedQuantity != "" {
|
|
||||||
updates[cfg.Columns.TotalUsedQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0)", cfg.Columns.TotalUsedQuantity))
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.Updates(updates).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) incrementStockableUsage(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
|
|
||||||
if qty == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
column := cfg.Columns.TotalUsedQuantity
|
|
||||||
query := tx.Table(cfg.Table).
|
|
||||||
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
|
|
||||||
if cfg.Scope != nil {
|
|
||||||
query = cfg.Scope(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.Update(column, gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty)).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
type allocationOutcome struct {
|
|
||||||
allocated float64
|
|
||||||
pending float64
|
|
||||||
allocations []AllocationDetail
|
|
||||||
}
|
|
||||||
|
|
||||||
type stockLot struct {
|
|
||||||
StockableKey fifo.StockableKey
|
|
||||||
RecordID uint
|
|
||||||
AvailableQty float64
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) allocateFromStock(
|
|
||||||
ctx context.Context,
|
|
||||||
tx *gorm.DB,
|
|
||||||
productWarehouseID uint,
|
|
||||||
usableKey fifo.UsableKey,
|
|
||||||
usableID uint,
|
|
||||||
requestQty float64,
|
|
||||||
) (*allocationOutcome, error) {
|
|
||||||
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(lots) == 0 {
|
|
||||||
return &allocationOutcome{pending: requestQty}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
remaining = requestQty
|
|
||||||
applied float64
|
|
||||||
allocations []*entities.StockAllocation
|
|
||||||
allocationSummaries []AllocationDetail
|
|
||||||
usageAdjustments = make(map[fifo.StockableKey]map[uint]float64)
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, lot := range lots {
|
|
||||||
if remaining <= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if lot.AvailableQty <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
portion := lot.AvailableQty
|
|
||||||
if portion > remaining {
|
|
||||||
portion = remaining
|
|
||||||
}
|
|
||||||
|
|
||||||
applied += portion
|
|
||||||
remaining -= portion
|
|
||||||
|
|
||||||
allocationSummaries = append(allocationSummaries, AllocationDetail{
|
|
||||||
StockableKey: lot.StockableKey,
|
|
||||||
StockableID: lot.RecordID,
|
|
||||||
Quantity: portion,
|
|
||||||
})
|
|
||||||
|
|
||||||
allocations = append(allocations, &entities.StockAllocation{
|
|
||||||
ProductWarehouseId: productWarehouseID,
|
|
||||||
StockableType: lot.StockableKey.String(),
|
|
||||||
StockableId: lot.RecordID,
|
|
||||||
UsableType: usableKey.String(),
|
|
||||||
UsableId: usableID,
|
|
||||||
Qty: portion,
|
|
||||||
Status: entities.StockAllocationStatusActive,
|
|
||||||
})
|
|
||||||
|
|
||||||
if _, ok := usageAdjustments[lot.StockableKey]; !ok {
|
|
||||||
usageAdjustments[lot.StockableKey] = make(map[uint]float64)
|
|
||||||
}
|
|
||||||
usageAdjustments[lot.StockableKey][lot.RecordID] += portion
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(allocations) > 0 {
|
|
||||||
if err := s.allocations.CreateMany(ctx, allocations, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return s.txOrDB(tx, db)
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, deltas := range usageAdjustments {
|
|
||||||
cfg, ok := fifo.Stockable(key)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for id, qty := range deltas {
|
|
||||||
if err := s.incrementStockableUsage(ctx, tx, cfg, id, qty); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &allocationOutcome{
|
|
||||||
allocated: applied,
|
|
||||||
pending: remaining,
|
|
||||||
allocations: allocationSummaries,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) {
|
|
||||||
configs := fifo.Stockables()
|
|
||||||
if len(configs) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var lots []stockLot
|
|
||||||
for key, cfg := range configs {
|
|
||||||
|
|
||||||
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
|
||||||
|
|
||||||
var selectStmt string
|
|
||||||
if usesNumericTime {
|
|
||||||
|
|
||||||
selectStmt = fmt.Sprintf(
|
|
||||||
"%s AS id, %s AS available_qty, '1970-01-01 00:00:00 UTC'::timestamp AS created_at",
|
|
||||||
cfg.Columns.ID,
|
|
||||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
selectStmt = fmt.Sprintf(
|
|
||||||
"%s AS id, %s AS available_qty, %s AS created_at",
|
|
||||||
cfg.Columns.ID,
|
|
||||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
|
||||||
cfg.Columns.CreatedAt,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []struct {
|
|
||||||
ID uint
|
|
||||||
AvailableQty float64
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
query := tx.Table(cfg.Table).
|
|
||||||
Select(selectStmt).
|
|
||||||
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
|
|
||||||
Where(fmt.Sprintf("%s > %s", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity))
|
|
||||||
|
|
||||||
if cfg.Scope != nil {
|
|
||||||
query = cfg.Scope(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, order := range s.orderClauses(cfg.OrderBy) {
|
|
||||||
query = query.Order(order)
|
|
||||||
}
|
|
||||||
query = query.Limit(s.maxLotsPerStockable)
|
|
||||||
|
|
||||||
if err := query.Find(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.AvailableQty <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lots = append(lots, stockLot{
|
|
||||||
StockableKey: key,
|
|
||||||
RecordID: row.ID,
|
|
||||||
AvailableQty: row.AvailableQty,
|
|
||||||
CreatedAt: row.CreatedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(lots) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(lots, func(i, j int) bool {
|
|
||||||
if lots[i].CreatedAt.Equal(lots[j].CreatedAt) {
|
|
||||||
return lots[i].RecordID < lots[j].RecordID
|
|
||||||
}
|
|
||||||
return lots[i].CreatedAt.Before(lots[j].CreatedAt)
|
|
||||||
})
|
|
||||||
|
|
||||||
return lots, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) applyUsableDeltas(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint, usageDelta, pendingDelta float64) error {
|
|
||||||
if usageDelta == 0 && pendingDelta == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
updates := map[string]any{}
|
|
||||||
if usageDelta != 0 {
|
|
||||||
updates[cfg.Columns.UsageQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.UsageQuantity), usageDelta)
|
|
||||||
}
|
|
||||||
if pendingDelta != 0 {
|
|
||||||
updates[cfg.Columns.PendingQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.PendingQuantity), pendingDelta)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := tx.Table(cfg.Table).Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
|
|
||||||
if cfg.Scope != nil {
|
|
||||||
query = cfg.Scope(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.Updates(updates).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
type pendingCandidate struct {
|
|
||||||
UsableKey fifo.UsableKey
|
|
||||||
Config fifo.UsableConfig
|
|
||||||
UsableID uint
|
|
||||||
Pending float64
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]PendingResolution, error) {
|
|
||||||
candidates, err := s.fetchPendingCandidates(ctx, tx, productWarehouseID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(candidates) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolutions []PendingResolution
|
|
||||||
|
|
||||||
for _, candidate := range candidates {
|
|
||||||
if candidate.Pending <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if outcome.allocated <= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.applyUsableDeltas(ctx, tx, candidate.Config, candidate.UsableID, outcome.allocated, -outcome.allocated); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
|
|
||||||
productWarehouseID: -outcome.allocated,
|
|
||||||
}, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return s.txOrDB(tx, db)
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resolutions = append(resolutions, PendingResolution{
|
|
||||||
UsableKey: candidate.UsableKey,
|
|
||||||
UsableID: candidate.UsableID,
|
|
||||||
Quantity: outcome.allocated,
|
|
||||||
})
|
|
||||||
|
|
||||||
if outcome.pending > 0 {
|
|
||||||
// No more stock available for this warehouse at the moment.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolutions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) releaseUsagePortion(
|
|
||||||
ctx context.Context,
|
|
||||||
tx *gorm.DB,
|
|
||||||
usableKey fifo.UsableKey,
|
|
||||||
usableID uint,
|
|
||||||
target float64,
|
|
||||||
) (float64, error) {
|
|
||||||
if target <= 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
allocations, err := s.allocations.FindActiveByUsable(ctx, usableKey.String(), usableID, func(db *gorm.DB) *gorm.DB {
|
|
||||||
target := s.txOrDB(tx, db)
|
|
||||||
return target.Clauses(clause.Locking{Strength: "UPDATE"})
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if len(allocations) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
remaining = target
|
|
||||||
totalReleased float64
|
|
||||||
warehouseAdjustments = make(map[uint]float64)
|
|
||||||
stockableAdjustments = make(map[fifo.StockableKey]map[uint]float64)
|
|
||||||
)
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
for i := len(allocations) - 1; i >= 0 && remaining > 0; i-- {
|
|
||||||
allocation := allocations[i]
|
|
||||||
releaseAmt := allocation.Qty
|
|
||||||
if releaseAmt > remaining {
|
|
||||||
releaseAmt = remaining
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining -= releaseAmt
|
|
||||||
totalReleased += releaseAmt
|
|
||||||
warehouseAdjustments[allocation.ProductWarehouseId] += releaseAmt
|
|
||||||
|
|
||||||
key := fifo.StockableKey(allocation.StockableType)
|
|
||||||
if _, ok := stockableAdjustments[key]; !ok {
|
|
||||||
stockableAdjustments[key] = make(map[uint]float64)
|
|
||||||
}
|
|
||||||
stockableAdjustments[key][allocation.StockableId] += releaseAmt
|
|
||||||
|
|
||||||
if releaseAmt == allocation.Qty {
|
|
||||||
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
|
|
||||||
"status": entities.StockAllocationStatusReleased,
|
|
||||||
"released_at": now,
|
|
||||||
}, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return s.txOrDB(tx, db)
|
|
||||||
}); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
|
|
||||||
"quantity": allocation.Qty - releaseAmt,
|
|
||||||
}, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return s.txOrDB(tx, db)
|
|
||||||
}); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if totalReleased == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, deltas := range stockableAdjustments {
|
|
||||||
cfg, ok := fifo.Stockable(key)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for id, qty := range deltas {
|
|
||||||
if err := s.incrementStockableUsage(ctx, tx, cfg, id, -qty); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(warehouseAdjustments) > 0 {
|
|
||||||
if err := s.productWarehouseRepo.AdjustQuantities(ctx, warehouseAdjustments, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return s.txOrDB(tx, db)
|
|
||||||
}); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for warehouseID := range warehouseAdjustments {
|
|
||||||
if _, err := s.resolvePendingForWarehouse(ctx, tx, warehouseID); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalReleased, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]pendingCandidate, error) {
|
|
||||||
configs := fifo.Usables()
|
|
||||||
if len(configs) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidates []pendingCandidate
|
|
||||||
|
|
||||||
for key, cfg := range configs {
|
|
||||||
selectStmt := fmt.Sprintf(
|
|
||||||
"%s AS id, %s AS pending_qty, %s AS created_at",
|
|
||||||
cfg.Columns.ID,
|
|
||||||
cfg.Columns.PendingQuantity,
|
|
||||||
cfg.Columns.CreatedAt,
|
|
||||||
)
|
|
||||||
|
|
||||||
var rows []struct {
|
|
||||||
ID uint
|
|
||||||
Pending float64
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
query := tx.Table(cfg.Table).
|
|
||||||
Select(selectStmt).
|
|
||||||
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
|
|
||||||
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
|
|
||||||
Limit(s.pendingBatchPerUsable)
|
|
||||||
|
|
||||||
if cfg.Scope != nil {
|
|
||||||
query = cfg.Scope(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, order := range s.orderClauses(cfg.OrderBy) {
|
|
||||||
query = query.Order(order)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Find(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.Pending <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
candidates = append(candidates, pendingCandidate{
|
|
||||||
UsableKey: key,
|
|
||||||
Config: cfg,
|
|
||||||
UsableID: row.ID,
|
|
||||||
Pending: row.Pending,
|
|
||||||
CreatedAt: row.CreatedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(candidates) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(candidates, func(i, j int) bool {
|
|
||||||
if candidates[i].CreatedAt.Equal(candidates[j].CreatedAt) {
|
|
||||||
return candidates[i].UsableID < candidates[j].UsableID
|
|
||||||
}
|
|
||||||
return candidates[i].CreatedAt.Before(candidates[j].CreatedAt)
|
|
||||||
})
|
|
||||||
|
|
||||||
return candidates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fifoService) orderClauses(custom []string) []string {
|
|
||||||
if len(custom) > 0 {
|
|
||||||
return custom
|
|
||||||
}
|
|
||||||
return s.defaultOrderBy
|
|
||||||
}
|
|
||||||
@@ -65,14 +65,6 @@ 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() {
|
||||||
@@ -114,16 +106,6 @@ 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,39 +2,39 @@
|
|||||||
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(50) NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
email VARCHAR(50) NOT NULL,
|
email VARCHAR 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)
|
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
|
||||||
WHERE
|
|
||||||
deleted_at IS NULL;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_email_unique ON users (email)
|
CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL;
|
||||||
WHERE
|
|
||||||
deleted_at IS NULL;
|
|
||||||
|
|
||||||
-- FLAGS
|
-- FLAGS
|
||||||
CREATE TABLE flags (
|
CREATE TABLE flags (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
flagable_id BIGINT NOT NULL,
|
flagable_id BIGINT NOT NULL,
|
||||||
flagable_type VARCHAR(50) NOT NULL,
|
flagable_type VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
|
CREATE UNIQUE INDEX flags_unique_flagable ON flags (
|
||||||
|
name,
|
||||||
|
flagable_id,
|
||||||
|
flagable_type
|
||||||
|
);
|
||||||
|
|
||||||
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
||||||
|
|
||||||
-- PRODUCT CATEGORIES
|
-- PRODUCT CATEGORIES
|
||||||
CREATE TABLE product_categories (
|
CREATE TABLE product_categories (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR 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(),
|
||||||
@@ -53,7 +53,7 @@ WHERE
|
|||||||
-- UOM
|
-- UOM
|
||||||
CREATE TABLE uoms (
|
CREATE TABLE uoms (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR 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,
|
||||||
@@ -67,9 +67,9 @@ WHERE
|
|||||||
-- BANKS
|
-- BANKS
|
||||||
CREATE TABLE banks (
|
CREATE TABLE banks (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
alias VARCHAR(5) NOT NULL,
|
alias VARCHAR(5) NOT NULL,
|
||||||
owner VARCHAR(50),
|
owner VARCHAR,
|
||||||
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(),
|
||||||
@@ -84,7 +84,7 @@ WHERE
|
|||||||
-- AREAS
|
-- AREAS
|
||||||
CREATE TABLE areas (
|
CREATE TABLE areas (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR 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,
|
||||||
@@ -98,7 +98,7 @@ WHERE
|
|||||||
-- LOCATIONS
|
-- LOCATIONS
|
||||||
CREATE TABLE locations (
|
CREATE TABLE locations (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR 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(),
|
||||||
@@ -114,7 +114,7 @@ WHERE
|
|||||||
-- KANDANG
|
-- KANDANG
|
||||||
CREATE TABLE kandangs (
|
CREATE TABLE kandangs (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR 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(),
|
||||||
@@ -130,7 +130,7 @@ WHERE
|
|||||||
-- WAREHOUSES
|
-- WAREHOUSES
|
||||||
CREATE TABLE warehouses (
|
CREATE TABLE warehouses (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR 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,
|
||||||
@@ -148,12 +148,12 @@ WHERE
|
|||||||
-- CUSTOMERS
|
-- CUSTOMERS
|
||||||
CREATE TABLE customers (
|
CREATE TABLE customers (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR 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(50) NOT NULL,
|
email VARCHAR 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(),
|
||||||
@@ -169,7 +169,7 @@ WHERE
|
|||||||
-- NONSTOCK
|
-- NONSTOCK
|
||||||
CREATE TABLE nonstocks (
|
CREATE TABLE nonstocks (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR 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(),
|
||||||
@@ -184,7 +184,7 @@ WHERE
|
|||||||
-- FCR
|
-- FCR
|
||||||
CREATE TABLE fcrs (
|
CREATE TABLE fcrs (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR 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,
|
||||||
@@ -209,14 +209,14 @@ CREATE TABLE fcr_standards (
|
|||||||
-- SUPPLIERS
|
-- SUPPLIERS
|
||||||
CREATE TABLE suppliers (
|
CREATE TABLE suppliers (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
alias VARCHAR(5) NOT NULL,
|
alias VARCHAR(5) NOT NULL,
|
||||||
pic VARCHAR(50) NOT NULL,
|
pic VARCHAR NOT NULL,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
category VARCHAR(20) NOT NULL,
|
category VARCHAR(20) NOT NULL,
|
||||||
hatchery VARCHAR(50),
|
hatchery VARCHAR,
|
||||||
phone VARCHAR(20) NOT NULL,
|
phone VARCHAR(20) NOT NULL,
|
||||||
email VARCHAR(50) NOT NULL,
|
email VARCHAR NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
npwp VARCHAR(50),
|
npwp VARCHAR(50),
|
||||||
account_number VARCHAR(50),
|
account_number VARCHAR(50),
|
||||||
@@ -242,8 +242,8 @@ CREATE TABLE nonstock_suppliers (
|
|||||||
-- PRODUCTS
|
-- PRODUCTS
|
||||||
CREATE TABLE products (
|
CREATE TABLE products (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
brand VARCHAR(50) NOT NULL,
|
brand VARCHAR 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,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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 NOT NULL,
|
supplier_id BIGINT NULL,
|
||||||
category VARCHAR(50) NOT NULL CHECK (
|
category VARCHAR(50) NOT NULL CHECK (
|
||||||
category IN ('BOP', 'NON-BOP')
|
category IN ('BOP', 'NON-BOP')
|
||||||
),
|
),
|
||||||
|
|||||||
-44
@@ -1,44 +0,0 @@
|
|||||||
-- ============================
|
|
||||||
-- 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();
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
DROP Table IF EXISTS project_budgets;
|
|
||||||
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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
@@ -1,3 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_project_flock_kandangs_closed_at;
|
|
||||||
ALTER TABLE project_flock_kandangs
|
|
||||||
DROP COLUMN IF EXISTS closed_at;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE products
|
|
||||||
DROP COLUMN IF EXISTS is_visible;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE products
|
|
||||||
ADD COLUMN IF NOT EXISTS is_visible BOOLEAN NOT NULL DEFAULT TRUE;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS documents_documentable_polymorphic;
|
|
||||||
DROP TABLE IF EXISTS documents;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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
@@ -1,35 +0,0 @@
|
|||||||
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
@@ -1,41 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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
@@ -1,33 +0,0 @@
|
|||||||
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
@@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
-- 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);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- Drop function and sequence for sales order numbers
|
|
||||||
DROP FUNCTION IF EXISTS generate_so_number();
|
|
||||||
DROP SEQUENCE IF EXISTS so_number_seq;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
-- 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
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE purchases
|
|
||||||
DROP COLUMN IF EXISTS credit_term;
|
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
ALTER TABLE purchases
|
|
||||||
ADD COLUMN IF NOT EXISTS credit_term INT NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
ALTER TABLE purchases
|
|
||||||
ALTER COLUMN credit_term DROP DEFAULT;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_payments_bank_id;
|
|
||||||
DROP INDEX IF EXISTS payments_party_polymorphic;
|
|
||||||
DROP TABLE IF EXISTS payments;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS payments (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
payment_code VARCHAR(50) NOT NULL,
|
|
||||||
reference_number VARCHAR(100) NULL,
|
|
||||||
transaction_type VARCHAR(50),
|
|
||||||
party_type VARCHAR(50) NOT NULL,
|
|
||||||
party_id BIGINT NOT NULL,
|
|
||||||
payment_date TIMESTAMPTZ NOT NULL,
|
|
||||||
payment_method VARCHAR(20) NOT NULL,
|
|
||||||
bank_id BIGINT NULL REFERENCES banks(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
|
||||||
direction VARCHAR(5) NOT NULL,
|
|
||||||
nominal NUMERIC(15, 3) NOT NULL,
|
|
||||||
notes TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ NULL,
|
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
CREATE INDEX payments_party_polymorphic ON payments (party_type, party_id);
|
|
||||||
CREATE INDEX idx_payments_bank_id ON payments (bank_id);
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
r record;
|
|
||||||
trigger_name text;
|
|
||||||
BEGIN
|
|
||||||
FOR r IN
|
|
||||||
SELECT table_schema, table_name
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE column_name = 'deleted_at'
|
|
||||||
AND table_schema = 'public'
|
|
||||||
GROUP BY table_schema, table_name
|
|
||||||
LOOP
|
|
||||||
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
|
||||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
DROP FUNCTION IF EXISTS soft_delete_handle_fk();
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
|
|
||||||
DECLARE
|
|
||||||
fk record;
|
|
||||||
child_column text;
|
|
||||||
parent_column text;
|
|
||||||
parent_value text;
|
|
||||||
child_has_deleted_at boolean;
|
|
||||||
ref_exists boolean;
|
|
||||||
sql text;
|
|
||||||
BEGIN
|
|
||||||
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
|
||||||
FOR fk IN
|
|
||||||
SELECT conrelid::regclass AS child_table,
|
|
||||||
conkey AS child_cols,
|
|
||||||
confkey AS parent_cols,
|
|
||||||
confdeltype
|
|
||||||
FROM pg_constraint
|
|
||||||
WHERE contype = 'f'
|
|
||||||
AND confrelid = TG_RELID
|
|
||||||
LOOP
|
|
||||||
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
|
|
||||||
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
|
|
||||||
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
|
|
||||||
CONTINUE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT attname INTO child_column
|
|
||||||
FROM pg_attribute
|
|
||||||
WHERE attrelid = fk.child_table
|
|
||||||
AND attnum = fk.child_cols[1]
|
|
||||||
AND NOT attisdropped;
|
|
||||||
|
|
||||||
SELECT attname INTO parent_column
|
|
||||||
FROM pg_attribute
|
|
||||||
WHERE attrelid = TG_RELID
|
|
||||||
AND attnum = fk.parent_cols[1]
|
|
||||||
AND NOT attisdropped;
|
|
||||||
|
|
||||||
EXECUTE format('SELECT ($1).%I', parent_column)
|
|
||||||
INTO parent_value
|
|
||||||
USING OLD;
|
|
||||||
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM pg_attribute
|
|
||||||
WHERE attrelid = fk.child_table
|
|
||||||
AND attname = 'deleted_at'
|
|
||||||
AND NOT attisdropped
|
|
||||||
) INTO child_has_deleted_at;
|
|
||||||
|
|
||||||
IF fk.confdeltype IN ('r', 'a') THEN
|
|
||||||
sql := format(
|
|
||||||
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)',
|
|
||||||
fk.child_table,
|
|
||||||
child_column,
|
|
||||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
|
||||||
);
|
|
||||||
EXECUTE sql INTO ref_exists USING parent_value;
|
|
||||||
IF ref_exists THEN
|
|
||||||
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
|
|
||||||
TG_TABLE_NAME, fk.child_table;
|
|
||||||
END IF;
|
|
||||||
ELSIF fk.confdeltype = 'n' THEN
|
|
||||||
sql := format(
|
|
||||||
'UPDATE %s SET %I = NULL WHERE %I = $1 %s',
|
|
||||||
fk.child_table,
|
|
||||||
child_column,
|
|
||||||
child_column,
|
|
||||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
|
||||||
);
|
|
||||||
EXECUTE sql USING parent_value;
|
|
||||||
ELSIF fk.confdeltype = 'c' THEN
|
|
||||||
IF child_has_deleted_at THEN
|
|
||||||
sql := format(
|
|
||||||
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL',
|
|
||||||
fk.child_table,
|
|
||||||
child_column
|
|
||||||
);
|
|
||||||
EXECUTE sql USING parent_value;
|
|
||||||
ELSE
|
|
||||||
sql := format(
|
|
||||||
'DELETE FROM %s WHERE %I = $1',
|
|
||||||
fk.child_table,
|
|
||||||
child_column
|
|
||||||
);
|
|
||||||
EXECUTE sql USING parent_value;
|
|
||||||
END IF;
|
|
||||||
ELSIF fk.confdeltype = 'd' THEN
|
|
||||||
sql := format(
|
|
||||||
'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s',
|
|
||||||
fk.child_table,
|
|
||||||
child_column,
|
|
||||||
child_column,
|
|
||||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
|
||||||
);
|
|
||||||
EXECUTE sql USING parent_value;
|
|
||||||
END IF;
|
|
||||||
END LOOP;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
r record;
|
|
||||||
trigger_name text;
|
|
||||||
BEGIN
|
|
||||||
FOR r IN
|
|
||||||
SELECT table_schema, table_name
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE column_name = 'deleted_at'
|
|
||||||
AND table_schema = 'public'
|
|
||||||
GROUP BY table_schema, table_name
|
|
||||||
LOOP
|
|
||||||
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
|
||||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
|
||||||
EXECUTE format(
|
|
||||||
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
|
|
||||||
trigger_name,
|
|
||||||
r.table_schema,
|
|
||||||
r.table_name
|
|
||||||
);
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP SEQUENCE IF EXISTS payments_code_seq;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CREATE SEQUENCE IF NOT EXISTS payments_code_seq START WITH 1 INCREMENT BY 1;
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
-- Rollback: restore document columns to expenses table
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS document_path JSON;
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS realization_document_path JSON;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- Delete document columns from expenses table since we now use Document service with polymorphic relations
|
|
||||||
ALTER TABLE expenses DROP COLUMN IF EXISTS document_path;
|
|
||||||
ALTER TABLE expenses DROP COLUMN IF EXISTS realization_document_path;
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- Rollback: Remove FIFO fields and restore qty column
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
-- STEP 1: Drop indexes
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_fifo_lookup;
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_pending_qty;
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_usage_qty;
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_created_at;
|
|
||||||
|
|
||||||
-- STEP 2: Drop constraints
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP CONSTRAINT IF EXISTS chk_marketing_delivery_products_fifo_nonneg;
|
|
||||||
|
|
||||||
-- STEP 3: Restore qty column from usage_qty data
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
|
|
||||||
|
|
||||||
-- Migrate data back from usage_qty to qty
|
|
||||||
UPDATE marketing_delivery_products
|
|
||||||
SET qty = usage_qty
|
|
||||||
WHERE qty = 0;
|
|
||||||
|
|
||||||
-- STEP 4: Drop FIFO columns
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP COLUMN IF EXISTS usage_qty,
|
|
||||||
DROP COLUMN IF EXISTS pending_qty,
|
|
||||||
DROP COLUMN IF EXISTS created_at;
|
|
||||||
-58
@@ -1,58 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- Add FIFO fields to marketing_delivery_products
|
|
||||||
-- This migration adds fields needed for FIFO stock management
|
|
||||||
-- and removes the old qty field in favor of FIFO-based allocation
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
-- STEP 0: Drop orphan indexes from previous migration
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_deleted_at;
|
|
||||||
|
|
||||||
-- STEP 1: Add created_at column (required for FIFO ordering)
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
|
||||||
|
|
||||||
-- STEP 2: Add FIFO tracking fields
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD COLUMN IF NOT EXISTS usage_qty NUMERIC(15, 3) DEFAULT 0,
|
|
||||||
ADD COLUMN IF NOT EXISTS pending_qty NUMERIC(15, 3) DEFAULT 0;
|
|
||||||
|
|
||||||
-- STEP 3: Migrate data from old qty to usage_qty for existing records
|
|
||||||
-- This preserves existing quantity data as allocated quantity
|
|
||||||
UPDATE marketing_delivery_products
|
|
||||||
SET
|
|
||||||
usage_qty = COALESCE(qty, 0),
|
|
||||||
pending_qty = 0
|
|
||||||
WHERE usage_qty = 0;
|
|
||||||
|
|
||||||
-- STEP 4: Drop the old qty column (replaced by usage_qty + pending_qty)
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP COLUMN IF EXISTS qty;
|
|
||||||
|
|
||||||
-- STEP 5: Make FIFO fields NOT NULL
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ALTER COLUMN usage_qty SET NOT NULL,
|
|
||||||
ALTER COLUMN pending_qty SET NOT NULL,
|
|
||||||
ALTER COLUMN created_at SET NOT NULL;
|
|
||||||
|
|
||||||
-- STEP 6: Add constraints to ensure non-negative values
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD CONSTRAINT chk_marketing_delivery_products_fifo_nonneg CHECK (
|
|
||||||
usage_qty >= 0 AND
|
|
||||||
pending_qty >= 0
|
|
||||||
);
|
|
||||||
|
|
||||||
-- STEP 7: Create indexes for FIFO operations
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_created_at
|
|
||||||
ON marketing_delivery_products(created_at DESC);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_usage_qty
|
|
||||||
ON marketing_delivery_products(usage_qty)
|
|
||||||
WHERE usage_qty > 0;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_pending_qty
|
|
||||||
ON marketing_delivery_products(pending_qty)
|
|
||||||
WHERE pending_qty > 0;
|
|
||||||
|
|
||||||
-- Composite index for FIFO lookups
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_fifo_lookup
|
|
||||||
ON marketing_delivery_products(marketing_product_id, created_at DESC);
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
-- Remove foreign key constraint
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_product_warehouse;
|
|
||||||
|
|
||||||
-- Drop product_warehouse_id column
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP COLUMN IF EXISTS product_warehouse_id;
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
-- Add product_warehouse_id column to marketing_delivery_products
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD COLUMN IF NOT EXISTS product_warehouse_id INT NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
-- Fill product_warehouse_id from marketing_products
|
|
||||||
UPDATE marketing_delivery_products mdp
|
|
||||||
SET product_warehouse_id = mp.product_warehouse_id
|
|
||||||
FROM marketing_products mp
|
|
||||||
WHERE mdp.marketing_product_id = mp.id
|
|
||||||
AND mdp.product_warehouse_id = 0;
|
|
||||||
|
|
||||||
-- Set NOT NULL constraint
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ALTER COLUMN product_warehouse_id SET NOT NULL;
|
|
||||||
|
|
||||||
-- Add foreign key constraint
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD CONSTRAINT fk_marketing_delivery_products_product_warehouse
|
|
||||||
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id);
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
-- Drop indexes
|
|
||||||
DROP INDEX IF EXISTS idx_standard_growth_details_standard_week;
|
|
||||||
DROP INDEX IF EXISTS idx_production_standard_details_standard_week;
|
|
||||||
DROP INDEX IF EXISTS idx_production_standards_project_category;
|
|
||||||
DROP INDEX IF EXISTS idx_production_standards_deleted_at;
|
|
||||||
|
|
||||||
-- Drop tables (in reverse order due to foreign keys)
|
|
||||||
DROP TABLE IF EXISTS standard_growth_details;
|
|
||||||
DROP TABLE IF EXISTS production_standard_details;
|
|
||||||
DROP TABLE IF EXISTS production_standards;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
-- Create production_standards table
|
|
||||||
CREATE TABLE IF NOT EXISTS production_standards (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
project_category VARCHAR(20) NOT NULL CHECK (project_category IN ('GROWING', 'LAYING')),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ,
|
|
||||||
created_by BIGINT
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create index for deleted_at (soft delete)
|
|
||||||
CREATE INDEX idx_production_standards_deleted_at ON production_standards(deleted_at);
|
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke users
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE production_standards
|
|
||||||
ADD CONSTRAINT fk_production_standards_created_by
|
|
||||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Index
|
|
||||||
CREATE INDEX idx_production_standards_created_by ON production_standards(created_by);
|
|
||||||
|
|
||||||
-- Create production_standard_details table
|
|
||||||
CREATE TABLE IF NOT EXISTS production_standard_details (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
production_standard_id BIGINT NOT NULL,
|
|
||||||
week INT NOT NULL,
|
|
||||||
target_hen_day_production NUMERIC(15, 3),
|
|
||||||
target_hen_house_production NUMERIC(15, 3),
|
|
||||||
target_egg_weight NUMERIC(15, 3),
|
|
||||||
target_egg_mass NUMERIC(15, 3),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke production_standards
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
|
|
||||||
ALTER TABLE production_standard_details
|
|
||||||
ADD CONSTRAINT fk_production_standard_details_standard
|
|
||||||
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Create unique constraint for standard_id + week
|
|
||||||
CREATE UNIQUE INDEX idx_production_standard_details_standard_week
|
|
||||||
ON production_standard_details(production_standard_id, week);
|
|
||||||
|
|
||||||
-- Create standard_growth_details table
|
|
||||||
CREATE TABLE IF NOT EXISTS standard_growth_details (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
production_standard_id BIGINT NOT NULL,
|
|
||||||
target_mean_bw NUMERIC(15, 3),
|
|
||||||
max_depletion NUMERIC(15, 3),
|
|
||||||
min_uniformity NUMERIC(15, 3) NOT NULL,
|
|
||||||
week INT NOT NULL,
|
|
||||||
feed_intake NUMERIC(15, 3),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
created_by BIGINT
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke production_standards
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
|
|
||||||
ALTER TABLE standard_growth_details
|
|
||||||
ADD CONSTRAINT fk_standard_growth_details_standard
|
|
||||||
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke users
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE standard_growth_details
|
|
||||||
ADD CONSTRAINT fk_standard_growth_details_created_by
|
|
||||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Create unique constraint for standard_id + week
|
|
||||||
CREATE UNIQUE INDEX idx_standard_growth_details_standard_week
|
|
||||||
ON standard_growth_details(production_standard_id, week);
|
|
||||||
|
|
||||||
-- Index
|
|
||||||
CREATE INDEX idx_standard_growth_details_created_by ON standard_growth_details(created_by);
|
|
||||||
|
|
||||||
-- Create index for project_category
|
|
||||||
CREATE INDEX idx_production_standards_project_category ON production_standards(project_category);
|
|
||||||
@@ -588,7 +588,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagAyamAfkir},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Mati",
|
Name: "Ayam Mati",
|
||||||
@@ -597,7 +596,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagAyamMati},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Culling",
|
Name: "Ayam Culling",
|
||||||
@@ -606,7 +604,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagAyamCulling},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Konsumsi Baik",
|
Name: "Telur Konsumsi Baik",
|
||||||
@@ -615,7 +612,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Unit",
|
Uom: "Unit",
|
||||||
Category: "Telur",
|
Category: "Telur",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagTelurUtuh},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Pecah",
|
Name: "Telur Pecah",
|
||||||
@@ -624,7 +620,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Unit",
|
Uom: "Unit",
|
||||||
Category: "Telur",
|
Category: "Telur",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagTelurPecah},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "281 SPECIAL STARTER",
|
Name: "281 SPECIAL STARTER",
|
||||||
@@ -637,16 +632,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||||
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
|
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "Ayam Layer",
|
|
||||||
Brand: "-",
|
|
||||||
Sku: "LYR0001",
|
|
||||||
Uom: "Ekor",
|
|
||||||
Category: "Pullet",
|
|
||||||
Price: 20000,
|
|
||||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
|
||||||
Flags: []utils.FlagType{utils.FlagLayer},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, seed := range seeds {
|
for _, seed := range seeds {
|
||||||
@@ -925,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:"type:varchar(50);not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"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:"type:varchar(50);not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"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:"type:varchar(50)"`
|
Owner *string `gorm:""`
|
||||||
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:"type:varchar(50);not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"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:"type:varchar(50);not null"`
|
Email string `gorm:"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"`
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -12,18 +13,20 @@ 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)"`
|
||||||
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
|
DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan
|
||||||
TransactionDate time.Time `gorm:"type:date;not null"`
|
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi
|
||||||
Notes string `gorm:"type:text;column:notes"`
|
RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi
|
||||||
|
ExpenseDate time.Time `gorm:"type:date;not null"`
|
||||||
|
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
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"`
|
||||||
Documents []Document `gorm:"foreignKey:DocumentableId;references:Id"`
|
|
||||||
RealizationDocuments []Document `gorm:"foreignKey:DocumentableId;references:Id"`
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
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:""`
|
||||||
@@ -11,13 +7,14 @@ 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"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
|
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
Notes string `gorm:"type:text;column:notes"`
|
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
Note string `gorm:"type:text"`
|
||||||
|
|
||||||
|
// 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:Id;references:ExpenseNonstockId"`
|
Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ import (
|
|||||||
type ExpenseRealization struct {
|
type ExpenseRealization struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
ExpenseNonstockId *uint64 `gorm:""`
|
ExpenseNonstockId *uint64 `gorm:""`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null;"`
|
RealizationQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);not null;"`
|
RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
Notes string `gorm:"type:text;"`
|
RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
RealizationDate time.Time `gorm:"type:date;not null"`
|
||||||
|
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:"type:varchar(50);not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
|
Name string `gorm:"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:"type:varchar(50);size:50;not null;uniqueIndex:flags_unique_flagable"`
|
Name string `gorm:"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"`
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Initial struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
ReferenceNumber string `gorm:"type:varchar(100);not null"`
|
|
||||||
TransactionType string `gorm:"type:varchar(50);not null"`
|
|
||||||
InitialBalanceType string `gorm:"type:varchar(20);not null"`
|
|
||||||
PartyType string `gorm:"type:varchar(50);not null;index:initials_party_polymorphic,priority:1"`
|
|
||||||
PartyId uint `gorm:"not null;index:initials_party_polymorphic,priority:2"`
|
|
||||||
BankId *uint `gorm:"index"`
|
|
||||||
Direction string `gorm:"type:varchar(5);not null"`
|
|
||||||
Nominal float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
Notes string `gorm:"type:text;not null"`
|
|
||||||
CreatedBy uint `gorm:"index" json:"-"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
|
|
||||||
Bank Bank `gorm:"foreignKey:BankId;references:Id"`
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
|
|
||||||
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Kandang struct {
|
type Kandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"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,6 +20,5 @@ 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:"type:varchar(50);not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"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"`
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
type MarketingDeliveryProduct struct {
|
type MarketingDeliveryProduct struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||||
ProductWarehouseId uint `gorm:"not null"`
|
Qty float64 `gorm:"type:numeric(15,3)"`
|
||||||
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||||
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
@@ -15,10 +15,5 @@ type MarketingDeliveryProduct struct {
|
|||||||
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||||
VehicleNumber string `gorm:"type:varchar(50)"`
|
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||||
|
|
||||||
// FIFO Fields
|
|
||||||
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
|
||||||
PendingQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
|
||||||
CreatedAt *time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
|
|
||||||
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Nonstock struct {
|
type Nonstock struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"type:varchar(50);not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"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"`
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Payment struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
PaymentCode string `gorm:"type:varchar(50);not null"`
|
|
||||||
ReferenceNumber *string `gorm:"type:varchar(100)"`
|
|
||||||
TransactionType string `gorm:"type:varchar(50)"`
|
|
||||||
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
|
|
||||||
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
|
|
||||||
PaymentDate time.Time `gorm:"not null"`
|
|
||||||
PaymentMethod string `gorm:"type:varchar(20);not null"`
|
|
||||||
BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
|
|
||||||
Direction string `gorm:"type:varchar(5);not null"`
|
|
||||||
Nominal float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
Notes string `gorm:"type:text;not null"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
CreatedBy uint `gorm:"index" json:"-"`
|
|
||||||
|
|
||||||
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
|
|
||||||
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type ProductCategory struct {
|
type ProductCategory struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"type:varchar(50);not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"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:"type:varchar(50);not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
|
||||||
Brand string `gorm:"type:varchar(50);not null"`
|
Brand string `gorm:"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,12 +21,10 @@ 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,15 +1,23 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type ProductWarehouse struct {
|
type ProductWarehouse struct {
|
||||||
Id uint `gorm:"primaryKey;column:id"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
ProductId uint `gorm:"column:product_id;not null"`
|
ProductId uint `gorm:"not null"`
|
||||||
WarehouseId uint `gorm:"column:warehouse_id;not null"`
|
WarehouseId uint `gorm:"not null"`
|
||||||
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id"`
|
Quantity float64 `gorm:"default:0"`
|
||||||
Quantity float64 `gorm:"column:qty;type:numeric(15,3);default:0"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
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"`
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
StockLogs []StockLog `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProductionStandard struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
Name string `gorm:"type:varchar(100);uniqueIndex;not null"`
|
|
||||||
ProjectCategory string `gorm:"type:varchar(20);not null"`
|
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
DeletedAt *time.Time `gorm:"type:timestamptz"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
ProductionStandardDetails []ProductionStandardDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
|
||||||
StandardGrowthDetails []StandardGrowthDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProductionStandardDetail struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
ProductionStandardId uint `gorm:"not null"`
|
|
||||||
Week int `gorm:"not null"`
|
|
||||||
TargetHenDayProduction *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
TargetEggWeight *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
TargetEggMass *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
|
|
||||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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,7 +24,6 @@ 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,7 +7,6 @@ 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,18 +5,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Purchase struct {
|
type Purchase struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
PrNumber string `gorm:"not null"`
|
PrNumber string `gorm:"not null"`
|
||||||
PoNumber *string
|
PoNumber *string
|
||||||
PoDate *time.Time
|
PoDate *time.Time
|
||||||
SupplierId uint `gorm:"not null"`
|
SupplierId uint64 `gorm:"not null"`
|
||||||
CreditTerm int `gorm:"column:credit_term;not null;default:0"`
|
CreditTerm *int
|
||||||
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 uint `gorm:"not null"`
|
CreatedBy uint64 `gorm:"not null"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseItem struct {
|
type PurchaseItem struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
PurchaseId uint `gorm:"not null"`
|
PurchaseId uint64 `gorm:"not null"`
|
||||||
ProductId uint `gorm:"not null"`
|
ProductId uint64 `gorm:"not null"`
|
||||||
WarehouseId uint `gorm:"not null"`
|
WarehouseId uint64 `gorm:"not null"`
|
||||||
ProductWarehouseId *uint
|
ProductWarehouseId *uint64
|
||||||
ProjectFlockKandangId *uint
|
|
||||||
ReceivedDate *time.Time
|
ReceivedDate *time.Time
|
||||||
TravelNumber *string
|
TravelNumber *string
|
||||||
TravelNumberDocs *string
|
TravelNumberDocs *string
|
||||||
@@ -20,10 +19,8 @@ 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,11 +7,24 @@ 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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StandardGrowthDetail struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
ProductionStandardId uint `gorm:"not null"`
|
|
||||||
TargetMeanBw *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
MaxDepletion *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
MinUniformity float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
Week int `gorm:"not null"`
|
|
||||||
FeedIntake *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
|
||||||
|
|
||||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
|
||||||
}
|
|
||||||
@@ -20,5 +20,4 @@ type StockTransfer struct {
|
|||||||
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
||||||
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
||||||
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
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,20 +1,35 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogTypeAdjustment = "ADJUSTMENT"
|
||||||
|
LogTypeTransfer = "TRANSFER"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TransactionTypeIncrease = "INCREASE"
|
||||||
|
TransactionTypeDecrease = "DECREASE"
|
||||||
|
)
|
||||||
|
|
||||||
type StockLog struct {
|
type StockLog struct {
|
||||||
Id uint `gorm:"primaryKey;column:id"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
TransactionType string `gorm:"type:varchar(20);not null"`
|
||||||
CreatedBy uint `gorm:"column:created_by;not null;index"`
|
Quantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
BeforeQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
Increase float64 `gorm:"column:increase;type:numeric(15,3);default:0"`
|
AfterQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
Decrease float64 `gorm:"column:decrease;type:numeric(15,3);default:0"`
|
LogType string `gorm:"type:varchar(50);not null;index:stock_logs_flaggable_lookup,priority:1"`
|
||||||
|
LogId uint `gorm:"not null;index:stock_logs_flaggable_lookup,priority:2"`
|
||||||
LoggableType string `gorm:"column:loggable_type;type:varchar(50);not null"`
|
Note string `gorm:"type:text"`
|
||||||
LoggableId uint `gorm:"column:loggable_id;not null"`
|
ProductWarehouseId uint `gorm:"not null;index"`
|
||||||
|
CreatedBy uint `gorm:"index"`
|
||||||
Notes string `gorm:"column:notes;type:text"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
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"`
|
||||||
|
|||||||
@@ -20,5 +20,4 @@ type StockTransferDelivery struct {
|
|||||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||||
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
||||||
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
||||||
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
|
||||||
}
|
}
|
||||||
@@ -8,14 +8,14 @@ import (
|
|||||||
|
|
||||||
type Supplier struct {
|
type Supplier struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"type:varchar(50);not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"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:"type:varchar(50);not null"`
|
Pic string `gorm:"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:"type:varchar(50)"`
|
Hatchery *string `gorm:"size:255"`
|
||||||
Phone string `gorm:"not null;size:20"`
|
Phone string `gorm:"not null;size:20"`
|
||||||
Email string `gorm:"type:varchar(50);not null"`
|
Email string `gorm:"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"`
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Transaction struct {
|
|
||||||
Id uint `gorm:"primaryKey"`
|
|
||||||
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Uom struct {
|
type Uom struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"type:varchar(50);not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"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:"type:varchar(50);uniqueIndex"`
|
Email string `gorm:"uniqueIndex"`
|
||||||
Name string `gorm:"type:varchar(50);not null"`
|
Name string `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type Warehouse struct {
|
type Warehouse struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"type:varchar(50);not null"`
|
Name string `gorm:"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,13 +3,14 @@ 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 (
|
||||||
@@ -90,6 +91,7 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,15 +105,6 @@ 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
|
|
||||||
return 1, 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)
|
||||||
@@ -198,71 +191,3 @@ 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,208 +1,75 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
// project-flock
|
import (
|
||||||
const (
|
"strings"
|
||||||
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
|
||||||
P_ProjectFlockKandangsCheckClosing = "lti.production.project_flock_kandangs.closing.detail"
|
|
||||||
P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list"
|
|
||||||
P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail"
|
|
||||||
|
|
||||||
P_ProjectFlockGetAll = "lti.production.project_flocks.list"
|
"github.com/gofiber/fiber/v2"
|
||||||
P_ProjectFlockCreate = "lti.production.project_flocks.create"
|
|
||||||
P_ProjectFlockGetOne = "lti.production.project_flocks.detail"
|
|
||||||
P_ProjectFlockUpdate = "lti.production.project_flocks.update"
|
|
||||||
P_ProjectFlockDelete = "lti.production.project_flocks.delete"
|
|
||||||
P_ProjectFlockApprove = "lti.production.project_flocks.approve"
|
|
||||||
P_ProjectFlockLookup = "lti.production.project_flocks.lookup"
|
|
||||||
P_ProjectFlockNextPeriod = "lti.production.project_flocks.next_period"
|
|
||||||
P_ProjectFlockResubmit = "lti.production.project_flocks.resubmit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
||||||
P_ExpenseGetAll = "lti.expense.list"
|
func RequirePermissions(perms ...string) fiber.Handler {
|
||||||
P_ExpenseCreateOne = "lti.expense.create"
|
required := canonicalPermissions(perms)
|
||||||
P_ExpenseUpdateOne = "lti.expense.update"
|
return func(c *fiber.Ctx) error {
|
||||||
P_ExpenseGetOne = "lti.expense.detail"
|
if len(required) == 0 {
|
||||||
P_ExpenseDeleteOne = "lti.expense.delete"
|
return c.Next()
|
||||||
P_ExpenseApprovalManager = "lti.expense.approve.manager"
|
}
|
||||||
P_ExpenseApprovalFinance = "lti.expense.approve.finance"
|
|
||||||
P_ExpenseCreateRealizations = "lti.expense.create.realization"
|
|
||||||
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
|
|
||||||
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
|
|
||||||
P_ExpenseDocument = "lti.expense.document"
|
|
||||||
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
P_AdjustmentGetAll = "lti.inventory.list"
|
|
||||||
P_AdjustmentCreate = "lti.inventory.create"
|
|
||||||
P_AdjustmentGetOne = "lti.inventory.detail"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
P_ApprovalGetAll = "lti.approval.list"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
P_ReportExpenseGetAll = "lti.repport.expense.list"
|
|
||||||
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
|
|
||||||
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
ctx, ok := AuthDetails(c)
|
||||||
P_ProductStockGetAll = "lti.inventory.product_stock.list"
|
if !ok || ctx == nil {
|
||||||
P_ProductStockGetOne = "lti.inventory.product_stock.detail"
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
P_ProductWarehousekGetAll = "lti.inventory.product_warehouses.list"
|
}
|
||||||
P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
P_ClosingGetAll = "lti.closing.list"
|
|
||||||
P_ClosingDetail = "lti.closing.detail"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
userPerms := ctx.permissionSet()
|
||||||
P_TransferGetAll = "lti.inventory.transfer.list"
|
if len(userPerms) == 0 {
|
||||||
P_TransferGetOne = "lti.inventory.transfer.detail"
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
P_TransferCreateOne = "lti.inventory.transfer.create"
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
for _, perm := range required {
|
||||||
P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list"
|
if _, has := userPerms[perm]; !has {
|
||||||
P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail"
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create"
|
}
|
||||||
P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update"
|
}
|
||||||
P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete"
|
|
||||||
P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve"
|
|
||||||
P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
return c.Next()
|
||||||
P_DeliveryGetAll = "lti.marketing.delivery_order.list"
|
}
|
||||||
P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
|
}
|
||||||
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
|
|
||||||
P_DeliveryCreateOne = "lti.marketing.delivery_order.Create"
|
|
||||||
P_SalesOrderDelete = "lti.marketing.sales_order.delete"
|
|
||||||
P_SalesOrderApproval = "lti.marketing.sales_order.approve"
|
|
||||||
P_SalesOrderCreateOne = "lti.marketing.sales_order.create"
|
|
||||||
P_SalesOrderUpdateOne = "lti.marketing.sales_order.update"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
// HasPermission reports whether the current request context includes the given permission.
|
||||||
P_AreaGetAll = "lti.master.area.list"
|
func HasPermission(c *fiber.Ctx, perm string) bool {
|
||||||
P_AreaGetOne = "lti.master.area.detail"
|
ctx, ok := AuthDetails(c)
|
||||||
P_AreaCreateOne = "lti.master.area.create"
|
if !ok || ctx == nil {
|
||||||
P_AreaUpdateOne = "lti.master.area.update"
|
return false
|
||||||
P_AreaDeleteOne = "lti.master.area.delete"
|
}
|
||||||
|
perm = canonicalPermission(perm)
|
||||||
|
if perm == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, has := ctx.permissionSet()[perm]
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
P_BanksGetAll = "lti.master.banks.list"
|
func (a *AuthContext) permissionSet() map[string]struct{} {
|
||||||
P_BanksGetOne = "lti.master.banks.detail"
|
if a == nil || a.Permissions == nil {
|
||||||
P_BanksCreateOne = "lti.master.banks.create"
|
return nil
|
||||||
P_BanksUpdateOne = "lti.master.banks.update"
|
}
|
||||||
P_BanksDeleteOne = "lti.master.banks.delete"
|
return a.Permissions
|
||||||
|
}
|
||||||
|
|
||||||
P_CustomerGetAll = "lti.master.customer.list"
|
func canonicalPermissions(perms []string) []string {
|
||||||
P_CustomerGetOne = "lti.master.customer.detail"
|
out := make([]string, 0, len(perms))
|
||||||
P_CustomerCreateOne = "lti.master.customer.create"
|
seen := make(map[string]struct{}, len(perms))
|
||||||
P_CustomerUpdateOne = "lti.master.customer.update"
|
for _, perm := range perms {
|
||||||
P_CustomerDeleteOne = "lti.master.customer.delete"
|
if canonical := canonicalPermission(perm); canonical != "" {
|
||||||
|
if _, ok := seen[canonical]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[canonical] = struct{}{}
|
||||||
|
out = append(out, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
P_FcrGetAll = "lti.master.fcr.list"
|
func canonicalPermission(perm string) string {
|
||||||
P_FcrGetOne = "lti.master.fcr.detail"
|
return strings.ToLower(strings.TrimSpace(perm))
|
||||||
P_FcrCreateOne = "lti.master.fcr.create"
|
}
|
||||||
P_FcrUpdateOne = "lti.master.fcr.update"
|
|
||||||
P_FcrDeleteOne = "lti.master.fcr.delete"
|
|
||||||
|
|
||||||
P_FlocksGetAll = "lti.master.flocks.list"
|
|
||||||
P_FlocksGetOne = "lti.master.flocks.detail"
|
|
||||||
P_FlocksCreateOne = "lti.master.flocks.create"
|
|
||||||
P_FlocksUpdateOne = "lti.master.flocks.update"
|
|
||||||
P_FlocksDeleteOne = "lti.master.flocks.delete"
|
|
||||||
|
|
||||||
P_KandangsGetAll = "lti.master.kandangs.list"
|
|
||||||
P_KandangsGetOne = "lti.master.kandangs.detail"
|
|
||||||
P_KandangsCreateOne = "lti.master.kandangs.create"
|
|
||||||
P_KandangsUpdateOne = "lti.master.kandangs.update"
|
|
||||||
P_KandangsDeleteOne = "lti.master.kandangs.delete"
|
|
||||||
|
|
||||||
P_LocationsGetAll = "lti.master.locations.list"
|
|
||||||
P_LocationsGetOne = "lti.master.locations.detail"
|
|
||||||
P_LocationsCreateOne = "lti.master.locations.create"
|
|
||||||
P_LocationsUpdateOne = "lti.master.locations.update"
|
|
||||||
P_LocationsDeleteOne = "lti.master.locations.delete"
|
|
||||||
|
|
||||||
P_NonstocksGetAll = "lti.master.nonstocks.list"
|
|
||||||
P_NonstocksGetOne = "lti.master.nonstocks.detail"
|
|
||||||
P_NonstocksCreateOne = "lti.master.nonstocks.create"
|
|
||||||
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
|
|
||||||
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
|
|
||||||
|
|
||||||
P_ProductCategoriesGetAll = "lti.master.Product_categories.list"
|
|
||||||
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail"
|
|
||||||
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create"
|
|
||||||
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update"
|
|
||||||
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete"
|
|
||||||
|
|
||||||
P_ProductsGetAll = "lti.master.Products.list"
|
|
||||||
P_ProductsGetOne = "lti.master.Products.detail"
|
|
||||||
P_ProductsCreateOne = "lti.master.Products.create"
|
|
||||||
P_ProductsUpdateOne = "lti.master.Products.update"
|
|
||||||
P_ProductsDeleteOne = "lti.master.Products.delete"
|
|
||||||
|
|
||||||
P_SuppliersGetAll = "lti.master.suppliers.list"
|
|
||||||
P_SuppliersGetOne = "lti.master.suppliers.detail"
|
|
||||||
P_SuppliersCreateOne = "lti.master.suppliers.create"
|
|
||||||
P_SuppliersUpdateOne = "lti.master.suppliers.update"
|
|
||||||
P_SuppliersDeleteOne = "lti.master.suppliers.delete"
|
|
||||||
|
|
||||||
P_UomsGetAll = "lti.master.uoms.list"
|
|
||||||
P_UomsGetOne = "lti.master.uoms.detail"
|
|
||||||
P_UomsCreateOne = "lti.master.uoms.create"
|
|
||||||
P_UomsUpdateOne = "lti.master.uoms.update"
|
|
||||||
P_UomsDeleteOne = "lti.master.uoms.delete"
|
|
||||||
|
|
||||||
P_WarehousesGetAll = "lti.master.warehouses.list"
|
|
||||||
P_WarehousesGetOne = "lti.master.warehouses.detail"
|
|
||||||
P_WarehousesCreateOne = "lti.master.warehouses.create"
|
|
||||||
P_WarehousesUpdateOne = "lti.master.warehouses.update"
|
|
||||||
P_WarehousesDeleteOne = "lti.master.warehouses.delete"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
P_ChickinsCreateOne = "lti.production.chickins.create"
|
|
||||||
P_ChickinsGetOne = "lti.production.chickins.detail"
|
|
||||||
P_ChickinsApproval = "lti.production.chickins.approve"
|
|
||||||
)
|
|
||||||
|
|
||||||
// recording
|
|
||||||
const (
|
|
||||||
P_RecordingGetAll = "lti.production.recording.list"
|
|
||||||
P_RecordingGetOne = "lti.production.recording.detail"
|
|
||||||
P_RecordingCreateOne = "lti.production.recording.create"
|
|
||||||
P_RecordingUpdateOne = "lti.production.recording.update"
|
|
||||||
P_RecordingDeleteOne = "lti.production.recording.delete"
|
|
||||||
P_RecordingNextDay = "lti.production.recording.next_day"
|
|
||||||
P_RecordingApproval = "lti.production.recording.approve"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
P_PurchaseGetAll = "lti.Purchase.list"
|
|
||||||
P_PurchaseGetOne = "lti.Purchase.detail"
|
|
||||||
P_PurchaseCreateOne = "lti.Purchase.create"
|
|
||||||
P_PurchaseUpdateOne = "lti.Purchase.update"
|
|
||||||
P_PurchaseDeleteOne = "lti.Purchase.delete"
|
|
||||||
P_PurchaseItemDeleteOne = "lti.Purchase.delete.item"
|
|
||||||
P_PurchaseReceive = "lti.Purchase.receive"
|
|
||||||
P_PurchaseApprovalStaff = "lti.Purchase.approve.staff"
|
|
||||||
P_PurchaseApprovalManager = "lti.Purchase.approve.manager"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
P_FinanceGetAll = "lti.finance.list"
|
|
||||||
P_FinanceGetOne = "lti.finance.detail"
|
|
||||||
P_FinanceCreateOne = "lti.finance.create"
|
|
||||||
P_FinanceUpdateOne = "lti.finance.update"
|
|
||||||
P_FinanceDeleteOne = "lti.finance.delete"
|
|
||||||
P_FinanceApproval = "lti.finance.approve"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
P_UserGetAll = "lti.users.list"
|
|
||||||
P_UserGetOne = "lti.users.detail"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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.Use(m.Auth(u))
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll,m.RequirePermissions(m.P_ApprovalGetAll))
|
route := v1.Group("/approvals")
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,353 +0,0 @@
|
|||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingKeuangan(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.GetClosingKeuangan(c, uint(projectFlockID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get closing keuangan successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("project_flock_id")
|
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(param)
|
|
||||||
if err != nil || projectFlockID <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
var projectFlockKandangID *uint
|
|
||||||
if raw := c.Query("project_flock_kandang_id"); raw != "" {
|
|
||||||
idInt, convErr := strconv.Atoi(raw)
|
|
||||||
if convErr != nil || idInt <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
|
||||||
}
|
|
||||||
idUint := uint(idInt)
|
|
||||||
projectFlockKandangID = &idUint
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), projectFlockKandangID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get expedition HPP successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetExpeditionHPPByKandang(c *fiber.Ctx) error {
|
|
||||||
projectParam := c.Params("project_flock_id")
|
|
||||||
kandangParam := c.Params("project_flock_kandang_id")
|
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(projectParam)
|
|
||||||
if err != nil || projectFlockID <= 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
kandangID := uint(pfkID)
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), &kandangID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get expedition HPP successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingDataProduksi(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.GetClosingDataProduksi(c, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Retrieved production data successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingPurchaseDTO struct {
|
|
||||||
InitialPopulation int `json:"initial_population"`
|
|
||||||
ClaimCulling int `json:"claim_culling"`
|
|
||||||
FinalPopulation int `json:"final_population"`
|
|
||||||
FeedIn float64 `json:"feed_in"`
|
|
||||||
FeedUsed float64 `json:"feed_used"`
|
|
||||||
FeedUsedPerHead float64 `json:"feed_used_per_head"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingSalesDTO struct {
|
|
||||||
SalesPopulation int `json:"sales_population"`
|
|
||||||
SalesWeight float64 `json:"sales_weight"`
|
|
||||||
AverageWeight float64 `json:"average_weight"`
|
|
||||||
AverageSellingPrice float64 `json:"chicken_average_selling_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingEggSalesDTO struct {
|
|
||||||
EggPieces int `json:"egg_pieces"`
|
|
||||||
EggMassKg float64 `json:"egg_mass_kg"`
|
|
||||||
AverageEggWeightKg float64 `json:"average_egg_weight_kg"`
|
|
||||||
AverageSellingPrice float64 `json:"egg_average_selling_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingPerformanceDTO struct {
|
|
||||||
Depletion float64 `json:"depletion"`
|
|
||||||
Age float64 `json:"age_day"`
|
|
||||||
MortalityStd float64 `json:"mortality_std"`
|
|
||||||
MortalityAct float64 `json:"mortality_act"`
|
|
||||||
DeffMortality float64 `json:"deff_mortality"`
|
|
||||||
FcrStd float64 `json:"fcr_std"`
|
|
||||||
FcrAct float64 `json:"fcr_act"`
|
|
||||||
DeffFcr float64 `json:"deff_fcr"`
|
|
||||||
Awg float64 `json:"awg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingSalesGroupDTO struct {
|
|
||||||
Chicken ClosingSalesDTO `json:"chicken"`
|
|
||||||
Egg *ClosingEggSalesDTO `json:"egg,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingProductionReportDTO struct {
|
|
||||||
Purchase ClosingPurchaseDTO `json:"purchase"`
|
|
||||||
Sales ClosingSalesGroupDTO `json:"sales"`
|
|
||||||
Performance ClosingPerformanceDTO `json:"performance"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CalculateAgeFromChickinDataProduksi(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
|
|
||||||
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
|
|
||||||
for _, chickin := range projectFlockKandang.Chickins {
|
|
||||||
if chickin.ChickInDate.Before(earliestChickinDate) {
|
|
||||||
earliestChickinDate = chickin.ChickInDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
|
|
||||||
ageInWeeks := ageInDays / 7
|
|
||||||
return ageInWeeks
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
// ExpeditionCostItemDTO merepresentasikan biaya ekspedisi per vendor.
|
|
||||||
type ExpeditionCostItemDTO struct {
|
|
||||||
Id uint64 `json:"id"`
|
|
||||||
ExpeditionVendorName string `json:"expedition_vendor_name"`
|
|
||||||
HPPAmount float64 `json:"hpp_amount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpeditionHPPDTO adalah struktur response utama untuk HPP Ekspedisi.
|
|
||||||
type ExpeditionHPPDTO struct {
|
|
||||||
ExpeditionCosts []ExpeditionCostItemDTO `json:"expedition_costs"`
|
|
||||||
TotalHPPAmount float64 `json:"total_hpp_amount"`
|
|
||||||
}
|
|
||||||
@@ -1,568 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// === CONSTANTS ===
|
|
||||||
const (
|
|
||||||
HPPGroupPengeluaran = "HPP dan Pengeluaran"
|
|
||||||
HPPGroupBahanBaku = "HPP dan Bahan Baku"
|
|
||||||
HPPLabelOverhead = "Pengeluaran Overhead"
|
|
||||||
HPPLabelEkspedisi = "Beban Ekspedisi"
|
|
||||||
HPPSummaryLabel = "HPP"
|
|
||||||
|
|
||||||
PLSalesTypeChicken = "Penjualan Ayam Besar"
|
|
||||||
PLSalesTypeEgg = "Penjualan Telur"
|
|
||||||
|
|
||||||
PLItemTypeSapronak = "Pembelian Sapronak"
|
|
||||||
PLItemTypeOverhead = "Pengeluaran Overhead"
|
|
||||||
PLItemTypeEkspedisi = "Beban Ekspedisi"
|
|
||||||
|
|
||||||
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
|
|
||||||
PLSummaryLabelSubTotal = "SUB TOTAL"
|
|
||||||
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
|
|
||||||
|
|
||||||
PurchaseLabelPrefix = "Pembelian "
|
|
||||||
)
|
|
||||||
|
|
||||||
// === CONTEXT STRUCTS ===
|
|
||||||
|
|
||||||
type CalculationContext struct {
|
|
||||||
TotalPopulation float64
|
|
||||||
TotalWeightProduced float64
|
|
||||||
TotalDepletion float64
|
|
||||||
TotalWeightSold float64
|
|
||||||
ActualPopulation float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingKeuanganInput struct {
|
|
||||||
ProjectFlockCategory string
|
|
||||||
PurchaseItems []entities.PurchaseItem
|
|
||||||
Budgets []entities.ProjectBudget
|
|
||||||
Realizations []entities.ExpenseRealization
|
|
||||||
DeliveryProducts []entities.MarketingDeliveryProduct
|
|
||||||
Chickins []entities.ProjectChickin
|
|
||||||
TotalWeightProduced float64
|
|
||||||
TotalDepletion float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// === BASE METRICS ===
|
|
||||||
|
|
||||||
type FinancialMetrics struct {
|
|
||||||
RpPerBird float64 `json:"rp_per_bird"`
|
|
||||||
RpPerKg float64 `json:"rp_per_kg"`
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Comparison struct {
|
|
||||||
Budgeting FinancialMetrics `json:"budgeting"`
|
|
||||||
Realization FinancialMetrics `json:"realization"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP PURCHASES PACKAGE ===
|
|
||||||
|
|
||||||
type HppItem struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Comparison
|
|
||||||
}
|
|
||||||
|
|
||||||
type HppGroup struct {
|
|
||||||
GroupName string `json:"group_name"`
|
|
||||||
Data []HppItem `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SummaryHpp struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
Comparison
|
|
||||||
}
|
|
||||||
|
|
||||||
type HppPurchasesSection struct {
|
|
||||||
Hpp []HppGroup `json:"hpp"`
|
|
||||||
SummaryHpp SummaryHpp `json:"summary_hpp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === PROFIT LOSS PACKAGE ===
|
|
||||||
|
|
||||||
type PLItem struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
FinancialMetrics
|
|
||||||
}
|
|
||||||
|
|
||||||
type PLSummaryItem struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
FinancialMetrics
|
|
||||||
}
|
|
||||||
|
|
||||||
type PLSummaryGroup struct {
|
|
||||||
GrossProfit PLSummaryItem `json:"gross_profit"`
|
|
||||||
SubTotal PLSummaryItem `json:"sub_total"`
|
|
||||||
NetProfit PLSummaryItem `json:"net_profit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfitLossData struct {
|
|
||||||
Penjualan []PLItem `json:"penjualan"`
|
|
||||||
Pembelian []PLItem `json:"pembelian"`
|
|
||||||
Overhead PLItem `json:"overhead"`
|
|
||||||
Ekspedisi PLItem `json:"ekspedisi"`
|
|
||||||
Summary PLSummaryGroup `json:"summary"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfitLossSection struct {
|
|
||||||
Data ProfitLossData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === RESPONSE DTO (ROOT) ===
|
|
||||||
|
|
||||||
type ReportResponse struct {
|
|
||||||
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
|
|
||||||
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === MAPPER FUNCTIONS ===
|
|
||||||
|
|
||||||
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
|
||||||
return FinancialMetrics{
|
|
||||||
RpPerBird: rpPerBird,
|
|
||||||
RpPerKg: rpPerKg,
|
|
||||||
Amount: amount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
|
|
||||||
return Comparison{
|
|
||||||
Budgeting: budgeting,
|
|
||||||
Realization: realization,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP PENGELUARAN (from Purchase Items) ===
|
|
||||||
|
|
||||||
func getFlagLabel(flagType utils.FlagType) string {
|
|
||||||
return PurchaseLabelPrefix + string(flagType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
|
|
||||||
flags := []utils.FlagType{
|
|
||||||
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
|
|
||||||
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
|
|
||||||
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
|
|
||||||
}
|
|
||||||
|
|
||||||
items := []HppItem{}
|
|
||||||
seenFlags := make(map[utils.FlagType]bool)
|
|
||||||
|
|
||||||
for _, item := range purchaseItems {
|
|
||||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, flag := range item.Product.Flags {
|
|
||||||
flagType := utils.FlagType(flag.Name)
|
|
||||||
|
|
||||||
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
|
|
||||||
amount := sumPurchasesByFlag(purchaseItems, flagType)
|
|
||||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
items = append(items, HppItem{
|
|
||||||
Type: getFlagLabel(flagType),
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
|
||||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
seenFlags[flagType] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
|
|
||||||
|
|
||||||
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
|
|
||||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
return HppItem{
|
|
||||||
Type: HPPLabelOverhead,
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
|
|
||||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
|
|
||||||
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
return HppItem{
|
|
||||||
Type: HPPLabelEkspedisi,
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
|
||||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
|
|
||||||
items := []HppItem{}
|
|
||||||
|
|
||||||
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
|
||||||
realizationAmount := getOperationalExpenses(realizations)
|
|
||||||
|
|
||||||
if budgetAmount > 0 || realizationAmount > 0 {
|
|
||||||
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
|
||||||
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
|
|
||||||
|
|
||||||
return HppGroup{
|
|
||||||
GroupName: HPPGroupBahanBaku,
|
|
||||||
Data: items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP SUMMARY ===
|
|
||||||
|
|
||||||
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) SummaryHpp {
|
|
||||||
purchaseTotal := sumPurchaseTotal(purchaseItems)
|
|
||||||
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
|
||||||
totalBudget := purchaseTotal + budgetTotal
|
|
||||||
|
|
||||||
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
|
|
||||||
|
|
||||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
return SummaryHpp{
|
|
||||||
Label: label,
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
|
||||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppPurchasesSection {
|
|
||||||
hppGroups := []HppGroup{
|
|
||||||
{
|
|
||||||
GroupName: HPPGroupPengeluaran,
|
|
||||||
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
|
|
||||||
},
|
|
||||||
ToHppBahanBakuGroup(budgets, realizations, ctx),
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, ctx)
|
|
||||||
|
|
||||||
return HppPurchasesSection{
|
|
||||||
Hpp: hppGroups,
|
|
||||||
SummaryHpp: summaryHpp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === PROFIT & LOSS ===
|
|
||||||
|
|
||||||
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
|
|
||||||
return PLItem{
|
|
||||||
Type: itemType,
|
|
||||||
FinancialMetrics: metrics,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
|
|
||||||
return PLSummaryItem{
|
|
||||||
Label: label,
|
|
||||||
FinancialMetrics: metrics,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem {
|
|
||||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced)
|
|
||||||
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
|
|
||||||
for _, item := range items {
|
|
||||||
totalAmount += item.Amount
|
|
||||||
totalPerBird += item.RpPerBird
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem {
|
|
||||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold)
|
|
||||||
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem {
|
|
||||||
items := []PLItem{}
|
|
||||||
|
|
||||||
categorized := categorizeDeliveriesBySalesType(deliveryProducts)
|
|
||||||
|
|
||||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
|
|
||||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
|
||||||
telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg])
|
|
||||||
|
|
||||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
|
||||||
items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx))
|
|
||||||
} else {
|
|
||||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
|
||||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
|
||||||
purchaseAmount := sumPurchaseTotal(purchases)
|
|
||||||
bopAmount := getOperationalExpenses(realizations)
|
|
||||||
totalCost := purchaseAmount + bopAmount
|
|
||||||
|
|
||||||
return []PLItem{
|
|
||||||
createPLItemWithMetrics(PLItemTypeSapronak, totalCost, ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
|
||||||
realizationAmount := getOperationalExpenses(realizations)
|
|
||||||
return []PLItem{
|
|
||||||
createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
|
||||||
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
|
||||||
return []PLItem{
|
|
||||||
createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup {
|
|
||||||
totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems)
|
|
||||||
totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems)
|
|
||||||
totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems)
|
|
||||||
totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems)
|
|
||||||
|
|
||||||
grossProfit := totalPenjualan - totalPembelian
|
|
||||||
grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird
|
|
||||||
|
|
||||||
totalOtherExpenses := totalOverhead + totalEkspedisi
|
|
||||||
totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird
|
|
||||||
|
|
||||||
netProfit := grossProfit - totalOtherExpenses
|
|
||||||
netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird
|
|
||||||
|
|
||||||
return PLSummaryGroup{
|
|
||||||
GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)),
|
|
||||||
SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)),
|
|
||||||
NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData {
|
|
||||||
summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
|
||||||
|
|
||||||
totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead)
|
|
||||||
totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi)
|
|
||||||
|
|
||||||
return ProfitLossData{
|
|
||||||
Penjualan: penjualanItems,
|
|
||||||
Pembelian: pembelianItems,
|
|
||||||
Overhead: totalOverhead,
|
|
||||||
Ekspedisi: totalEkspedisi,
|
|
||||||
Summary: summary,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
|
|
||||||
return ProfitLossSection{
|
|
||||||
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func aggregatePLItems(items []PLItem, label string) PLItem {
|
|
||||||
totalAmount, totalPerBird := sumPLItems(items)
|
|
||||||
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
|
|
||||||
return ReportResponse{
|
|
||||||
HppPurchases: hppPurchases,
|
|
||||||
ProfitLoss: profitLoss,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
|
|
||||||
var totalPopulation float64
|
|
||||||
var totalWeightSold float64
|
|
||||||
|
|
||||||
for _, chickin := range input.Chickins {
|
|
||||||
totalPopulation += chickin.UsageQty
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, delivery := range input.DeliveryProducts {
|
|
||||||
totalWeightSold += delivery.TotalWeight
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := CalculationContext{
|
|
||||||
TotalPopulation: totalPopulation,
|
|
||||||
TotalWeightProduced: input.TotalWeightProduced,
|
|
||||||
TotalDepletion: input.TotalDepletion,
|
|
||||||
TotalWeightSold: totalWeightSold,
|
|
||||||
ActualPopulation: totalPopulation - input.TotalDepletion,
|
|
||||||
}
|
|
||||||
|
|
||||||
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, ctx)
|
|
||||||
penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx)
|
|
||||||
pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx)
|
|
||||||
overheadItems := ToOverheadItems(input.Realizations, ctx)
|
|
||||||
ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx)
|
|
||||||
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
|
||||||
|
|
||||||
return ToReportResponse(hppSection, plSection)
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HELPER FUNCTIONS ===
|
|
||||||
|
|
||||||
func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) {
|
|
||||||
if totalPopulation > 0 {
|
|
||||||
rpPerBird = amount / totalPopulation
|
|
||||||
}
|
|
||||||
if totalWeightSold > 0 {
|
|
||||||
rpPerKg = amount / totalWeightSold
|
|
||||||
}
|
|
||||||
return rpPerBird, rpPerKg
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool {
|
|
||||||
for _, flag := range flags {
|
|
||||||
if strings.ToUpper(flag.Name) == string(flagType) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool {
|
|
||||||
return func(item *entities.PurchaseItem) bool {
|
|
||||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasProductFlag(item.Product.Flags, flagType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
|
||||||
return func(realization *entities.ExpenseRealization) bool {
|
|
||||||
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
|
||||||
hasFlag := filterRealizationByNonstockFlag(flagType)
|
|
||||||
return func(realization *entities.ExpenseRealization) bool {
|
|
||||||
return !hasFlag(realization)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 {
|
|
||||||
amount := 0.0
|
|
||||||
for i := range items {
|
|
||||||
if filter(&items[i]) {
|
|
||||||
amount += extractor(&items[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return amount
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 {
|
|
||||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 {
|
|
||||||
return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType))
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 {
|
|
||||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true })
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 {
|
|
||||||
return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 {
|
|
||||||
return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 {
|
|
||||||
return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
|
|
||||||
}
|
|
||||||
|
|
||||||
func isChickenProductFlag(flagType utils.FlagType) bool {
|
|
||||||
switch flagType {
|
|
||||||
case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer,
|
|
||||||
utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEggProductFlag(flagType utils.FlagType) bool {
|
|
||||||
switch flagType {
|
|
||||||
case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah,
|
|
||||||
utils.FlagTelurPutih, utils.FlagTelurRetak:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSalesTypeFromProductFlags(product *entities.Product) string {
|
|
||||||
if product == nil || len(product.Flags) == 0 {
|
|
||||||
return PLSalesTypeChicken
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, flag := range product.Flags {
|
|
||||||
flagType := utils.FlagType(strings.ToUpper(flag.Name))
|
|
||||||
|
|
||||||
if isEggProductFlag(flagType) {
|
|
||||||
return PLSalesTypeEgg
|
|
||||||
}
|
|
||||||
if isChickenProductFlag(flagType) {
|
|
||||||
return PLSalesTypeChicken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PLSalesTypeChicken
|
|
||||||
}
|
|
||||||
|
|
||||||
func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct {
|
|
||||||
categorized := make(map[string][]entities.MarketingDeliveryProduct)
|
|
||||||
|
|
||||||
for _, delivery := range deliveries {
|
|
||||||
product := delivery.MarketingProduct.ProductWarehouse.Product
|
|
||||||
salesType := getSalesTypeFromProductFlags(&product)
|
|
||||||
|
|
||||||
categorized[salesType] = append(categorized[salesType], delivery)
|
|
||||||
}
|
|
||||||
|
|
||||||
return categorized
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 {
|
|
||||||
amount := 0.0
|
|
||||||
for _, delivery := range deliveries {
|
|
||||||
amount += delivery.TotalPrice
|
|
||||||
}
|
|
||||||
return amount
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
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 {
|
|
||||||
Sales []SalesDTO `json:"sales"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions ===
|
|
||||||
|
|
||||||
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
|
||||||
|
|
||||||
age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate)
|
|
||||||
|
|
||||||
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.UsageQty, // Show allocated quantity from FIFO
|
|
||||||
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 {
|
|
||||||
|
|
||||||
return PenjualanRealisasiResponseDTO{
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
|
|
||||||
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
|
|
||||||
for _, chickin := range projectFlockKandang.Chickins {
|
|
||||||
if chickin.ChickInDate.Before(earliestChickinDate) {
|
|
||||||
earliestChickinDate = chickin.ChickInDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
|
|
||||||
ageInWeeks := ageInDays / 7
|
|
||||||
return ageInWeeks
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user