mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf7b3418a5 | |||
| f60564d673 | |||
| 3ada837b8b | |||
| 4ce7611c26 | |||
| e98d0a9fa1 | |||
| 08c8c4a747 | |||
| de6304332b | |||
| f073bcc2c1 | |||
| 4853891191 | |||
| 086184bbaa | |||
| 4161dcfbdd | |||
| d0309f25dd | |||
| 59ebe29ec8 | |||
| 2b6ba3a41d | |||
| bb1e6833f0 | |||
| a536094481 | |||
| c33cc05f72 | |||
| 3f9865d267 | |||
| 822ca0268e | |||
| 16d1358b3a | |||
| e00f168a15 | |||
| 79d488c979 | |||
| 576f8083a3 | |||
| d7c543bc9d | |||
| 4a2a80916f | |||
| 511e5501bb | |||
| 0fbf04fc1d | |||
| 536e76d481 | |||
| 29aa737422 | |||
| 26f2f3ccbf | |||
| 4b147a3be7 | |||
| 7094d90034 | |||
| 347f21b45c | |||
| 89b23b0653 | |||
| e2a6c2a733 | |||
| e0e2d91db5 | |||
| 6e176688fa | |||
| cbb7f45c5f | |||
| fc9197d00a | |||
| f8e0614d50 | |||
| a8434a5246 | |||
| 9f239b1840 | |||
| 167fd6d6cb | |||
| ec2aca936c | |||
| f701b30cb3 | |||
| 0a18753dde | |||
| 4638fba318 | |||
| 296e8e4c18 | |||
| a586fe3781 | |||
| 2d3f7f7ef9 | |||
| 67f7ec3a40 | |||
| 2fbf66f9f7 | |||
| 70b2a5a2d1 | |||
| 008709c19c | |||
| 6572176cca | |||
| 4c63bd14c3 | |||
| c593df661c | |||
| ee2db748ea | |||
| 5afee298b0 | |||
| 2bc67a8433 | |||
| b4ccd33ea0 | |||
| c279303b99 | |||
| b8a769dc72 | |||
| 8c883669d3 | |||
| 1e9a637202 | |||
| fc14f9a98f | |||
| 17269d701c | |||
| c3305d3089 | |||
| b43e2b44ec | |||
| 6e3a8f3551 | |||
| c064fb1765 | |||
| 4af631a1d3 | |||
| 91e4762945 | |||
| b8403f1c7e | |||
| 415d5c0e67 | |||
| 1bca29cd31 | |||
| 753d8575c4 | |||
| 4c5266da23 | |||
| d79b1868fc | |||
| 33a9d7806e | |||
| ea294c6a18 | |||
| d572d04e3b | |||
| 730fb22cc2 | |||
| 94fc9219af | |||
| beee88322a | |||
| 1b464884c5 | |||
| 31699f4162 | |||
| e667d88218 | |||
| 002981e63b | |||
| 1d0ef8fb93 | |||
| d76db26a4d | |||
| 29f0fd6edb |
@@ -4,11 +4,17 @@ go 1.23
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0
|
github.com/MicahParks/keyfunc/v2 v2.1.0
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.40.0
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.2
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1
|
||||||
github.com/bytedance/sonic v1.12.1
|
github.com/bytedance/sonic v1.12.1
|
||||||
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
github.com/gofiber/contrib/jwt v1.0.10
|
github.com/gofiber/contrib/jwt v1.0.10
|
||||||
github.com/gofiber/fiber/v2 v2.52.5
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgconn v1.14.1
|
github.com/jackc/pgconn v1.14.1
|
||||||
github.com/redis/go-redis/v9 v9.14.0
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
@@ -20,17 +26,33 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
|
||||||
|
github.com/aws/smithy-go v1.23.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
github.com/jackc/pgio v1.0.0 // indirect
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
@@ -51,6 +73,7 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/philhofer/fwd v1.1.2 // indirect
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
@@ -75,4 +98,8 @@ require (
|
|||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.22.5 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
modernc.org/sqlite v1.23.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,44 @@ github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+G
|
|||||||
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
|
||||||
|
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||||
|
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
@@ -27,12 +65,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||||
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -50,6 +94,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
@@ -146,6 +192,9 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
@@ -306,4 +355,12 @@ gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
|||||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||||
|
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||||
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||||
|
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
|
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||||
|
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package capabilities
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
permission "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
||||||
@@ -37,8 +37,8 @@ func normalizeAndAllow(perm string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allowed = map[string]struct{}{
|
var allowed = map[string]struct{}{
|
||||||
recordings.PermissionRecordingRead: {},
|
permission.PermissionRecordingRead: {},
|
||||||
recordings.PermissionRecordingCreate: {},
|
permission.PermissionRecordingCreate: {},
|
||||||
recordings.PermissionRecordingUpdate: {},
|
permission.PermissionRecordingUpdate: {},
|
||||||
recordings.PermissionRecordingDelete: {},
|
permission.PermissionRecordingDelete: {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentRepository interface {
|
||||||
|
BaseRepository[entity.Document]
|
||||||
|
ListByTarget(ctx context.Context, documentableType string, documentableID uint64, modifier func(*gorm.DB) *gorm.DB) ([]entity.Document, error)
|
||||||
|
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type documentRepositoryImpl struct {
|
||||||
|
*BaseRepositoryImpl[entity.Document]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentRepository(db *gorm.DB) DocumentRepository {
|
||||||
|
return &documentRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: NewBaseRepository[entity.Document](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *documentRepositoryImpl) ListByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
documentableType string,
|
||||||
|
documentableID uint64,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) ([]entity.Document, error) {
|
||||||
|
var documents []entity.Document
|
||||||
|
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Where("documentable_type = ? AND documentable_id = ?", documentableType, documentableID)
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Order("created_at ASC").Find(&documents).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return documents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *documentRepositoryImpl) DeleteByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
documentableType string,
|
||||||
|
documentableID uint64,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Where("documentable_type = ? AND documentable_id = ?", documentableType, documentableID)
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Delete(&entity.Document{}).Error
|
||||||
|
}
|
||||||
@@ -63,13 +63,14 @@ func (r *StockAllocationRepositoryImpl) ReleaseByUsable(
|
|||||||
updates["note"] = *note
|
updates["note"] = *note
|
||||||
}
|
}
|
||||||
|
|
||||||
q := r.DB().WithContext(ctx).
|
baseDB := r.DB()
|
||||||
|
if modifier != nil {
|
||||||
|
baseDB = modifier(baseDB)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := baseDB.WithContext(ctx).
|
||||||
Model(&entity.StockAllocation{}).
|
Model(&entity.StockAllocation{}).
|
||||||
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
|
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
|
||||||
|
|
||||||
if modifier != nil {
|
|
||||||
q = modifier(q)
|
|
||||||
}
|
|
||||||
|
|
||||||
return q.Updates(updates).Error
|
return q.Updates(updates).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dipakai untuk semua module yang butuh cek:
|
||||||
|
// "PW ini → warehouse → kandang → project_flock_kandang sudah closing atau belum"
|
||||||
|
func EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
productWarehouseIDs []uint,
|
||||||
|
) error {
|
||||||
|
if len(productWarehouseIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(db)
|
||||||
|
wRepo := warehouseRepo.NewWarehouseRepository(db)
|
||||||
|
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
|
seenPW := make(map[uint]struct{})
|
||||||
|
seenKandang := make(map[uint]struct{})
|
||||||
|
|
||||||
|
for _, pwID := range productWarehouseIDs {
|
||||||
|
if pwID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seenPW[pwID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenPW[pwID] = struct{}{}
|
||||||
|
|
||||||
|
pw, err := pwRepo.GetByID(ctx, pwID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Product warehouse %d tidak ditemukan", pwID))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
wh, err := wRepo.GetByID(ctx, uint(pw.WarehouseId), nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Warehouse %d tidak ditemukan", pw.WarehouseId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warehouse tanpa kandang → bukan kandang produksi → skip
|
||||||
|
if wh.KandangId == nil || *wh.KandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := uint(*wh.KandangId)
|
||||||
|
if _, ok := seenKandang[kandangID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenKandang[kandangID] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := pfkRepo.GetActiveByKandangID(ctx, kandangID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// nggak ada project aktif untuk kandang ini → aman
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
// INTI RULE: kalau aktif tapi sudah punya ClosedAt → anggap "project sudah closing"
|
||||||
|
if pfk != nil && pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureProjectFlockNotClosedByProjectFlockKandangID(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
pfkIDs []uint,
|
||||||
|
) error {
|
||||||
|
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
|
seen := make(map[uint]struct{})
|
||||||
|
for _, id := range pfkIDs {
|
||||||
|
if id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := pfkRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Project flock kandang %d tidak ditemukan", id))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultDocumentPathLimit = 50
|
||||||
|
defaultDocumentKeyPrefix = "docs"
|
||||||
|
maxDocumentNameLength = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentService interface {
|
||||||
|
UploadDocuments(ctx context.Context, req DocumentUploadRequest) ([]DocumentUploadResult, error)
|
||||||
|
ListByTarget(ctx context.Context, documentableType string, documentableID uint64) ([]entity.Document, error)
|
||||||
|
DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error
|
||||||
|
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error
|
||||||
|
PublicURL(document entity.Document) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentUploadRequest struct {
|
||||||
|
DocumentableType string
|
||||||
|
DocumentableID uint64
|
||||||
|
CreatedBy *uint
|
||||||
|
Files []DocumentFile
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentFile struct {
|
||||||
|
File *multipart.FileHeader
|
||||||
|
Type string
|
||||||
|
Index *int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentUploadResult struct {
|
||||||
|
Document entity.Document
|
||||||
|
URL string
|
||||||
|
Index *int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentServiceOption func(*documentService)
|
||||||
|
|
||||||
|
type documentService struct {
|
||||||
|
repo commonRepo.DocumentRepository
|
||||||
|
storage DocumentStorage
|
||||||
|
keyPrefix string
|
||||||
|
maxPathLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentService(repo commonRepo.DocumentRepository, storage DocumentStorage, opts ...DocumentServiceOption) DocumentService {
|
||||||
|
svc := &documentService{
|
||||||
|
repo: repo,
|
||||||
|
storage: storage,
|
||||||
|
keyPrefix: defaultDocumentKeyPrefix,
|
||||||
|
maxPathLength: defaultDocumentPathLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(svc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentServiceFromConfig(ctx context.Context, repo commonRepo.DocumentRepository) (DocumentService, error) {
|
||||||
|
if repo == nil {
|
||||||
|
return nil, errors.New("document repository is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(config.S3Bucket) == "" {
|
||||||
|
return nil, errors.New("S3_BUCKET is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err := NewS3DocumentStorage(ctx, S3DocumentStorageConfig{
|
||||||
|
Region: config.S3Region,
|
||||||
|
Bucket: config.S3Bucket,
|
||||||
|
AccessKey: config.S3AccessKey,
|
||||||
|
SecretKey: config.S3SecretKey,
|
||||||
|
Endpoint: config.S3Endpoint,
|
||||||
|
BaseURL: config.S3PublicBaseURL,
|
||||||
|
ForcePathStyle: config.S3ForcePathStyle,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := config.S3DocumentKeyPrefix
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = defaultDocumentKeyPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewDocumentService(
|
||||||
|
repo,
|
||||||
|
storage,
|
||||||
|
WithDocumentKeyPrefix(prefix),
|
||||||
|
WithDocumentPathLimit(defaultDocumentPathLimit),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDocumentKeyPrefix(prefix string) DocumentServiceOption {
|
||||||
|
return func(svc *documentService) {
|
||||||
|
prefix = strings.Trim(prefix, "/")
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = defaultDocumentKeyPrefix
|
||||||
|
}
|
||||||
|
svc.keyPrefix = prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDocumentPathLimit(limit int) DocumentServiceOption {
|
||||||
|
return func(svc *documentService) {
|
||||||
|
if limit > 0 {
|
||||||
|
svc.maxPathLength = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) UploadDocuments(ctx context.Context, req DocumentUploadRequest) ([]DocumentUploadResult, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
if s.storage == nil {
|
||||||
|
return nil, errors.New("document storage not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentableType := strings.ToUpper(strings.TrimSpace(req.DocumentableType))
|
||||||
|
if documentableType == "" {
|
||||||
|
return nil, errors.New("documentable type is required")
|
||||||
|
}
|
||||||
|
if req.DocumentableID == 0 {
|
||||||
|
return nil, errors.New("documentable id is required")
|
||||||
|
}
|
||||||
|
if len(req.Files) == 0 {
|
||||||
|
return nil, errors.New("no files to upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdBy *uint
|
||||||
|
if req.CreatedBy != nil && *req.CreatedBy != 0 {
|
||||||
|
idCopy := *req.CreatedBy
|
||||||
|
createdBy = &idCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]DocumentUploadResult, 0, len(req.Files))
|
||||||
|
createdDocs := make([]entity.Document, 0, len(req.Files))
|
||||||
|
|
||||||
|
for _, file := range req.Files {
|
||||||
|
if file.File == nil {
|
||||||
|
return nil, errors.New("file header is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
originalName := sanitizeDocumentName(file.File.Filename)
|
||||||
|
contentType := detectContentType(file.File, originalName)
|
||||||
|
ext := detectExtension(file.File.Filename, contentType)
|
||||||
|
key, err := s.generateObjectKey(ext)
|
||||||
|
if err != nil {
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := file.File.Open()
|
||||||
|
if err != nil {
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uploadRes, err := s.storage.Upload(ctx, key, reader, file.File.Size, contentType)
|
||||||
|
_ = reader.Close()
|
||||||
|
if err != nil {
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
docType := resolveDocumentType(file.Type, documentableType)
|
||||||
|
doc := entity.Document{
|
||||||
|
DocumentableType: documentableType,
|
||||||
|
DocumentableId: req.DocumentableID,
|
||||||
|
Type: docType,
|
||||||
|
Path: uploadRes.Key,
|
||||||
|
Name: originalName,
|
||||||
|
Ext: strings.TrimPrefix(ext, "."),
|
||||||
|
Size: float64(file.File.Size),
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.CreateOne(ctx, &doc, nil); err != nil {
|
||||||
|
_ = s.storage.Delete(ctx, uploadRes.Key)
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdDocs = append(createdDocs, doc)
|
||||||
|
results = append(results, DocumentUploadResult{
|
||||||
|
Document: doc,
|
||||||
|
URL: uploadRes.URL,
|
||||||
|
Index: cloneIndex(file.Index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) ListByTarget(ctx context.Context, documentableType string, documentableID uint64) ([]entity.Document, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentableType = strings.ToUpper(strings.TrimSpace(documentableType))
|
||||||
|
if documentableType == "" {
|
||||||
|
return nil, errors.New("documentable type is required")
|
||||||
|
}
|
||||||
|
if documentableID == 0 {
|
||||||
|
return nil, errors.New("documentable id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.ListByTarget(ctx, documentableType, documentableID, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error {
|
||||||
|
if s.repo == nil {
|
||||||
|
return errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
docs, err := s.repo.GetByIDs(ctx, ids, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, doc := range docs {
|
||||||
|
if err := s.repo.DeleteOne(ctx, doc.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if removeFromStorage && s.storage != nil {
|
||||||
|
if err := s.storage.Delete(ctx, doc.Path); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to delete document object %s", doc.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error {
|
||||||
|
if s.repo == nil {
|
||||||
|
return errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentableType = strings.ToUpper(strings.TrimSpace(documentableType))
|
||||||
|
if documentableType == "" || documentableID == 0 {
|
||||||
|
return errors.New("documentable type and id are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var docs []entity.Document
|
||||||
|
if removeFromStorage && s.storage != nil {
|
||||||
|
var err error
|
||||||
|
docs, err = s.repo.ListByTarget(ctx, documentableType, documentableID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.DeleteByTarget(ctx, documentableType, documentableID, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if removeFromStorage && len(docs) > 0 {
|
||||||
|
for _, doc := range docs {
|
||||||
|
if err := s.storage.Delete(ctx, doc.Path); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to delete document object %s", doc.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) PublicURL(document entity.Document) string {
|
||||||
|
if s.storage == nil || strings.TrimSpace(document.Path) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s.storage.URL(document.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) generateObjectKey(ext string) (string, error) {
|
||||||
|
normalizedExt := strings.TrimSpace(ext)
|
||||||
|
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
|
||||||
|
normalizedExt = "." + normalizedExt
|
||||||
|
}
|
||||||
|
|
||||||
|
u := uuid.New().String()
|
||||||
|
key := fmt.Sprintf("%s/%s%s", strings.Trim(s.keyPrefix, "/"), u, normalizedExt)
|
||||||
|
if s.keyPrefix == "" {
|
||||||
|
key = fmt.Sprintf("%s%s", u, normalizedExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) > s.maxPathLength {
|
||||||
|
key = fmt.Sprintf("%s%s", u, normalizedExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) > s.maxPathLength {
|
||||||
|
return "", fmt.Errorf("object key exceeds maximum length (%d)", s.maxPathLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) rollbackDocuments(ctx context.Context, docs []entity.Document) {
|
||||||
|
if len(docs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(docs) - 1; i >= 0; i-- {
|
||||||
|
doc := docs[i]
|
||||||
|
if s.repo != nil && doc.Id != 0 {
|
||||||
|
if err := s.repo.DeleteOne(ctx, doc.Id); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to rollback document #%d", doc.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.storage != nil && strings.TrimSpace(doc.Path) != "" {
|
||||||
|
if err := s.storage.Delete(ctx, doc.Path); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to rollback document object %s", doc.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeDocumentName(name string) string {
|
||||||
|
name = filepath.Base(strings.TrimSpace(name))
|
||||||
|
if name == "." || name == "" {
|
||||||
|
name = "document"
|
||||||
|
}
|
||||||
|
name = strings.Map(func(r rune) rune {
|
||||||
|
if r < 32 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case '\\', '/', ':', '*', '?', '"', '<', '>', '|':
|
||||||
|
return '-'
|
||||||
|
default:
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}, name)
|
||||||
|
|
||||||
|
if len(name) > maxDocumentNameLength {
|
||||||
|
runes := []rune(name)
|
||||||
|
if len(runes) > maxDocumentNameLength {
|
||||||
|
name = string(runes[:maxDocumentNameLength])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectExtension(filename, contentType string) string {
|
||||||
|
ext := strings.ToLower(strings.TrimSpace(filepath.Ext(filename)))
|
||||||
|
if ext == "" && contentType != "" {
|
||||||
|
if exts, _ := mime.ExtensionsByType(contentType); len(exts) > 0 {
|
||||||
|
ext = exts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ext == "" {
|
||||||
|
return ".bin"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(ext, ".") {
|
||||||
|
ext = "." + ext
|
||||||
|
}
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectContentType(file *multipart.FileHeader, filename string) string {
|
||||||
|
if file == nil {
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
contentType := strings.TrimSpace(file.Header.Get("Content-Type"))
|
||||||
|
if contentType != "" {
|
||||||
|
return contentType
|
||||||
|
}
|
||||||
|
if ext := filepath.Ext(filename); ext != "" {
|
||||||
|
if guess := mime.TypeByExtension(ext); guess != "" {
|
||||||
|
return guess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDocumentType(fileType, fallback string) string {
|
||||||
|
value := strings.ToUpper(strings.TrimSpace(fileType))
|
||||||
|
if value == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneIndex(index *int) *int {
|
||||||
|
if index == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := *index
|
||||||
|
return &value
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDocumentServiceUpload(t *testing.T) {
|
||||||
|
if strings.TrimSpace(config.S3Bucket) == "" {
|
||||||
|
t.Fatal("S3 bucket is not configured; set S3_* env vars to run this test")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
db := setupDocumentTestDB(t)
|
||||||
|
repo := commonRepo.NewDocumentRepository(db)
|
||||||
|
|
||||||
|
svc, err := NewDocumentServiceFromConfig(ctx, repo)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create document service from config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := newTestFileHeader(t, "integration-proof.txt", "text/plain", []byte("document integration test"))
|
||||||
|
userID := uint(100)
|
||||||
|
|
||||||
|
results, err := svc.UploadDocuments(ctx, DocumentUploadRequest{
|
||||||
|
DocumentableType: "INVENTORY_TRANSFER",
|
||||||
|
DocumentableID: 99,
|
||||||
|
CreatedBy: &userID,
|
||||||
|
Files: []DocumentFile{
|
||||||
|
{File: file, Type: "integration"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("upload to S3 failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("expected 1 uploaded document, got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := results[0].Document
|
||||||
|
if doc.Path == "" {
|
||||||
|
t.Fatalf("expected non-empty storage path")
|
||||||
|
}
|
||||||
|
if results[0].URL == "" {
|
||||||
|
t.Fatalf("expected public URL for uploaded document")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("uploaded document #%d to %s (path=%s)", doc.Id, results[0].URL, doc.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDocumentTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
if strings.TrimSpace(config.DBHost) == "" || strings.TrimSpace(config.DBName) == "" {
|
||||||
|
t.Fatal("database configuration missing; ensure DB_HOST and DB_NAME are set")
|
||||||
|
}
|
||||||
|
db := database.Connect(config.DBHost, config.DBName)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatal("failed to create database connection")
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&entity.Document{}); err != nil {
|
||||||
|
t.Fatalf("failed to migrate document table: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestFileHeader(t *testing.T, filename, contentType string, data []byte) *multipart.FileHeader {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, err := writer.CreateFormFile("documents", filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create form file: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := part.Write(data); err != nil {
|
||||||
|
t.Fatalf("failed to write file data: %v", err)
|
||||||
|
}
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close writer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://example.com/upload", body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
_, fileHeader, err := req.FormFile("documents")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse form file: %v", err)
|
||||||
|
}
|
||||||
|
fileHeader.Header.Set("Content-Type", contentType)
|
||||||
|
return fileHeader
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentStorage interface {
|
||||||
|
Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error)
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
URL(key string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentStorageUploadResult struct {
|
||||||
|
Key string
|
||||||
|
URL string
|
||||||
|
ETag string
|
||||||
|
}
|
||||||
|
|
||||||
|
type S3DocumentStorageConfig struct {
|
||||||
|
Region string
|
||||||
|
Bucket string
|
||||||
|
AccessKey string
|
||||||
|
SecretKey string
|
||||||
|
Endpoint string
|
||||||
|
BaseURL string
|
||||||
|
ForcePathStyle bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type s3DocumentStorage struct {
|
||||||
|
client *s3.Client
|
||||||
|
bucket string
|
||||||
|
base string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (DocumentStorage, error) {
|
||||||
|
bucket := strings.TrimSpace(cfg.Bucket)
|
||||||
|
if bucket == "" {
|
||||||
|
return nil, errors.New("s3 bucket is required")
|
||||||
|
}
|
||||||
|
region := strings.TrimSpace(cfg.Region)
|
||||||
|
if region == "" {
|
||||||
|
region = "us-east-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []func(*awsconfig.LoadOptions) error{
|
||||||
|
awsconfig.WithRegion(region),
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := strings.TrimSpace(cfg.Endpoint)
|
||||||
|
if endpoint != "" {
|
||||||
|
resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) {
|
||||||
|
if service == s3.ServiceID {
|
||||||
|
return aws.Endpoint{
|
||||||
|
URL: endpoint,
|
||||||
|
SigningRegion: region,
|
||||||
|
HostnameImmutable: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
|
||||||
|
})
|
||||||
|
options = append(options, awsconfig.WithEndpointResolverWithOptions(resolver))
|
||||||
|
}
|
||||||
|
|
||||||
|
accessKey := strings.TrimSpace(cfg.AccessKey)
|
||||||
|
secretKey := strings.TrimSpace(cfg.SecretKey)
|
||||||
|
if accessKey != "" && secretKey != "" {
|
||||||
|
options = append(options, awsconfig.WithCredentialsProvider(
|
||||||
|
credentials.NewStaticCredentialsProvider(accessKey, secretKey, ""),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||||
|
o.UsePathStyle = cfg.ForcePathStyle
|
||||||
|
})
|
||||||
|
|
||||||
|
baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/")
|
||||||
|
if baseURL == "" {
|
||||||
|
if endpoint != "" {
|
||||||
|
baseURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(endpoint, "/"), bucket)
|
||||||
|
} else {
|
||||||
|
baseURL = fmt.Sprintf("https://%s.s3.%s.amazonaws.com", bucket, region)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &s3DocumentStorage{
|
||||||
|
client: client,
|
||||||
|
bucket: bucket,
|
||||||
|
base: baseURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3DocumentStorage) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error) {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return DocumentStorageUploadResult{}, errors.New("storage key is required")
|
||||||
|
}
|
||||||
|
if size < 0 {
|
||||||
|
size = 0
|
||||||
|
}
|
||||||
|
input := &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
input.ContentLength = aws.Int64(size)
|
||||||
|
if ct := strings.TrimSpace(contentType); ct != "" {
|
||||||
|
input.ContentType = aws.String(ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := s.client.PutObject(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return DocumentStorageUploadResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var etag string
|
||||||
|
if out.ETag != nil {
|
||||||
|
etag = strings.Trim(*out.ETag, "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentStorageUploadResult{
|
||||||
|
Key: key,
|
||||||
|
URL: s.URL(key),
|
||||||
|
ETag: etag,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3DocumentStorage) Delete(ctx context.Context, key string) error {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3DocumentStorage) URL(key string) string {
|
||||||
|
key = strings.TrimPrefix(strings.TrimSpace(key), "/")
|
||||||
|
if key == "" {
|
||||||
|
return s.base
|
||||||
|
}
|
||||||
|
if s.base == "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", s.base, key)
|
||||||
|
}
|
||||||
@@ -65,6 +65,14 @@ var (
|
|||||||
SSOUserSyncDrift time.Duration
|
SSOUserSyncDrift time.Duration
|
||||||
SSOUserSyncNonceTTL time.Duration
|
SSOUserSyncNonceTTL time.Duration
|
||||||
SSOUserSyncMaxBodyBytes int
|
SSOUserSyncMaxBodyBytes int
|
||||||
|
S3Endpoint string
|
||||||
|
S3Region string
|
||||||
|
S3Bucket string
|
||||||
|
S3AccessKey string
|
||||||
|
S3SecretKey string
|
||||||
|
S3ForcePathStyle bool
|
||||||
|
S3PublicBaseURL string
|
||||||
|
S3DocumentKeyPrefix string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -106,6 +114,16 @@ func init() {
|
|||||||
// Redis
|
// Redis
|
||||||
RedisURL = viper.GetString("REDIS_URL")
|
RedisURL = viper.GetString("REDIS_URL")
|
||||||
|
|
||||||
|
// Object storage
|
||||||
|
S3Endpoint = strings.TrimSpace(viper.GetString("S3_ENDPOINT"))
|
||||||
|
S3Region = strings.TrimSpace(viper.GetString("S3_REGION"))
|
||||||
|
S3Bucket = strings.TrimSpace(viper.GetString("S3_BUCKET"))
|
||||||
|
S3AccessKey = strings.TrimSpace(viper.GetString("S3_ACCESS_KEY"))
|
||||||
|
S3SecretKey = strings.TrimSpace(viper.GetString("S3_SECRET_KEY"))
|
||||||
|
S3ForcePathStyle = viper.GetBool("S3_FORCE_PATH_STYLE")
|
||||||
|
S3PublicBaseURL = strings.TrimSuffix(strings.TrimSpace(viper.GetString("S3_PUBLIC_BASE_URL")), "/")
|
||||||
|
S3DocumentKeyPrefix = defaultString(strings.Trim(strings.TrimSpace(viper.GetString("S3_DOCUMENT_PREFIX")), "/"), "docs")
|
||||||
|
|
||||||
// SSO integration
|
// SSO integration
|
||||||
SSOIssuer = viper.GetString("SSO_ISSUER")
|
SSOIssuer = viper.GetString("SSO_ISSUER")
|
||||||
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
|
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandangs_closed_at;
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
DROP COLUMN IF EXISTS closed_at;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_flock_kandangs_closed_at
|
||||||
|
ON project_flock_kandangs (closed_at);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE products
|
||||||
|
DROP COLUMN IF EXISTS is_visible;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE products
|
||||||
|
ADD COLUMN IF NOT EXISTS is_visible BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX IF EXISTS documents_documentable_polymorphic;
|
||||||
|
DROP TABLE IF EXISTS documents;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE documents (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
documentable_type VARCHAR(50) NOT NULL,
|
||||||
|
documentable_id BIGINT NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
path VARCHAR(50) NOT NULL,
|
||||||
|
name VARCHAR(50) NOT NULL,
|
||||||
|
ext VARCHAR(50) NOT NULL,
|
||||||
|
size NUMERIC(15, 3) NOT NULL,
|
||||||
|
created_by BIGINT REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX documents_documentable_polymorphic ON documents (documentable_type, documentable_id);
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop new indexes and FK
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_project_flock_kandang_id;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_unique;
|
||||||
|
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_product_warehouses_project_flock_kandang_id,
|
||||||
|
ALTER COLUMN project_flock_kandang_id DROP NOT NULL,
|
||||||
|
DROP COLUMN IF EXISTS project_flock_kandang_id;
|
||||||
|
|
||||||
|
-- Revert qty to integer quantity
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
RENAME COLUMN qty TO quantity;
|
||||||
|
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ALTER COLUMN quantity TYPE INTEGER USING quantity::integer,
|
||||||
|
ALTER COLUMN quantity SET DEFAULT 0,
|
||||||
|
ALTER COLUMN quantity SET NOT NULL;
|
||||||
|
|
||||||
|
-- Restore audit/soft-delete columns
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id),
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Recreate prior indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_product_warehouses_deleted_at ON product_warehouses (deleted_at);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_product_warehouses_unique
|
||||||
|
ON product_warehouses (product_id, warehouse_id)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop indexes that depend on deleted_at or old uniqueness
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_deleted_at;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_unique;
|
||||||
|
|
||||||
|
-- Add new relation and adjust quantity column
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
|
||||||
|
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
RENAME COLUMN quantity TO qty;
|
||||||
|
|
||||||
|
-- Enforce numeric quantity with precision and default
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ALTER COLUMN qty TYPE NUMERIC(15, 3) USING qty::numeric(15, 3),
|
||||||
|
ALTER COLUMN qty SET DEFAULT 0,
|
||||||
|
ALTER COLUMN qty SET NOT NULL;
|
||||||
|
|
||||||
|
-- Remove audit/soft-delete columns no longer used
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
DROP COLUMN IF EXISTS created_by,
|
||||||
|
DROP COLUMN IF EXISTS created_at,
|
||||||
|
DROP COLUMN IF EXISTS updated_at,
|
||||||
|
DROP COLUMN IF EXISTS deleted_at;
|
||||||
|
|
||||||
|
-- Enforce FK and not-null for project_flock_kandang_id
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ADD CONSTRAINT fk_product_warehouses_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs (id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- New indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_product_warehouses_project_flock_kandang_id
|
||||||
|
ON product_warehouses (project_flock_kandang_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_product_warehouses_unique
|
||||||
|
ON product_warehouses (product_id, warehouse_id, project_flock_kandang_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop new indexes
|
||||||
|
DROP INDEX IF EXISTS stock_logs_loggable_type_loggable_id_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_product_warehouse_id_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_created_by_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_created_at_idx;
|
||||||
|
|
||||||
|
-- Restore obsolete columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
ADD COLUMN IF NOT EXISTS transaction_type VARCHAR(20) DEFAULT '' NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS before_quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS after_quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Rename columns back
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN loggable_type TO log_type;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN loggable_id TO log_id;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN notes TO note;
|
||||||
|
|
||||||
|
-- Drop new columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
DROP COLUMN IF EXISTS increase,
|
||||||
|
DROP COLUMN IF EXISTS decrease;
|
||||||
|
|
||||||
|
-- Restore indexes for old structure
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_log_type_log_id_idx ON stock_logs (log_type, log_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_by_idx ON stock_logs (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_at_idx ON stock_logs (created_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_deleted_at_idx ON stock_logs (deleted_at);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop old indexes tied to removed columns
|
||||||
|
DROP INDEX IF EXISTS stock_logs_log_type_log_id_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_deleted_at_idx;
|
||||||
|
|
||||||
|
-- Rename columns to new naming
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN log_type TO loggable_type;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN log_id TO loggable_id;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
-- Add new increase/decrease columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
ADD COLUMN IF NOT EXISTS increase NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS decrease NUMERIC(15, 3) DEFAULT 0;
|
||||||
|
|
||||||
|
-- Adjust column definitions
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
ALTER COLUMN loggable_type TYPE VARCHAR(50),
|
||||||
|
ALTER COLUMN loggable_type SET NOT NULL,
|
||||||
|
ALTER COLUMN loggable_id SET NOT NULL,
|
||||||
|
ALTER COLUMN increase SET DEFAULT 0,
|
||||||
|
ALTER COLUMN increase SET NOT NULL,
|
||||||
|
ALTER COLUMN decrease SET DEFAULT 0,
|
||||||
|
ALTER COLUMN decrease SET NOT NULL;
|
||||||
|
|
||||||
|
-- Remove obsolete columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
DROP COLUMN IF EXISTS transaction_type,
|
||||||
|
DROP COLUMN IF EXISTS quantity,
|
||||||
|
DROP COLUMN IF EXISTS before_quantity,
|
||||||
|
DROP COLUMN IF EXISTS after_quantity,
|
||||||
|
DROP COLUMN IF EXISTS updated_at,
|
||||||
|
DROP COLUMN IF EXISTS deleted_at;
|
||||||
|
|
||||||
|
-- Recreate indexes for new structure
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_loggable_type_loggable_id_idx ON stock_logs (loggable_type, loggable_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_by_idx ON stock_logs (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_at_idx ON stock_logs (created_at);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Remove grading details from recording_eggs
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP COLUMN IF EXISTS weight;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0);
|
||||||
|
|
||||||
|
-- Restore grading_eggs table for rollback scenarios
|
||||||
|
CREATE TABLE grading_eggs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_egg_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15,3) NOT NULL,
|
||||||
|
grade VARCHAR,
|
||||||
|
created_by BIGINT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT fk_grading_eggs_recording_egg
|
||||||
|
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_grading_eggs_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||||
|
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_grading_eggs_recording_egg
|
||||||
|
ON grading_eggs (recording_egg_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Remove separate grading table and move grading details into recording_eggs
|
||||||
|
DROP INDEX IF EXISTS idx_grading_eggs_recording_egg;
|
||||||
|
DROP TABLE IF EXISTS grading_eggs;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD COLUMN IF NOT EXISTS weight NUMERIC(10,3);
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
|
||||||
|
qty >= 0 AND (weight IS NULL OR weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_expense_nonstock;
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_project_flock_kandang;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_purchase_items_expense_nonstock_id;
|
||||||
|
DROP INDEX IF EXISTS idx_purchase_items_project_flock_kandang_id;
|
||||||
|
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP COLUMN IF EXISTS expense_nonstock_id,
|
||||||
|
DROP COLUMN IF EXISTS project_flock_kandang_id,
|
||||||
|
ALTER COLUMN vehicle_number DROP NOT NULL,
|
||||||
|
ALTER COLUMN vehicle_number TYPE VARCHAR USING vehicle_number;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN pr_number TYPE VARCHAR USING pr_number,
|
||||||
|
ALTER COLUMN po_number TYPE VARCHAR USING po_number,
|
||||||
|
ALTER COLUMN created_at DROP DEFAULT,
|
||||||
|
ALTER COLUMN updated_at DROP DEFAULT;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ADD COLUMN credit_term INT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN grand_total NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN credit_term DROP DEFAULT,
|
||||||
|
ALTER COLUMN grand_total DROP DEFAULT;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- Adjust purchases table to new purchasing schema
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN pr_number TYPE VARCHAR(50) USING LEFT(pr_number, 50),
|
||||||
|
ALTER COLUMN po_number TYPE VARCHAR(50) USING LEFT(po_number, 50),
|
||||||
|
ALTER COLUMN created_at SET DEFAULT now(),
|
||||||
|
ALTER COLUMN updated_at SET DEFAULT now();
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
DROP COLUMN IF EXISTS credit_term,
|
||||||
|
DROP COLUMN IF EXISTS grand_total;
|
||||||
|
|
||||||
|
-- Bring purchase_items in line with new requirements
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
ADD COLUMN IF NOT EXISTS expense_nonstock_id BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
|
||||||
|
|
||||||
|
UPDATE purchase_items
|
||||||
|
SET vehicle_number = ''
|
||||||
|
WHERE vehicle_number IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
ALTER COLUMN vehicle_number TYPE VARCHAR(10) USING LEFT(vehicle_number, 10),
|
||||||
|
ALTER COLUMN vehicle_number SET NOT NULL;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_expense_nonstock
|
||||||
|
FOREIGN KEY (expense_nonstock_id)
|
||||||
|
REFERENCES expense_nonstocks(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_project_flock_kandang
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_expense_nonstock_id
|
||||||
|
ON purchase_items (expense_nonstock_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_project_flock_kandang_id
|
||||||
|
ON purchase_items (project_flock_kandang_id);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Drop function and sequence for sales order numbers
|
||||||
|
DROP FUNCTION IF EXISTS generate_so_number();
|
||||||
|
DROP SEQUENCE IF EXISTS so_number_seq;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Create sequence for sales order numbers
|
||||||
|
CREATE SEQUENCE so_number_seq START WITH 1 INCREMENT BY 1;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION generate_so_number()
|
||||||
|
RETURNS VARCHAR AS $$
|
||||||
|
DECLARE
|
||||||
|
next_val INTEGER;
|
||||||
|
BEGIN
|
||||||
|
next_val := nextval('so_number_seq');
|
||||||
|
RETURN 'SO-' || LPAD(next_val::TEXT, 5, '0');
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE purchases
|
||||||
|
DROP COLUMN IF EXISTS credit_term;
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE purchases
|
||||||
|
ADD COLUMN IF NOT EXISTS credit_term INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN credit_term DROP DEFAULT;
|
||||||
@@ -910,7 +910,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
|||||||
ProductId: product.Id,
|
ProductId: product.Id,
|
||||||
WarehouseId: warehouse.Id,
|
WarehouseId: warehouse.Id,
|
||||||
Quantity: seed.Quantity,
|
Quantity: seed.Quantity,
|
||||||
CreatedBy: createdBy,
|
// CreatedBy: createdBy,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&productWarehouse).Error; err != nil {
|
if err := tx.Create(&productWarehouse).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
DocumentableType string `gorm:"size:50;not null;index:documents_documentable_polymorphic,priority:1"`
|
||||||
|
DocumentableId uint64 `gorm:"not null;index:documents_documentable_polymorphic,priority:2"`
|
||||||
|
Type string `gorm:"size:50;not null"`
|
||||||
|
Path string `gorm:"size:50;not null"`
|
||||||
|
Name string `gorm:"size:50;not null"`
|
||||||
|
Ext string `gorm:"size:50;not null"`
|
||||||
|
Size float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
CreatedBy *uint `gorm:"index"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
}
|
||||||
@@ -20,5 +20,6 @@ type Kandang struct {
|
|||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||||
|
Warehouses []Warehouse `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ type Product struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
IsVisible bool `gorm:"column:is_visible;default:true"`
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
||||||
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
||||||
ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
|
ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
|
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
|
||||||
|
ProductWarehouses []ProductWarehouse `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProductWarehouse struct {
|
type ProductWarehouse struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
ProductId uint `gorm:"not null"`
|
ProductId uint `gorm:"column:product_id;not null"`
|
||||||
WarehouseId uint `gorm:"not null"`
|
WarehouseId uint `gorm:"column:warehouse_id;not null"`
|
||||||
Quantity float64 `gorm:"default:0"`
|
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Quantity float64 `gorm:"column:qty;type:numeric(15,3);default:0"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
StockLogs []StockLog `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectBudget struct {
|
type ProjectBudget struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
ProjectFlockId uint `gorm:"not null"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);not null"`
|
NonstockId uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"`
|
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
||||||
ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"`
|
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type ProjectFlock struct {
|
|||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
||||||
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||||
|
Budgets []ProjectBudget `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||||
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package entities
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ProjectFlockKandang struct {
|
type ProjectFlockKandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
Period int `gorm:"not null"`
|
Period int `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
ClosedAt *time.Time `gorm:"index"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
|||||||
@@ -5,19 +5,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Purchase struct {
|
type Purchase struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
PrNumber string `gorm:"not null"`
|
PrNumber string `gorm:"not null"`
|
||||||
PoNumber *string
|
PoNumber *string
|
||||||
PoDate *time.Time
|
PoDate *time.Time
|
||||||
SupplierId uint `gorm:"not null"`
|
SupplierId uint `gorm:"not null"`
|
||||||
CreditTerm *int
|
CreditTerm int `gorm:"column:credit_term;not null;default:0"`
|
||||||
DueDate *time.Time
|
DueDate *time.Time
|
||||||
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
|
||||||
Notes *string
|
Notes *string
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
|||||||
@@ -5,22 +5,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseItem struct {
|
type PurchaseItem struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
PurchaseId uint `gorm:"not null"`
|
PurchaseId uint `gorm:"not null"`
|
||||||
ProductId uint `gorm:"not null"`
|
ProductId uint `gorm:"not null"`
|
||||||
WarehouseId uint `gorm:"not null"`
|
WarehouseId uint `gorm:"not null"`
|
||||||
ProductWarehouseId *uint
|
ProductWarehouseId *uint
|
||||||
ReceivedDate *time.Time
|
ProjectFlockKandangId *uint
|
||||||
TravelNumber *string
|
ReceivedDate *time.Time
|
||||||
TravelNumberDocs *string
|
TravelNumber *string
|
||||||
VehicleNumber *string
|
TravelNumberDocs *string
|
||||||
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
VehicleNumber *string
|
||||||
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
TotalPrice 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"`
|
||||||
|
ExpenseNonstockId *uint64
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
|
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||||
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
|
|||||||
@@ -7,24 +7,11 @@ type RecordingEgg struct {
|
|||||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
Qty int `gorm:"column:qty;not null"`
|
Qty int `gorm:"column:qty;not null"`
|
||||||
|
Weight *float64 `gorm:"column:weight"`
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GradingEgg struct {
|
|
||||||
Id uint `gorm:"primaryKey"`
|
|
||||||
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
|
|
||||||
Qty float64 `gorm:"column:qty;not null"`
|
|
||||||
Grade string `gorm:"column:grade;type:varchar(50)"`
|
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
|
|
||||||
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
import "time"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LogTypeAdjustment = "ADJUSTMENT"
|
LogTypeAdjustment = "ADJUSTMENT"
|
||||||
@@ -17,19 +13,18 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StockLog struct {
|
type StockLog struct {
|
||||||
Id uint `gorm:"primaryKey;column:id"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
TransactionType string `gorm:"type:varchar(20);not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
||||||
Quantity float64 `gorm:"type:numeric(15,3);not null"`
|
CreatedBy uint `gorm:"column:created_by;not null;index"`
|
||||||
BeforeQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
AfterQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
Increase float64 `gorm:"column:increase;type:numeric(15,3);default:0"`
|
||||||
LogType string `gorm:"type:varchar(50);not null;index:stock_logs_flaggable_lookup,priority:1"`
|
Decrease float64 `gorm:"column:decrease;type:numeric(15,3);default:0"`
|
||||||
LogId uint `gorm:"not null;index:stock_logs_flaggable_lookup,priority:2"`
|
|
||||||
Note string `gorm:"type:text"`
|
LoggableType string `gorm:"column:loggable_type;type:varchar(50);not null"`
|
||||||
ProductWarehouseId uint `gorm:"not null;index"`
|
LoggableId uint `gorm:"column:loggable_id;not null"`
|
||||||
CreatedBy uint `gorm:"index"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Notes string `gorm:"column:notes;type:text"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
|
||||||
|
|
||||||
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -91,7 +90,6 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl
|
|||||||
|
|
||||||
c.Locals(authContextLocalsKey, ctx)
|
c.Locals(authContextLocalsKey, ctx)
|
||||||
c.Locals(authUserLocalsKey, user)
|
c.Locals(authUserLocalsKey, user)
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +105,7 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
|
|||||||
|
|
||||||
func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
|
func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||||
user, ok := AuthenticatedUser(c)
|
user, ok := AuthenticatedUser(c)
|
||||||
if !ok || user == nil || user.Id == 0 {
|
if !ok || user == nil || user.Id == 0 {
|
||||||
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
}
|
}
|
||||||
return user.Id, nil
|
return user.Id, nil
|
||||||
@@ -199,3 +197,71 @@ func hasAllScopes(have, required []string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
||||||
|
func RequirePermissions(perms ...string) fiber.Handler {
|
||||||
|
required := canonicalPermissions(perms)
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
if len(required) == 0 {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
userPerms := ctx.permissionSet()
|
||||||
|
if len(userPerms) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, perm := range required {
|
||||||
|
if _, has := userPerms[perm]; !has {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPermission reports whether the current request context includes the given permission.
|
||||||
|
func HasPermission(c *fiber.Ctx, perm string) bool {
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
perm = canonicalPermission(perm)
|
||||||
|
if perm == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, has := ctx.permissionSet()[perm]
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthContext) permissionSet() map[string]struct{} {
|
||||||
|
if a == nil || a.Permissions == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.Permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermissions(perms []string) []string {
|
||||||
|
out := make([]string, 0, len(perms))
|
||||||
|
seen := make(map[string]struct{}, len(perms))
|
||||||
|
for _, perm := range perms {
|
||||||
|
if canonical := canonicalPermission(perm); canonical != "" {
|
||||||
|
if _, ok := seen[canonical]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[canonical] = struct{}{}
|
||||||
|
out = append(out, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermission(perm string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(perm))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,75 +1,14 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
//project-flock
|
||||||
"strings"
|
const (
|
||||||
|
PermissionProjectFlockClosing = "lti:project-flock:closing"
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
//recording
|
||||||
func RequirePermissions(perms ...string) fiber.Handler {
|
const (
|
||||||
required := canonicalPermissions(perms)
|
PermissionRecordingRead = "recording.index"
|
||||||
return func(c *fiber.Ctx) error {
|
PermissionRecordingCreate = "recording.create"
|
||||||
if len(required) == 0 {
|
PermissionRecordingUpdate = "recording.update"
|
||||||
return c.Next()
|
PermissionRecordingDelete = "recording.delete"
|
||||||
}
|
)
|
||||||
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
|
||||||
}
|
|
||||||
|
|
||||||
userPerms := ctx.permissionSet()
|
|
||||||
if len(userPerms) == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, perm := range required {
|
|
||||||
if _, has := userPerms[perm]; !has {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPermission reports whether the current request context includes the given permission.
|
|
||||||
func HasPermission(c *fiber.Ctx, perm string) bool {
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
perm = canonicalPermission(perm)
|
|
||||||
if perm == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, has := ctx.permissionSet()[perm]
|
|
||||||
return has
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AuthContext) permissionSet() map[string]struct{} {
|
|
||||||
if a == nil || a.Permissions == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return a.Permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermissions(perms []string) []string {
|
|
||||||
out := make([]string, 0, len(perms))
|
|
||||||
seen := make(map[string]struct{}, len(perms))
|
|
||||||
for _, perm := range perms {
|
|
||||||
if canonical := canonicalPermission(perm); canonical != "" {
|
|
||||||
if _, ok := seen[canonical]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[canonical] = struct{}{}
|
|
||||||
out = append(out, canonical)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermission(perm string) string {
|
|
||||||
return strings.ToLower(strings.TrimSpace(perm))
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
@@ -13,12 +14,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClosingController struct {
|
type ClosingController struct {
|
||||||
ClosingService service.ClosingService
|
ClosingService service.ClosingService
|
||||||
|
SapronakService service.SapronakService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingController(closingService service.ClosingService) *ClosingController {
|
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
||||||
return &ClosingController{
|
return &ClosingController{
|
||||||
ClosingService: closingService,
|
ClosingService: closingService,
|
||||||
|
SapronakService: sapronakService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,17 +42,17 @@ func (u *ClosingController) GetAll(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{
|
JSON(response.SuccessWithPaginate[dto.ClosingListItemDTO]{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get all closings successfully",
|
Message: "Retrieved closing projects list successfully",
|
||||||
Meta: response.Meta{
|
Meta: response.Meta{
|
||||||
Page: query.Page,
|
Page: query.Page,
|
||||||
Limit: query.Limit,
|
Limit: query.Limit,
|
||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
TotalResults: totalResults,
|
TotalResults: totalResults,
|
||||||
},
|
},
|
||||||
Data: dto.ToClosingListDTOs(result),
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +60,11 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
|||||||
param := c.Params("id")
|
param := c.Params("id")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil || id <= 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingService.GetOne(c, uint(id))
|
result, err := u.ClosingService.GetProjectFlockByID(c, uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,175 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
|||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get closing successfully",
|
Message: "Retrieved closing information successfully",
|
||||||
Data: dto.ToClosingListDTO(*result),
|
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -26,6 +27,101 @@ type ClosingDetailDTO struct {
|
|||||||
ClosingListDTO
|
ClosingListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClosingListItemDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
LocationID uint `json:"location_id"`
|
||||||
|
LocationName string `json:"location_name"`
|
||||||
|
ProjectCategory string `json:"project_category"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
ClosingDate string `json:"closing_date"`
|
||||||
|
ShedLabel string `json:"shed_label"`
|
||||||
|
ShedCount int `json:"shed_count"`
|
||||||
|
SalesPaidAmount int64 `json:"sales_paid_amount"`
|
||||||
|
SalesRemainingAmount int64 `json:"sales_remaining_amount"`
|
||||||
|
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||||
|
ProjectStatus string `json:"project_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingSummaryDTO struct {
|
||||||
|
FlockID uint `json:"flock_id"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
// JenisProduk string `json:"jenis_produk"`
|
||||||
|
// LabelPopulasi string `json:"label_populasi"`
|
||||||
|
Population int `json:"population"`
|
||||||
|
PopulationFormatted string `json:"population_formatted"`
|
||||||
|
ProjectType string `json:"project_type"`
|
||||||
|
ActiveHouseCount int `json:"active_house_count"`
|
||||||
|
ActiveHouseLabel string `json:"active_house_label"`
|
||||||
|
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||||
|
// StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
||||||
|
StatusProject string `json:"project_status"`
|
||||||
|
StatusClosing string `json:"closing_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
||||||
|
history := project.KandangHistory
|
||||||
|
|
||||||
|
period := maxPeriod(history)
|
||||||
|
kandangCount := len(history)
|
||||||
|
population := sumPopulation(history)
|
||||||
|
populationInt := int(population)
|
||||||
|
|
||||||
|
return ClosingSummaryDTO{
|
||||||
|
FlockID: project.Id,
|
||||||
|
Period: period,
|
||||||
|
// JenisProduk: project.Category,
|
||||||
|
// LabelPopulasi: "",
|
||||||
|
Population: populationInt,
|
||||||
|
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||||
|
ProjectType: project.Category,
|
||||||
|
ActiveHouseCount: kandangCount,
|
||||||
|
ActiveHouseLabel: fmt.Sprintf("%d Kandang", kandangCount),
|
||||||
|
SalesPaymentStatus: "Tempo",
|
||||||
|
// StatusPembayaranMitra: "",
|
||||||
|
StatusProject: statusProject,
|
||||||
|
StatusClosing: statusClosing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToClosingListItemDTO(project entity.ProjectFlock, projectStatus string) ClosingListItemDTO {
|
||||||
|
shedCount := len(project.KandangHistory)
|
||||||
|
|
||||||
|
return ClosingListItemDTO{
|
||||||
|
Id: project.Id,
|
||||||
|
LocationID: project.LocationId,
|
||||||
|
LocationName: project.Location.Name,
|
||||||
|
ProjectCategory: project.Category,
|
||||||
|
Period: maxPeriod(project.KandangHistory),
|
||||||
|
ClosingDate: "17-Nov-2025",
|
||||||
|
ShedLabel: fmt.Sprintf("%d Kandang", shedCount),
|
||||||
|
ShedCount: shedCount,
|
||||||
|
SalesPaidAmount: 21993726,
|
||||||
|
SalesRemainingAmount: 11075919,
|
||||||
|
SalesPaymentStatus: "Lunas",
|
||||||
|
ProjectStatus: projectStatus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxPeriod(history []entity.ProjectFlockKandang) int {
|
||||||
|
max := 0
|
||||||
|
for _, h := range history {
|
||||||
|
if h.Period > max {
|
||||||
|
max = h.Period
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumPopulation(history []entity.ProjectFlockKandang) float64 {
|
||||||
|
var total float64
|
||||||
|
for _, h := range history {
|
||||||
|
for _, chickin := range h.Chickins {
|
||||||
|
total += chickin.UsageQty + chickin.PendingUsageQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
|
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === Response DTO ===
|
||||||
|
type SalesDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
RealizationDate time.Time `json:"realization_date"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
DoNumber string `json:"do_number"`
|
||||||
|
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||||
|
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
|
PaymentStatus string `json:"payment_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PenjualanRealisasiResponseDTO struct {
|
||||||
|
ProjectType string `json:"project_type"`
|
||||||
|
FlockId uint `json:"flock_id"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
Sales []SalesDTO `json:"sales"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||||
|
|
||||||
|
// todo: usia ayam masih dummy
|
||||||
|
age := 0
|
||||||
|
|
||||||
|
var product *productDTO.ProductRelationDTO
|
||||||
|
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||||
|
mapped := productDTO.ToProductRelationDTO(e.MarketingProduct.ProductWarehouse.Product)
|
||||||
|
product = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer *customerDTO.CustomerRelationDTO
|
||||||
|
if e.MarketingProduct.Marketing.Customer.Id != 0 {
|
||||||
|
mapped := customerDTO.ToCustomerRelationDTO(e.MarketingProduct.Marketing.Customer)
|
||||||
|
customer = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandang *kandangDTO.KandangRelationDTO
|
||||||
|
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang)
|
||||||
|
kandang = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
|
||||||
|
|
||||||
|
return SalesDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
RealizationDate: *e.DeliveryDate,
|
||||||
|
Age: age,
|
||||||
|
DoNumber: doNumber,
|
||||||
|
Product: product,
|
||||||
|
Customer: customer,
|
||||||
|
Qty: e.Qty,
|
||||||
|
Weight: e.TotalWeight,
|
||||||
|
AvgWeight: e.AvgWeight,
|
||||||
|
Price: e.UnitPrice,
|
||||||
|
TotalPrice: e.TotalPrice,
|
||||||
|
Kandang: kandang,
|
||||||
|
PaymentStatus: "Paid",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
||||||
|
result := make([]SalesDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToSalesDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||||
|
period := extractPeriodFromRealisasi(e)
|
||||||
|
return PenjualanRealisasiResponseDTO{
|
||||||
|
ProjectType: projectType,
|
||||||
|
FlockId: projectFlockID,
|
||||||
|
Period: period,
|
||||||
|
Sales: ToSalesDTOs(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int {
|
||||||
|
if len(realisasi) > 0 {
|
||||||
|
for _, item := range realisasi {
|
||||||
|
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
||||||
|
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type OverheadDTO struct {
|
||||||
|
ItemName string `json:"item_name"`
|
||||||
|
UOMName string `json:"uom_name"`
|
||||||
|
BudgetQuantity float64 `json:"budget_quantity"`
|
||||||
|
BudgetUnitPrice float64 `json:"budget_unit_price"`
|
||||||
|
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||||
|
ActualDate string `json:"actual_date"`
|
||||||
|
ActualQuantity float64 `json:"actual_quantity"`
|
||||||
|
ActualUnitPrice float64 `json:"actual_unit_price"`
|
||||||
|
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||||
|
CostPerBird float64 `json:"cost_per_bird"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TotalDTO struct {
|
||||||
|
BudgetQuantity float64 `json:"budget_quantity"`
|
||||||
|
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||||
|
ActualQuantity float64 `json:"actual_quantity"`
|
||||||
|
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||||
|
CostPerBird float64 `json:"cost_per_bird"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverheadListDTO struct {
|
||||||
|
Total TotalDTO `json:"total"`
|
||||||
|
Overheads []OverheadDTO `json:"overheads"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseRealization) OverheadDTO {
|
||||||
|
if budget == nil && realization == nil {
|
||||||
|
return OverheadDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemName, itemUOM string
|
||||||
|
if budget != nil {
|
||||||
|
itemName, itemUOM = getItemInfo(budget.Nonstock)
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemName == "" && realization != nil && realization.ExpenseNonstock != nil {
|
||||||
|
itemName, itemUOM = getItemInfo(realization.ExpenseNonstock.Nonstock)
|
||||||
|
}
|
||||||
|
|
||||||
|
dto := OverheadDTO{
|
||||||
|
ItemName: itemName,
|
||||||
|
UOMName: itemUOM,
|
||||||
|
}
|
||||||
|
|
||||||
|
if budget != nil {
|
||||||
|
dto.BudgetQuantity = budget.Qty
|
||||||
|
dto.BudgetUnitPrice = budget.Price
|
||||||
|
dto.BudgetTotalAmount = calculateTotal(budget.Qty, budget.Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
if realization != nil {
|
||||||
|
dto.ActualQuantity = realization.Qty
|
||||||
|
dto.ActualUnitPrice = realization.Price
|
||||||
|
dto.ActualTotalAmount = calculateTotal(realization.Qty, realization.Price)
|
||||||
|
dto.ActualDate = formatRealizationDate(realization)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO {
|
||||||
|
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||||
|
latestDateByNonstockID := make(map[uint]string)
|
||||||
|
|
||||||
|
for i := range budgets {
|
||||||
|
nonstockID := budgets[i].NonstockId
|
||||||
|
if overheadsByNonstockID[nonstockID] == nil {
|
||||||
|
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
||||||
|
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||||
|
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range realizations {
|
||||||
|
if realizations[i].ExpenseNonstock == nil || realizations[i].ExpenseNonstock.NonstockId == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nonstockID := uint(*realizations[i].ExpenseNonstock.NonstockId)
|
||||||
|
if overheadsByNonstockID[nonstockID] == nil {
|
||||||
|
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
|
||||||
|
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||||
|
|
||||||
|
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
||||||
|
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
||||||
|
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||||
|
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||||
|
}
|
||||||
|
|
||||||
|
realizationDateStr := formatRealizationDate(&realizations[i])
|
||||||
|
if realizationDateStr != "" {
|
||||||
|
if latestDateByNonstockID[nonstockID] == "" || realizationDateStr > latestDateByNonstockID[nonstockID] {
|
||||||
|
latestDateByNonstockID[nonstockID] = realizationDateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalBudgetQuantity, totalBudgetAmount, totalActualQuantity, totalActualAmount float64
|
||||||
|
overheadItems := make([]OverheadDTO, 0, len(overheadsByNonstockID))
|
||||||
|
|
||||||
|
for nonstockID, overhead := range overheadsByNonstockID {
|
||||||
|
overhead.ActualDate = latestDateByNonstockID[nonstockID]
|
||||||
|
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty)
|
||||||
|
|
||||||
|
if overhead.ActualQuantity > 0 {
|
||||||
|
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBudgetQuantity += overhead.BudgetQuantity
|
||||||
|
totalBudgetAmount += overhead.BudgetTotalAmount
|
||||||
|
totalActualQuantity += overhead.ActualQuantity
|
||||||
|
totalActualAmount += overhead.ActualTotalAmount
|
||||||
|
|
||||||
|
overheadItems = append(overheadItems, *overhead)
|
||||||
|
}
|
||||||
|
|
||||||
|
return OverheadListDTO{
|
||||||
|
Total: TotalDTO{
|
||||||
|
BudgetQuantity: totalBudgetQuantity,
|
||||||
|
BudgetTotalAmount: totalBudgetAmount,
|
||||||
|
ActualQuantity: totalActualQuantity,
|
||||||
|
ActualTotalAmount: totalActualAmount,
|
||||||
|
CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty),
|
||||||
|
},
|
||||||
|
Overheads: overheadItems,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helper Functions ===
|
||||||
|
|
||||||
|
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
||||||
|
if nonstock != nil && nonstock.Id != 0 {
|
||||||
|
return nonstock.Name, nonstock.Uom.Name
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateTotal(qty, price float64) float64 {
|
||||||
|
return qty * price
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 {
|
||||||
|
if totalChickinQty > 0 {
|
||||||
|
return totalPrice / totalChickinQty
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRealizationDate(realization *entity.ExpenseRealization) string {
|
||||||
|
if realization != nil && realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil {
|
||||||
|
if !realization.ExpenseNonstock.Expense.RealizationDate.IsZero() {
|
||||||
|
return realization.ExpenseNonstock.Expense.RealizationDate.Format("2006-01-02T15:04:05Z07:00")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SapronakDetailDTO struct {
|
||||||
|
ProductID uint `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
Tanggal *time.Time `json:"tanggal,omitempty"`
|
||||||
|
NoReferensi string `json:"no_referensi,omitempty"`
|
||||||
|
JenisTransaksi string `json:"jenis_transaksi,omitempty"`
|
||||||
|
QtyMasuk float64 `json:"qty_masuk"`
|
||||||
|
QtyKeluar float64 `json:"qty_keluar"`
|
||||||
|
Harga float64 `json:"harga"`
|
||||||
|
Nilai float64 `json:"nilai"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakGroupDTO struct {
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
Items []SapronakDetailDTO `json:"items"`
|
||||||
|
TotalMasuk float64 `json:"total_masuk"`
|
||||||
|
TotalKeluar float64 `json:"total_keluar"`
|
||||||
|
SaldoAkhir float64 `json:"saldo_akhir"`
|
||||||
|
TotalNilai float64 `json:"total_nilai"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakItemDTO struct {
|
||||||
|
ProductID uint `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
IncomingQty float64 `json:"incoming_qty"`
|
||||||
|
IncomingValue float64 `json:"incoming_value"`
|
||||||
|
UsageQty float64 `json:"usage_qty"`
|
||||||
|
UsageValue float64 `json:"usage_value"`
|
||||||
|
RemainingQty float64 `json:"remaining_qty"`
|
||||||
|
AveragePrice float64 `json:"average_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakReportDTO struct {
|
||||||
|
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
||||||
|
ProjectFlockID uint `json:"project_flock_id"`
|
||||||
|
ProjectName string `json:"project_name"`
|
||||||
|
KandangID uint `json:"kandang_id"`
|
||||||
|
KandangName string `json:"kandang_name"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
StartDate *time.Time `json:"start_date,omitempty"`
|
||||||
|
EndDate *time.Time `json:"end_date,omitempty"`
|
||||||
|
TotalIncomingValue float64 `json:"total_incoming_value"`
|
||||||
|
TotalUsageValue float64 `json:"total_usage_value"`
|
||||||
|
Items []SapronakItemDTO `json:"items"`
|
||||||
|
Groups []SapronakGroupDTO `json:"groups,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified view for project-level sapronak response
|
||||||
|
type SapronakCategoryRowDTO struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
ReferenceNumber string `json:"reference_number"`
|
||||||
|
QtyIn float64 `json:"qty_in"`
|
||||||
|
QtyOut float64 `json:"qty_out"`
|
||||||
|
QtyUsed float64 `json:"qty_used"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ProductCategory string `json:"product_category"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakCategoryTotalDTO struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
QtyIn float64 `json:"qty_in"`
|
||||||
|
QtyOut float64 `json:"qty_out"`
|
||||||
|
QtyUsed float64 `json:"qty_used"`
|
||||||
|
AvgUnitPrice float64 `json:"avg_unit_price"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakCategoryDTO struct {
|
||||||
|
Rows []SapronakCategoryRowDTO `json:"rows"`
|
||||||
|
Total SapronakCategoryTotalDTO `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakProjectAggregatedDTO struct {
|
||||||
|
Doc *SapronakCategoryDTO `json:"doc,omitempty"`
|
||||||
|
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
|
||||||
|
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
|
||||||
|
Pullet *SapronakCategoryDTO `json:"pullet,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingSapronakItemDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
ReferenceNumber string `json:"reference_number"`
|
||||||
|
TransactionType string `json:"transaction_type"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ProductCategory string `json:"product_category"`
|
||||||
|
ProductSubCategory string `json:"product_sub_category"`
|
||||||
|
SourceWarehouse string `json:"source_warehouse"`
|
||||||
|
DestinationWarehouse string `json:"destination_warehouse,omitempty"`
|
||||||
|
// Destination string `json:"destination,omitempty"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Unit string `json:"unit"`
|
||||||
|
FormattedQuantity string `json:"formatted_quantity"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
SortDate time.Time `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingSapronakDTO struct {
|
||||||
|
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
|
||||||
|
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions for Aggregated Sapronak Response ===
|
||||||
|
|
||||||
|
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||||
|
result := SapronakProjectAggregatedDTO{}
|
||||||
|
|
||||||
|
if len(reports) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
rep := reports[0]
|
||||||
|
return ToSapronakProjectAggregatedFromReport(&rep, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||||
|
result := SapronakProjectAggregatedDTO{}
|
||||||
|
|
||||||
|
if report == nil {
|
||||||
|
report = &SapronakReportDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := strings.ToUpper(strings.TrimSpace(flag))
|
||||||
|
|
||||||
|
byFlag := map[string]**SapronakCategoryDTO{}
|
||||||
|
if filter == "" || filter == "DOC" {
|
||||||
|
result.Doc = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["DOC"] = &result.Doc
|
||||||
|
}
|
||||||
|
if filter == "" || filter == "OVK" {
|
||||||
|
result.Ovk = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["OVK"] = &result.Ovk
|
||||||
|
}
|
||||||
|
if filter == "" || filter == "PAKAN" {
|
||||||
|
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["PAKAN"] = &result.Pakan
|
||||||
|
}
|
||||||
|
if filter == "" || filter == "PULLET" {
|
||||||
|
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["PULLET"] = &result.Pullet
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate := func(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format("02-Jan-2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range report.Groups {
|
||||||
|
flagKey := strings.ToUpper(group.Flag)
|
||||||
|
ptr := byFlag[flagKey]
|
||||||
|
if ptr == nil || *ptr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
target := *ptr
|
||||||
|
|
||||||
|
rowIndexByProduct := make(map[string]int)
|
||||||
|
|
||||||
|
getOrCreateRow := func(productKey string, base SapronakCategoryRowDTO) *SapronakCategoryRowDTO {
|
||||||
|
if idx, ok := rowIndexByProduct[productKey]; ok {
|
||||||
|
return &target.Rows[idx]
|
||||||
|
}
|
||||||
|
target.Rows = append(target.Rows, base)
|
||||||
|
idx := len(target.Rows) - 1
|
||||||
|
rowIndexByProduct[productKey] = idx
|
||||||
|
return &target.Rows[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, item := range group.Items {
|
||||||
|
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
|
||||||
|
baseRow := SapronakCategoryRowDTO{
|
||||||
|
ID: idx + 1,
|
||||||
|
Date: formatDate(item.Tanggal),
|
||||||
|
ReferenceNumber: item.NoReferensi,
|
||||||
|
Description: item.ProductName,
|
||||||
|
ProductCategory: item.ProductName,
|
||||||
|
UnitPrice: item.Harga,
|
||||||
|
Notes: "-",
|
||||||
|
}
|
||||||
|
|
||||||
|
row := getOrCreateRow(productKey, baseRow)
|
||||||
|
|
||||||
|
switch strings.ToLower(item.JenisTransaksi) {
|
||||||
|
case "pembelian", "adjustment masuk", "mutasi masuk":
|
||||||
|
row.QtyIn += item.QtyMasuk
|
||||||
|
row.TotalAmount += item.Nilai
|
||||||
|
case "pemakaian", "adjustment keluar":
|
||||||
|
row.QtyUsed += item.QtyKeluar
|
||||||
|
case "mutasi keluar":
|
||||||
|
row.QtyOut += item.QtyKeluar
|
||||||
|
default:
|
||||||
|
row.QtyIn += item.QtyMasuk
|
||||||
|
row.TotalAmount += item.Nilai
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.QtyIn > 0 {
|
||||||
|
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range target.Rows {
|
||||||
|
target.Rows[i].ID = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTotals := func(cat *SapronakCategoryDTO, label string) {
|
||||||
|
if cat == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var qtyIn, qtyOut, qtyUsed, total float64
|
||||||
|
for _, r := range cat.Rows {
|
||||||
|
qtyIn += r.QtyIn
|
||||||
|
qtyOut += r.QtyOut
|
||||||
|
qtyUsed += r.QtyUsed
|
||||||
|
total += r.TotalAmount
|
||||||
|
}
|
||||||
|
avg := 0.0
|
||||||
|
if qtyIn > 0 {
|
||||||
|
avg = total / qtyIn
|
||||||
|
}
|
||||||
|
cat.Total = SapronakCategoryTotalDTO{
|
||||||
|
Label: label,
|
||||||
|
QtyIn: qtyIn,
|
||||||
|
QtyOut: qtyOut,
|
||||||
|
QtyUsed: qtyUsed,
|
||||||
|
AvgUnitPrice: avg,
|
||||||
|
TotalAmount: total,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTotals(result.Doc, "TOTAL DOC")
|
||||||
|
buildTotals(result.Ovk, "TOTAL OVK")
|
||||||
|
buildTotals(result.Pakan, "TOTAL PAKAN")
|
||||||
|
buildTotals(result.Pullet, "TOTAL PULLET")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -5,8 +5,14 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
|
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -17,10 +23,19 @@ type ClosingModule struct{}
|
|||||||
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
closingRepo := rClosing.NewClosingRepository(db)
|
closingRepo := rClosing.NewClosingRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||||
|
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
|
||||||
|
marketingRepo := rMarketings.NewMarketingRepository(db)
|
||||||
|
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
||||||
|
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
|
||||||
|
chickinRepo := rChickin.NewChickinRepository(db)
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, validate)
|
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
|
||||||
|
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ClosingRoutes(router, userService, closingService)
|
ClosingRoutes(router, userService, closingService, sapronakService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClosingRepository interface {
|
type ClosingRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectFlock]
|
repository.BaseRepository[entity.ProjectFlock]
|
||||||
|
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||||
|
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
||||||
|
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||||
|
FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||||
|
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingRepositoryImpl struct {
|
type ClosingRepositoryImpl struct {
|
||||||
@@ -19,3 +35,566 @@ func NewClosingRepository(db *gorm.DB) ClosingRepository {
|
|||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SapronakRow struct {
|
||||||
|
Id uint64 `gorm:"column:id"`
|
||||||
|
SortDate time.Time `gorm:"column:sort_date"`
|
||||||
|
DateText string `gorm:"column:date_text"`
|
||||||
|
ReferenceNumber string `gorm:"column:reference_number"`
|
||||||
|
TransactionType string `gorm:"column:transaction_type"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
ProductCategory string `gorm:"column:product_category"`
|
||||||
|
ProductSubCategory string `gorm:"column:product_sub_category"`
|
||||||
|
SourceWarehouse string `gorm:"column:source_warehouse"`
|
||||||
|
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
||||||
|
Destination string `gorm:"column:destination"`
|
||||||
|
Quantity float64 `gorm:"column:quantity"`
|
||||||
|
Unit string `gorm:"column:unit"`
|
||||||
|
Notes string `gorm:"column:notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakQueryParams struct {
|
||||||
|
Type string
|
||||||
|
WarehouseIDs []uint
|
||||||
|
ProjectFlockKandangIDs []uint
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
||||||
|
db := r.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
unionParts []string
|
||||||
|
args []any
|
||||||
|
)
|
||||||
|
|
||||||
|
switch params.Type {
|
||||||
|
case validation.SapronakTypeIncoming:
|
||||||
|
if len(params.WarehouseIDs) == 0 {
|
||||||
|
return []SapronakRow{}, 0, nil
|
||||||
|
}
|
||||||
|
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
||||||
|
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||||
|
case validation.SapronakTypeOutgoing:
|
||||||
|
if len(params.WarehouseIDs) > 0 {
|
||||||
|
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
||||||
|
args = append(args, params.WarehouseIDs)
|
||||||
|
}
|
||||||
|
if len(params.ProjectFlockKandangIDs) > 0 {
|
||||||
|
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
||||||
|
args = append(args, params.ProjectFlockKandangIDs)
|
||||||
|
}
|
||||||
|
if len(unionParts) == 0 {
|
||||||
|
return []SapronakRow{}, 0, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, 0, fmt.Errorf("invalid sapronak type: %s", params.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||||
|
|
||||||
|
var totalResults int64
|
||||||
|
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
|
||||||
|
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
|
||||||
|
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
|
||||||
|
|
||||||
|
var rows []SapronakRow
|
||||||
|
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, totalResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sapronakIncomingPurchasesSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(pi.id AS BIGINT) AS id,
|
||||||
|
COALESCE(pi.received_date, '1970-01-01') AS sort_date,
|
||||||
|
COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
|
||||||
|
COALESCE(p.po_number, '') AS reference_number,
|
||||||
|
'Purchase' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
'External Supplier' AS source_warehouse,
|
||||||
|
w.name AS destination_warehouse,
|
||||||
|
'' AS destination,
|
||||||
|
pi.total_qty AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
COALESCE(p.notes, '') AS notes
|
||||||
|
FROM purchase_items pi
|
||||||
|
JOIN purchases p ON p.id = pi.purchase_id
|
||||||
|
JOIN products prod ON prod.id = pi.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
JOIN warehouses w ON w.id = pi.warehouse_id
|
||||||
|
WHERE pi.warehouse_id IN ?
|
||||||
|
`
|
||||||
|
|
||||||
|
sapronakIncomingTransfersSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(st.id AS BIGINT) AS id,
|
||||||
|
st.transfer_date AS sort_date,
|
||||||
|
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||||
|
st.movement_number AS reference_number,
|
||||||
|
'Internal Transfer In' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
COALESCE(fw.name, '') AS source_warehouse,
|
||||||
|
COALESCE(tw.name, '') AS destination_warehouse,
|
||||||
|
'' AS destination,
|
||||||
|
std.quantity AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
'Stock Refill' AS notes
|
||||||
|
FROM stock_transfer_details std
|
||||||
|
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||||
|
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||||
|
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||||
|
JOIN products prod ON prod.id = std.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
WHERE st.to_warehouse_id IN ?
|
||||||
|
`
|
||||||
|
|
||||||
|
sapronakOutgoingTransfersSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(st.id AS BIGINT) AS id,
|
||||||
|
st.transfer_date AS sort_date,
|
||||||
|
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||||
|
st.movement_number AS reference_number,
|
||||||
|
'Internal Transfer Out' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
COALESCE(fw.name, '') AS source_warehouse,
|
||||||
|
'' AS destination_warehouse,
|
||||||
|
COALESCE(tw.name, '') AS destination,
|
||||||
|
std.quantity AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
'Transfer to other unit' AS notes
|
||||||
|
FROM stock_transfer_details std
|
||||||
|
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||||
|
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||||
|
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||||
|
JOIN products prod ON prod.id = std.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
WHERE st.from_warehouse_id IN ?
|
||||||
|
`
|
||||||
|
|
||||||
|
sapronakOutgoingMarketingsSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(mp.id AS BIGINT) AS id,
|
||||||
|
m.so_date AS sort_date,
|
||||||
|
TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text,
|
||||||
|
m.so_number AS reference_number,
|
||||||
|
'Trading Sales' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
w.name AS source_warehouse,
|
||||||
|
'' AS destination_warehouse,
|
||||||
|
'RETAIL CUSTOMER' AS destination,
|
||||||
|
mp.qty AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
m.notes AS notes
|
||||||
|
FROM marketing_products mp
|
||||||
|
JOIN marketings m ON m.id = mp.marketing_id
|
||||||
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||||
|
JOIN products prod ON prod.id = pw.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||||
|
WHERE pw.project_flock_kandang_id IN ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
type SapronakIncomingRow struct {
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
Flag string
|
||||||
|
Qty float64
|
||||||
|
Value float64
|
||||||
|
DefaultPrice float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakUsageRow struct {
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
Flag string
|
||||||
|
Qty float64
|
||||||
|
DefaultPrice float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakDetailRow struct {
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
Flag string
|
||||||
|
Date *time.Time
|
||||||
|
Reference string
|
||||||
|
QtyIn float64
|
||||||
|
QtyOut float64
|
||||||
|
Price float64
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }
|
||||||
|
|
||||||
|
func applyJoins(db *gorm.DB, joins ...string) *gorm.DB {
|
||||||
|
for _, j := range joins {
|
||||||
|
if strings.TrimSpace(j) != "" {
|
||||||
|
db = db.Joins(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func sapronakFlags(flags ...utils.FlagType) []string {
|
||||||
|
out := make([]string, len(flags))
|
||||||
|
for i, f := range flags {
|
||||||
|
out[i] = string(f)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
|
||||||
|
sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
|
||||||
|
sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
|
||||||
|
)
|
||||||
|
|
||||||
|
func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
|
||||||
|
m := make(map[uint][]SapronakDetailRow)
|
||||||
|
for _, row := range rows {
|
||||||
|
m[row.ProductID] = append(m[row.ProductID], row)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanAndGroupDetails(db *gorm.DB) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
rows := make([]SapronakDetailRow, 0)
|
||||||
|
if err := db.Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return groupSapronakDetails(rows), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Usage (summary + details)
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) usageQuery(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) *gorm.DB {
|
||||||
|
db := r.withCtx(ctx).Table(table).Select(`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
COALESCE(SUM(usage_qty), 0) AS qty,
|
||||||
|
COALESCE(p.product_price, 0) AS default_price
|
||||||
|
`)
|
||||||
|
db = applyJoins(db, joins...)
|
||||||
|
return db.
|
||||||
|
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Where(where, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) fetchSapronakUsage(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) ([]SapronakUsageRow, error) {
|
||||||
|
rows := make([]SapronakUsageRow, 0)
|
||||||
|
db := r.usageQuery(ctx, table, pwJoinCond, joins, where, args...)
|
||||||
|
if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) detailQuery(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
selectSQL string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) *gorm.DB {
|
||||||
|
db := r.withCtx(ctx).
|
||||||
|
Table(table).
|
||||||
|
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct)
|
||||||
|
|
||||||
|
db = applyJoins(db, joins...)
|
||||||
|
return db.Select(selectSQL).Where(where, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) fetchSapronakDetails(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
selectSQL string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return scanAndGroupDetails(r.detailQuery(ctx, table, pwJoinCond, joins, selectSQL, where, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||||
|
if pfkID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return r.fetchSapronakUsage(
|
||||||
|
ctx,
|
||||||
|
"recording_stocks rs",
|
||||||
|
"pw.id = rs.product_warehouse_id",
|
||||||
|
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
|
||||||
|
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsUsage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||||
|
if pfkID == 0 {
|
||||||
|
return []SapronakUsageRow{}, nil
|
||||||
|
}
|
||||||
|
return r.fetchSapronakUsage(
|
||||||
|
ctx,
|
||||||
|
"project_chickins pc",
|
||||||
|
"pw.id = pc.product_warehouse_id",
|
||||||
|
nil,
|
||||||
|
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsChickin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return r.fetchSapronakDetails(
|
||||||
|
ctx,
|
||||||
|
"recording_stocks rs",
|
||||||
|
"pw.id = rs.product_warehouse_id",
|
||||||
|
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"}, // penting: supaya alias r valid
|
||||||
|
`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
r.record_datetime AS date,
|
||||||
|
CAST(r.id AS TEXT) AS reference,
|
||||||
|
0 AS qty_in,
|
||||||
|
COALESCE(rs.usage_qty,0) AS qty_out,
|
||||||
|
COALESCE(p.product_price,0) AS price
|
||||||
|
`,
|
||||||
|
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsUsage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return r.fetchSapronakDetails(
|
||||||
|
ctx,
|
||||||
|
"project_chickins pc",
|
||||||
|
"pw.id = pc.product_warehouse_id",
|
||||||
|
nil,
|
||||||
|
`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
pc.chick_in_date AS date,
|
||||||
|
CAST(pc.id AS TEXT) AS reference,
|
||||||
|
0 AS qty_in,
|
||||||
|
COALESCE(pc.usage_qty,0) AS qty_out,
|
||||||
|
COALESCE(p.product_price,0) AS price
|
||||||
|
`,
|
||||||
|
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsChickin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
|
||||||
|
return r.withCtx(ctx).
|
||||||
|
Table("purchase_items AS pi").
|
||||||
|
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
|
||||||
|
Joins("JOIN products p ON p.id = pi.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
|
Where("pi.received_date IS NOT NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
|
||||||
|
rows := make([]SapronakIncomingRow, 0)
|
||||||
|
db := r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||||
|
pi.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
COALESCE(SUM(pi.total_qty), 0) AS qty,
|
||||||
|
COALESCE(SUM(pi.total_qty * pi.price), 0) AS value,
|
||||||
|
COALESCE(p.product_price, 0) AS default_price
|
||||||
|
`)
|
||||||
|
if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return scanAndGroupDetails(
|
||||||
|
r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||||
|
pi.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
pi.received_date AS date,
|
||||||
|
COALESCE(po.po_number, '') AS reference,
|
||||||
|
COALESCE(pi.total_qty,0) AS qty_in,
|
||||||
|
0 AS qty_out,
|
||||||
|
COALESCE(pi.price,0) AS price
|
||||||
|
`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stockLogSapronakRow struct {
|
||||||
|
ID uint `gorm:"column:id"`
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
Flag string `gorm:"column:flag"`
|
||||||
|
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||||
|
Increase float64 `gorm:"column:increase"`
|
||||||
|
Decrease float64 `gorm:"column:decrease"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
MovementNumber string `gorm:"column:movement_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID uint, logType any, withMovement bool) ([]stockLogSapronakRow, error) {
|
||||||
|
rows := make([]stockLogSapronakRow, 0)
|
||||||
|
|
||||||
|
movementSelect := "'' AS movement_number"
|
||||||
|
joins := []string{}
|
||||||
|
if withMovement {
|
||||||
|
movementSelect = "COALESCE(st.movement_number,'') AS movement_number"
|
||||||
|
joins = append(joins, "JOIN stock_transfers st ON st.id = sl.loggable_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := r.withCtx(ctx).
|
||||||
|
Table("stock_logs sl").
|
||||||
|
Select(`
|
||||||
|
sl.id AS id,
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
sl.created_at AS created_at,
|
||||||
|
COALESCE(sl.increase,0) AS increase,
|
||||||
|
COALESCE(sl.decrease,0) AS decrease,
|
||||||
|
COALESCE(p.product_price,0) AS price,
|
||||||
|
` + movementSelect + `
|
||||||
|
`).
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
|
||||||
|
|
||||||
|
db = applyJoins(db, joins...)
|
||||||
|
|
||||||
|
if err := db.
|
||||||
|
Where("sl.loggable_type = ?", logType).
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) string) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow) {
|
||||||
|
in := make(map[uint][]SapronakDetailRow)
|
||||||
|
out := make(map[uint][]SapronakDetailRow)
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
base := SapronakDetailRow{
|
||||||
|
ProductID: row.ProductID,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
Flag: row.Flag,
|
||||||
|
Date: row.CreatedAt,
|
||||||
|
Reference: refFn(row),
|
||||||
|
Price: row.Price,
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.Increase > 0 {
|
||||||
|
d := base
|
||||||
|
d.QtyIn = row.Increase
|
||||||
|
in[row.ProductID] = append(in[row.ProductID], d)
|
||||||
|
}
|
||||||
|
if row.Decrease > 0 {
|
||||||
|
d := base
|
||||||
|
d.QtyOut = row.Decrease
|
||||||
|
out[row.ProductID] = append(out[row.ProductID], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return in, out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||||
|
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) })
|
||||||
|
return in, out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||||
|
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
|
||||||
|
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("TRF-%d", row.ID)
|
||||||
|
})
|
||||||
|
return in, out, nil
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
|
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
||||||
ctrl := controller.NewClosingController(s)
|
ctrl := controller.NewClosingController(s, sapronakSvc)
|
||||||
|
|
||||||
route := v1.Group("/closings")
|
route := v1.Group("/closings")
|
||||||
|
|
||||||
@@ -21,5 +21,10 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
||||||
|
route.Get("/:project_flock_id/overhead", ctrl.GetOverhead)
|
||||||
|
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang)
|
||||||
|
route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject)
|
||||||
|
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
||||||
|
route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
|
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -15,21 +25,39 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClosingService interface {
|
type ClosingService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||||
|
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
|
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||||
|
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
||||||
|
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type closingService struct {
|
type closingService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ClosingRepository
|
Repository repository.ClosingRepository
|
||||||
|
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||||
|
MarketingRepo marketingRepository.MarketingRepository
|
||||||
|
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||||
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
|
||||||
|
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
|
||||||
|
ChickinRepo chickinRepository.ProjectChickinRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingService(repo repository.ClosingRepository, validate *validator.Validate) ClosingService {
|
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService {
|
||||||
return &closingService{
|
return &closingService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
|
MarketingRepo: marketingRepo,
|
||||||
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
|
ApprovalSvc: approvalSvc,
|
||||||
|
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||||
|
ProjectBudgetRepo: projectBudgetRepo,
|
||||||
|
ChickinRepo: chickinRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +65,14 @@ func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
return db.Preload("CreatedUser")
|
return db.Preload("CreatedUser")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.withRelations(db).
|
||||||
|
Preload("Location").
|
||||||
|
Preload("KandangHistory").
|
||||||
|
Preload("KandangHistory.Chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -45,9 +80,9 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withClosingRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
return db.Where("flock_name LIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -56,17 +91,291 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
s.Log.Errorf("Failed to get closings: %+v", err)
|
s.Log.Errorf("Failed to get closings: %+v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return closings, total, nil
|
|
||||||
|
result := make([]dto.ClosingListItemDTO, 0, len(closings))
|
||||||
|
for _, closing := range closings {
|
||||||
|
statusProject, _, err := s.getApprovalStatuses(c.Context(), closing.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", closing.Id, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, dto.ToClosingListItemDTO(closing, statusProject))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
||||||
closing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Closing not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed get closing by id: %+v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return closing, nil
|
return projectFlock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
|
||||||
|
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("MarketingProduct").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||||
|
Preload("MarketingProduct.Marketing").
|
||||||
|
Preload("MarketingProduct.Marketing.Customer").
|
||||||
|
Order("marketing_delivery_products.delivery_date DESC")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(realisasi) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found")
|
||||||
|
}
|
||||||
|
return realisasi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get project flock %d for closing summary: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := dto.ToClosingSummaryDTO(*project, statusProject, statusClosing)
|
||||||
|
|
||||||
|
return &summary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if params == nil {
|
||||||
|
params = &validation.ClosingSapronakQuery{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Page == 0 {
|
||||||
|
params.Page = 1
|
||||||
|
}
|
||||||
|
if params.Limit == 0 {
|
||||||
|
params.Limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectFlockKandangIDs []uint
|
||||||
|
if params.Type == validation.SapronakTypeOutgoing {
|
||||||
|
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
rows, totalResults, err := s.Repository.GetSapronak(c.Context(), repository.SapronakQueryParams{
|
||||||
|
Type: params.Type,
|
||||||
|
WarehouseIDs: warehouseIDs,
|
||||||
|
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||||
|
Limit: params.Limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak data")
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.ClosingSapronakItemDTO, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
dateStr := row.DateText
|
||||||
|
if dateStr == "" && !row.SortDate.IsZero() {
|
||||||
|
dateStr = row.SortDate.Format("02-Jan-2006")
|
||||||
|
}
|
||||||
|
items = append(items, dto.ClosingSapronakItemDTO{
|
||||||
|
Id: row.Id,
|
||||||
|
Date: dateStr,
|
||||||
|
ReferenceNumber: row.ReferenceNumber,
|
||||||
|
TransactionType: row.TransactionType,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
ProductCategory: row.ProductCategory,
|
||||||
|
ProductSubCategory: row.ProductSubCategory,
|
||||||
|
SourceWarehouse: row.SourceWarehouse,
|
||||||
|
DestinationWarehouse: row.DestinationWarehouse,
|
||||||
|
// Destination: row.Destination,
|
||||||
|
Quantity: row.Quantity,
|
||||||
|
Unit: row.Unit,
|
||||||
|
FormattedQuantity: formatQuantity(row.Quantity, row.Unit),
|
||||||
|
Notes: row.Notes,
|
||||||
|
SortDate: row.SortDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, totalResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
|
var kandangIDs []uint
|
||||||
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
if err := db.Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Pluck("kandang_id", &kandangIDs).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(kandangIDs) == 0 {
|
||||||
|
return []uint{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var warehouses []entity.Warehouse
|
||||||
|
if err := db.Where("kandang_id IN ?", kandangIDs).Find(&warehouses).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unique := make(map[uint]struct{})
|
||||||
|
for _, warehouse := range warehouses {
|
||||||
|
unique[warehouse.Id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]uint, 0, len(unique))
|
||||||
|
for id := range unique {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
|
var ids []uint
|
||||||
|
err := s.Repository.DB().WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Pluck("id", &ids).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatQuantity(qty float64, uom string) string {
|
||||||
|
qtyStr := strconv.FormatFloat(qty, 'f', -1, 64)
|
||||||
|
if uom == "" {
|
||||||
|
return qtyStr
|
||||||
|
}
|
||||||
|
return qtyStr + " " + uom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) {
|
||||||
|
if s.ApprovalSvc == nil {
|
||||||
|
return "", "Belum Selesai", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
minStep uint16
|
||||||
|
statusProject string
|
||||||
|
completed int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, rec := range records {
|
||||||
|
if minStep == 0 || rec.StepNumber < minStep {
|
||||||
|
minStep = rec.StepNumber
|
||||||
|
statusProject = rec.StepName
|
||||||
|
}
|
||||||
|
if rec.StepNumber == uint16(utils.ProjectFlockStepAktif) {
|
||||||
|
completed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusProject == "" && minStep > 0 {
|
||||||
|
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, approvalutils.ApprovalStep(minStep)); ok {
|
||||||
|
statusProject = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusClosing := "Belum Selesai"
|
||||||
|
switch {
|
||||||
|
case len(records) == 0 || completed == 0:
|
||||||
|
statusClosing = "Belum Selesai"
|
||||||
|
case completed < len(records):
|
||||||
|
statusClosing = "Sebagian"
|
||||||
|
default:
|
||||||
|
statusClosing = "Selesai"
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusProject, statusClosing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
|
||||||
|
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalChickinQty float64
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
totalChickinQty += chickin.UsageQty
|
||||||
|
}
|
||||||
|
|
||||||
|
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty)
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,681 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SapronakService interface {
|
||||||
|
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
|
||||||
|
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sapronakService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.ClosingRepository
|
||||||
|
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSapronakService(
|
||||||
|
repo repository.ClosingRepository,
|
||||||
|
pfkRepo projectflockRepository.ProjectFlockKandangRepository,
|
||||||
|
validate *validator.Validate,
|
||||||
|
) SapronakService {
|
||||||
|
return &sapronakService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
ProjectFlockKandangRepo: pfkRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
||||||
|
}
|
||||||
|
reports, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||||
|
ProjectFlockID: projectFlockID,
|
||||||
|
Status: "all",
|
||||||
|
Flag: flag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(reports) <= 1 {
|
||||||
|
return reports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
combined := s.combineSapronakReports(reports, projectFlockID)
|
||||||
|
return []dto.SapronakReportDTO{combined}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error) {
|
||||||
|
if projectFlockID == 0 || pfkID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||||
|
ProjectFlockID: projectFlockID,
|
||||||
|
ProjectFlockKandangID: pfkID,
|
||||||
|
Status: "all",
|
||||||
|
Flag: flag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, res := range results {
|
||||||
|
if res.ProjectFlockID == projectFlockID && res.ProjectFlockKandangID == pfkID {
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||||
|
pfks, err := s.loadProjectFlockKandangs(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(pfks) == 0 {
|
||||||
|
return []dto.SapronakReportDTO{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filterStatus := strings.ToLower(strings.TrimSpace(params.Status))
|
||||||
|
if filterStatus == "" {
|
||||||
|
filterStatus = "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]dto.SapronakReportDTO, 0, len(pfks))
|
||||||
|
for _, pfk := range pfks {
|
||||||
|
status := "closing"
|
||||||
|
if pfk.ClosedAt == nil {
|
||||||
|
status = "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||||
|
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, dto.SapronakReportDTO{
|
||||||
|
ProjectFlockKandangID: pfk.Id,
|
||||||
|
ProjectFlockID: pfk.ProjectFlockId,
|
||||||
|
ProjectName: pfk.ProjectFlock.FlockName,
|
||||||
|
KandangID: pfk.KandangId,
|
||||||
|
KandangName: pfk.Kandang.Name,
|
||||||
|
Period: pfk.Period,
|
||||||
|
Status: status,
|
||||||
|
StartDate: nil,
|
||||||
|
EndDate: nil,
|
||||||
|
TotalIncomingValue: totalIncoming,
|
||||||
|
TotalUsageValue: totalUsage,
|
||||||
|
Items: items,
|
||||||
|
Groups: groups,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||||
|
db := s.ProjectFlockKandangRepo.DB().WithContext(ctx).
|
||||||
|
Preload("ProjectFlock").
|
||||||
|
Preload("Kandang").
|
||||||
|
Preload("Chickins")
|
||||||
|
|
||||||
|
if params != nil {
|
||||||
|
if params.ProjectFlockID > 0 {
|
||||||
|
db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockID)
|
||||||
|
}
|
||||||
|
if params.KandangID > 0 {
|
||||||
|
db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangID)
|
||||||
|
}
|
||||||
|
if params.ProjectFlockKandangID > 0 {
|
||||||
|
db = db.Where("project_flock_kandangs.id = ?", params.ProjectFlockKandangID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pfks []entity.ProjectFlockKandang
|
||||||
|
if err := db.Find(&pfks).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandangs")
|
||||||
|
}
|
||||||
|
return pfks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO, projectID uint) dto.SapronakReportDTO {
|
||||||
|
if len(reports) == 0 {
|
||||||
|
return dto.SapronakReportDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
totalIncoming float64
|
||||||
|
totalUsage float64
|
||||||
|
projectName = reports[0].ProjectName
|
||||||
|
)
|
||||||
|
|
||||||
|
itemMap := make(map[uint]dto.SapronakItemDTO)
|
||||||
|
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||||
|
|
||||||
|
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||||
|
if g, ok := groupMap[flag]; ok {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||||
|
return groupMap[flag]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range reports {
|
||||||
|
totalIncoming += r.TotalIncomingValue
|
||||||
|
totalUsage += r.TotalUsageValue
|
||||||
|
|
||||||
|
for _, it := range r.Items {
|
||||||
|
cur := itemMap[it.ProductID]
|
||||||
|
if cur.ProductID == 0 {
|
||||||
|
cur.ProductID = it.ProductID
|
||||||
|
cur.ProductName = it.ProductName
|
||||||
|
cur.Flag = it.Flag
|
||||||
|
}
|
||||||
|
cur.IncomingQty += it.IncomingQty
|
||||||
|
cur.IncomingValue += it.IncomingValue
|
||||||
|
cur.UsageQty += it.UsageQty
|
||||||
|
cur.UsageValue += it.UsageValue
|
||||||
|
if cur.IncomingQty >= cur.UsageQty {
|
||||||
|
cur.RemainingQty = cur.IncomingQty - cur.UsageQty
|
||||||
|
} else {
|
||||||
|
cur.RemainingQty = 0
|
||||||
|
}
|
||||||
|
if cur.IncomingQty > 0 {
|
||||||
|
cur.AveragePrice = cur.IncomingValue / cur.IncomingQty
|
||||||
|
} else {
|
||||||
|
cur.AveragePrice = it.AveragePrice
|
||||||
|
}
|
||||||
|
itemMap[it.ProductID] = cur
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, g := range r.Groups {
|
||||||
|
agg := ensureGroup(g.Flag)
|
||||||
|
agg.TotalMasuk += g.TotalMasuk
|
||||||
|
agg.TotalKeluar += g.TotalKeluar
|
||||||
|
agg.SaldoAkhir += g.SaldoAkhir
|
||||||
|
agg.TotalNilai += g.TotalNilai
|
||||||
|
agg.Items = append(agg.Items, g.Items...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||||
|
for _, it := range itemMap {
|
||||||
|
items = append(items, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||||
|
for _, g := range groupMap {
|
||||||
|
groups = append(groups, *g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto.SapronakReportDTO{
|
||||||
|
ProjectFlockID: projectID,
|
||||||
|
ProjectName: projectName,
|
||||||
|
Status: "combined",
|
||||||
|
StartDate: nil,
|
||||||
|
TotalIncomingValue: totalIncoming,
|
||||||
|
TotalUsageValue: totalUsage,
|
||||||
|
Items: items,
|
||||||
|
Groups: groups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapIncomingUsage(incomingRows []repository.SapronakIncomingRow, usageRows []repository.SapronakUsageRow) (map[uint]repository.SapronakIncomingRow, map[uint]repository.SapronakUsageRow) {
|
||||||
|
incoming := make(map[uint]repository.SapronakIncomingRow, len(incomingRows))
|
||||||
|
for _, row := range incomingRows {
|
||||||
|
incoming[row.ProductID] = row
|
||||||
|
}
|
||||||
|
usage := make(map[uint]repository.SapronakUsageRow, len(usageRows))
|
||||||
|
for _, row := range usageRows {
|
||||||
|
usage[row.ProductID] = row
|
||||||
|
}
|
||||||
|
return incoming, usage
|
||||||
|
}
|
||||||
|
|
||||||
|
type sapronakDetailMaps struct {
|
||||||
|
Incoming map[uint][]dto.SapronakDetailDTO
|
||||||
|
Usage map[uint][]dto.SapronakDetailDTO
|
||||||
|
AdjIncoming map[uint][]dto.SapronakDetailDTO
|
||||||
|
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
||||||
|
TransferIn map[uint][]dto.SapronakDetailDTO
|
||||||
|
TransferOut map[uint][]dto.SapronakDetailDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSapronakDetails(
|
||||||
|
incomingRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
usageRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
adjIncomingRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
transferInRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
transferOutRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
) sapronakDetailMaps {
|
||||||
|
result := sapronakDetailMaps{
|
||||||
|
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
Usage: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
AdjIncoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
}
|
||||||
|
|
||||||
|
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
||||||
|
for pid, rows := range src {
|
||||||
|
for _, r := range rows {
|
||||||
|
d := dto.SapronakDetailDTO{
|
||||||
|
ProductID: r.ProductID,
|
||||||
|
ProductName: r.ProductName,
|
||||||
|
Flag: r.Flag,
|
||||||
|
Tanggal: r.Date,
|
||||||
|
NoReferensi: r.Reference,
|
||||||
|
JenisTransaksi: jenis,
|
||||||
|
Harga: r.Price,
|
||||||
|
}
|
||||||
|
if masuk {
|
||||||
|
d.QtyMasuk = r.QtyIn
|
||||||
|
d.Nilai = r.QtyIn * r.Price
|
||||||
|
} else {
|
||||||
|
d.QtyKeluar = r.QtyOut
|
||||||
|
d.Nilai = r.QtyOut * r.Price
|
||||||
|
}
|
||||||
|
target[pid] = append(target[pid], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRows(result.Incoming, incomingRows, "Pembelian", true)
|
||||||
|
addRows(result.Usage, usageRows, "Pemakaian", false)
|
||||||
|
addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true)
|
||||||
|
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||||
|
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||||
|
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||||
|
// For sapronak closing report we intentionally ignore date range
|
||||||
|
// and aggregate all historical transactions for the kandang/project.
|
||||||
|
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
usageRows, err := s.Repository.FetchSapronakUsage(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
chickinUsageRows, err := s.Repository.FetchSapronakChickinUsage(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
usageDetailsRows, err := s.Repository.FetchSapronakUsageDetails(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
chickinUsageDetailsRows, err := s.Repository.FetchSapronakChickinUsageDetails(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
transIncomingRows, transOutgoingRows, err := s.Repository.FetchSapronakTransfers(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
||||||
|
matchesFlag := func(f string) bool {
|
||||||
|
if filterFlag == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.ToUpper(f) == filterFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
// For project flocks with category GROWING, pullet usage from chickin
|
||||||
|
// should not be counted yet. Only when category is LAYING we allow
|
||||||
|
// pullet usage to contribute to qty_used.
|
||||||
|
isLaying := strings.EqualFold(string(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying))
|
||||||
|
|
||||||
|
if !isLaying {
|
||||||
|
filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
|
||||||
|
for _, row := range chickinUsageRows {
|
||||||
|
if strings.ToUpper(row.Flag) == "DOC" {
|
||||||
|
filteredUsage = append(filteredUsage, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chickinUsageRows = filteredUsage
|
||||||
|
|
||||||
|
filteredDetail := make(map[uint][]repository.SapronakDetailRow, len(chickinUsageDetailsRows))
|
||||||
|
for pid, rows := range chickinUsageDetailsRows {
|
||||||
|
for _, d := range rows {
|
||||||
|
if strings.ToUpper(d.Flag) == "DOC" {
|
||||||
|
filteredDetail[pid] = append(filteredDetail[pid], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chickinUsageDetailsRows = filteredDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsageRows := append(usageRows, chickinUsageRows...)
|
||||||
|
incoming, usage := mapIncomingUsage(incomingRows, allUsageRows)
|
||||||
|
itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage))
|
||||||
|
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||||
|
|
||||||
|
for pid, rows := range chickinUsageDetailsRows {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
||||||
|
}
|
||||||
|
|
||||||
|
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
|
||||||
|
incomingDetails := detailMaps.Incoming
|
||||||
|
usageDetails := detailMaps.Usage
|
||||||
|
adjIncoming := detailMaps.AdjIncoming
|
||||||
|
adjOutgoing := detailMaps.AdjOutgoing
|
||||||
|
transIncoming := detailMaps.TransferIn
|
||||||
|
transOutgoing := detailMaps.TransferOut
|
||||||
|
|
||||||
|
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||||
|
if g, ok := groupMap[flag]; ok {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||||
|
return groupMap[flag]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range incoming {
|
||||||
|
if !matchesFlag(row.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
avgPrice := row.DefaultPrice
|
||||||
|
if row.Qty > 0 && row.Value > 0 {
|
||||||
|
avgPrice = row.Value / row.Qty
|
||||||
|
}
|
||||||
|
|
||||||
|
itemMap[row.ProductID] = dto.SapronakItemDTO{
|
||||||
|
ProductID: row.ProductID,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
Flag: row.Flag,
|
||||||
|
IncomingQty: row.Qty,
|
||||||
|
IncomingValue: row.Value,
|
||||||
|
RemainingQty: row.Qty,
|
||||||
|
AveragePrice: avgPrice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range usage {
|
||||||
|
if !matchesFlag(row.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[row.ProductID]
|
||||||
|
price := existing.AveragePrice
|
||||||
|
if price == 0 {
|
||||||
|
price = row.DefaultPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
usageValue := row.Qty * price
|
||||||
|
|
||||||
|
existing.ProductID = row.ProductID
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = row.ProductName
|
||||||
|
}
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = row.Flag
|
||||||
|
}
|
||||||
|
existing.AveragePrice = price
|
||||||
|
existing.UsageQty += row.Qty
|
||||||
|
existing.UsageValue += usageValue
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
itemMap[row.ProductID] = existing
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjIncoming {
|
||||||
|
for _, d := range details {
|
||||||
|
if !matchesFlag(d.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[productID]
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = d.Flag
|
||||||
|
}
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = d.ProductName
|
||||||
|
}
|
||||||
|
existing.IncomingQty += d.QtyMasuk
|
||||||
|
existing.IncomingValue += d.Nilai
|
||||||
|
if existing.IncomingQty > 0 {
|
||||||
|
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||||
|
}
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
itemMap[productID] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjOutgoing {
|
||||||
|
for _, d := range details {
|
||||||
|
if !matchesFlag(d.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[productID]
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = d.Flag
|
||||||
|
}
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = d.ProductName
|
||||||
|
}
|
||||||
|
existing.UsageQty += d.QtyKeluar
|
||||||
|
existing.UsageValue += d.Nilai
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
itemMap[productID] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range transIncoming {
|
||||||
|
for _, d := range details {
|
||||||
|
if !matchesFlag(d.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[productID]
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = d.Flag
|
||||||
|
}
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = d.ProductName
|
||||||
|
}
|
||||||
|
existing.IncomingQty += d.QtyMasuk
|
||||||
|
existing.IncomingValue += d.Nilai
|
||||||
|
if existing.IncomingQty > 0 {
|
||||||
|
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||||
|
}
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
itemMap[productID] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||||
|
var totalIncoming, totalUsage float64
|
||||||
|
for _, item := range itemMap {
|
||||||
|
totalIncoming += item.IncomingValue
|
||||||
|
totalUsage += item.UsageValue
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range incomingDetails {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalMasuk += d.QtyMasuk
|
||||||
|
group.TotalNilai += d.Nilai
|
||||||
|
group.SaldoAkhir += d.QtyMasuk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjIncoming {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalMasuk += d.QtyMasuk
|
||||||
|
group.TotalNilai += d.Nilai
|
||||||
|
group.SaldoAkhir += d.QtyMasuk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range usageDetails {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjOutgoing {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range transIncoming {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalMasuk += d.QtyMasuk
|
||||||
|
group.TotalNilai += d.Nilai
|
||||||
|
group.SaldoAkhir += d.QtyMasuk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range transOutgoing {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||||
|
for _, g := range groupMap {
|
||||||
|
groups = append(groups, *g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, groups, totalIncoming, totalUsage, nil
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `json:"name" validate:"required_strict,min=3"`
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
@@ -13,3 +13,14 @@ type Query struct {
|
|||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SapronakTypeIncoming = "incoming"
|
||||||
|
SapronakTypeOutgoing = "outgoing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClosingSapronakQuery struct {
|
||||||
|
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type CountSapronakQuery struct {
|
||||||
|
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
|
||||||
|
KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||||
|
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
|
||||||
|
Status string `query:"status" validate:"omitempty,oneof=active closing all"`
|
||||||
|
Flag string `query:"flag" validate:"omitempty,oneof=DOC OVK PAKAN PULLET doc ovk pakan pullet"`
|
||||||
|
}
|
||||||
@@ -151,12 +151,16 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Documents = form.File["documents"]
|
req.Documents = form.File["documents"]
|
||||||
if transactionDate := c.FormValue("transaction_date"); transactionDate != "" {
|
|
||||||
|
transactionDate := c.FormValue("transaction_date")
|
||||||
|
if transactionDate != "" {
|
||||||
req.TransactionDate = &transactionDate
|
req.TransactionDate = &transactionDate
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryVal := c.FormValue("category")
|
categoryVal := c.FormValue("category")
|
||||||
req.Category = &categoryVal
|
if categoryVal != "" {
|
||||||
|
req.Category = &categoryVal
|
||||||
|
}
|
||||||
|
|
||||||
supplierIDVal := c.FormValue("supplier_id")
|
supplierIDVal := c.FormValue("supplier_id")
|
||||||
if supplierIDVal != "" {
|
if supplierIDVal != "" {
|
||||||
@@ -312,13 +316,18 @@ func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
req.Documents = form.File["documents"]
|
req.Documents = form.File["documents"]
|
||||||
|
|
||||||
req.RealizationDate = c.FormValue("realization_date")
|
realizationDate := c.FormValue("realization_date")
|
||||||
|
if realizationDate != "" {
|
||||||
|
req.RealizationDate = &realizationDate
|
||||||
|
}
|
||||||
|
|
||||||
realizationsJSON := c.FormValue("realizations")
|
realizationsJSON := c.FormValue("realizations")
|
||||||
if realizationsJSON != "" {
|
if realizationsJSON != "" {
|
||||||
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
|
var realizations []validation.RealizationItem
|
||||||
|
if err := json.Unmarshal([]byte(realizationsJSON), &realizations); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
|
||||||
}
|
}
|
||||||
|
req.Realizations = &realizations
|
||||||
}
|
}
|
||||||
|
|
||||||
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
|
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ type ExpenseRepository interface {
|
|||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
GetNextSequence(ctx context.Context) (int, error)
|
GetNextSequence(ctx context.Context) (int, error)
|
||||||
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
|
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
|
||||||
|
WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB
|
||||||
|
CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRepositoryImpl struct {
|
type ExpenseRepositoryImpl struct {
|
||||||
@@ -49,3 +53,57 @@ func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64)
|
|||||||
}
|
}
|
||||||
return &expense, nil
|
return &expense, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if pfkID == 0 && kandangID == 0 {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
q := db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id")
|
||||||
|
if pfkID > 0 && kandangID > 0 {
|
||||||
|
return q.Where("expense_nonstocks.project_flock_kandang_id = ? OR expense_nonstocks.kandang_id = ?", pfkID, kandangID)
|
||||||
|
}
|
||||||
|
if pfkID > 0 {
|
||||||
|
return q.Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID)
|
||||||
|
}
|
||||||
|
return q.Where("expense_nonstocks.kandang_id = ?", kandangID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) {
|
||||||
|
if pfkID == 0 && kandangID == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []uint64
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Table("expenses").
|
||||||
|
Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)).
|
||||||
|
Group("expenses.id").Where("expenses.deleted_at IS NULL").
|
||||||
|
Pluck("expenses.id", &ids).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var unfinished int64
|
||||||
|
for _, id := range ids {
|
||||||
|
var latest entity.Approval
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("approvals").
|
||||||
|
Where("approvable_type = ? AND approvable_id = ?", utils.ApprovalWorkflowExpense.String(), id).
|
||||||
|
Order("action_at DESC").
|
||||||
|
Limit(1).
|
||||||
|
First(&latest).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if isFinished != nil {
|
||||||
|
if !isFinished(&latest) {
|
||||||
|
unfinished++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unfinished, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type ExpenseRealizationRepository interface {
|
|||||||
repository.BaseRepository[entity.ExpenseRealization]
|
repository.BaseRepository[entity.ExpenseRealization]
|
||||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
IdExists(ctx context.Context, id uint64) (bool, error)
|
||||||
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
||||||
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRealizationRepositoryImpl struct {
|
type ExpenseRealizationRepositoryImpl struct {
|
||||||
@@ -30,11 +31,22 @@ func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint
|
|||||||
|
|
||||||
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
|
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
|
||||||
var realization entity.ExpenseRealization
|
var realization entity.ExpenseRealization
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).Where("expense_nonstock_id = ?", expenseNonstockID).First(&realization).Error
|
||||||
Where("expense_nonstock_id = ?", expenseNonstockID).
|
return &realization, err
|
||||||
First(&realization).Error
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error) {
|
||||||
}
|
var realizations []entity.ExpenseRealization
|
||||||
return &realization, nil
|
err := r.DB().WithContext(ctx).
|
||||||
|
Preload("ExpenseNonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock.Uom").
|
||||||
|
Preload("ExpenseNonstock.Expense").
|
||||||
|
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
||||||
|
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
|
||||||
|
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
||||||
|
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||||
|
Where("expenses.category = ?", "BOP").
|
||||||
|
Find(&realizations).Error
|
||||||
|
return realizations, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
|
|||||||
|
|
||||||
route := v1.Group("/expenses")
|
route := v1.Group("/expenses")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
|
// route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||||
@@ -183,12 +184,16 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction)
|
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction)
|
||||||
|
|
||||||
referenceNumber, err := s.generateReferenceNumber(dbTransaction)
|
referenceNumber, err := GenerateExpenseReferenceNumber(c.Context(), expenseRepoTx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
||||||
}
|
}
|
||||||
|
|
||||||
createdBy := uint64(1) //todo get from auth
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
createdBy := uint64(actorID)
|
||||||
expense = &entity.Expense{
|
expense = &entity.Expense{
|
||||||
ReferenceNumber: referenceNumber,
|
ReferenceNumber: referenceNumber,
|
||||||
PoNumber: req.PoNumber,
|
PoNumber: req.PoNumber,
|
||||||
@@ -360,6 +365,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
categoryChanged := false
|
categoryChanged := false
|
||||||
var newCategory string
|
var newCategory string
|
||||||
if req.Category != nil && *req.Category != currentExpense.Category {
|
if req.Category != nil && *req.Category != currentExpense.Category {
|
||||||
@@ -404,6 +412,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if ens.KandangId != nil {
|
if ens.KandangId != nil {
|
||||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
||||||
}
|
}
|
||||||
@@ -496,7 +507,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
||||||
|
|
||||||
approvalAction := entity.ApprovalActionUpdated
|
approvalAction := entity.ApprovalActionUpdated
|
||||||
@@ -543,7 +557,21 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get expense for ID %d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||||
@@ -572,6 +600,20 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
@@ -655,7 +697,10 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
|
||||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -712,7 +757,19 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
||||||
@@ -732,10 +789,10 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
|
|
||||||
// Check if only updating documents
|
// Check if only updating documents
|
||||||
updateDataOnly := len(req.Realizations) == 0 && len(req.Documents) > 0
|
updateDataOnly := req.Realizations == nil && len(req.Documents) > 0
|
||||||
|
|
||||||
if len(req.Realizations) > 0 {
|
if req.Realizations != nil {
|
||||||
for _, realizationItem := range req.Realizations {
|
for _, realizationItem := range *req.Realizations {
|
||||||
|
|
||||||
expenseNonstockID := realizationItem.ExpenseNonstockID
|
expenseNonstockID := realizationItem.ExpenseNonstockID
|
||||||
|
|
||||||
@@ -770,6 +827,12 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.RealizationDate != nil {
|
||||||
|
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{"realization_date": *req.RealizationDate}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(req.Documents) > 0 {
|
if len(req.Documents) > 0 {
|
||||||
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -954,11 +1017,14 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "No expense IDs provided")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "No expense IDs provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
|
||||||
var results []expenseDto.ExpenseDetailDTO
|
var results []expenseDto.ExpenseDetailDTO
|
||||||
|
|
||||||
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
@@ -1004,6 +1070,21 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
} else {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
||||||
}
|
}
|
||||||
|
if approvalAction == entity.ApprovalActionApproved {
|
||||||
|
expense, err := expenseRepoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
@@ -1050,17 +1131,6 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *expenseService) generateReferenceNumber(ctx *gorm.DB) (string, error) {
|
|
||||||
|
|
||||||
sequence, err := s.Repository.GetNextSequence(ctx.Statement.Context)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
refNum := fmt.Sprintf("BOP-LTI-%05d", sequence)
|
|
||||||
|
|
||||||
return refNum, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) {
|
func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) {
|
||||||
|
|
||||||
expenseRepoTx := repository.NewExpenseRepository(ctx)
|
expenseRepoTx := repository.NewExpenseRepository(ctx)
|
||||||
@@ -1084,13 +1154,45 @@ func (s *expenseService) validateExpenseNonstockRelation(ctx *fiber.Ctx, expense
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func actorIDFromContext(c *fiber.Ctx) (uint, error) {
|
func (s *expenseService) ensureProjectFlockNotClosedForExpense(
|
||||||
// user, ok := authmiddleware.AuthenticatedUser(c)
|
ctx context.Context,
|
||||||
// if !ok || user == nil || user.Id == 0 {
|
expense *entity.Expense,
|
||||||
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
) error {
|
||||||
// }
|
// Kalau repo belum di-wire atau expense kosong → gak usah ngecek apa-apa
|
||||||
// return user.Id, nil
|
if s.ProjectFlockKandangRepo == nil || expense == nil {
|
||||||
// }
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// return user.Id, nil
|
seen := make(map[uint]struct{})
|
||||||
// }
|
|
||||||
|
for _, ens := range expense.Nonstocks {
|
||||||
|
// Field ini pointer, bisa nil
|
||||||
|
if ens.ProjectFlockKandangId == nil || *ens.ProjectFlockKandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pfkID := uint(*ens.ProjectFlockKandangId)
|
||||||
|
if _, ok := seen[pfkID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[pfkID] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, pfkID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Project flock %d tidak ditemukan", pfkID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to validate project flock %d for expense %d: %+v", pfkID, expense.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
// ❗ RULE: kalau ClosedAt tidak nil → project sudah closing
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateExpenseReferenceNumber builds a new reference number using the expense sequence.
|
||||||
|
func GenerateExpenseReferenceNumber(ctx context.Context, repo expenseRepo.ExpenseRepository) (string, error) {
|
||||||
|
sequence, err := repo.GetNextSequence(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("BOP-LTI-%05d", sequence), nil
|
||||||
|
}
|
||||||
@@ -5,11 +5,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
|
PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
|
||||||
TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
|
TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
|
||||||
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
|
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
|
||||||
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
|
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
ExpenseNonstocks []ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"required,min=1,dive"`
|
ExpenseNonstocks []ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,11 +26,11 @@ type CostItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
|
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||||
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||||
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
@@ -46,9 +46,9 @@ type CreateRealization struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateRealization struct {
|
type UpdateRealization struct {
|
||||||
RealizationDate string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
|
RealizationDate *string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
|
Realizations *[]RealizationItem `form:"realizations" json:"realizations" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RealizationItem struct {
|
type RealizationItem struct {
|
||||||
|
|||||||
@@ -104,12 +104,12 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
|
|||||||
|
|
||||||
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
||||||
return AdjustmentRelationDTO{
|
return AdjustmentRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
TransactionType: e.TransactionType,
|
// TransactionType: e.LoggableType,
|
||||||
Quantity: e.Quantity,
|
// Quantity: e.Q,
|
||||||
BeforeQuantity: e.BeforeQuantity,
|
// BeforeQuantity: e.BeforeQuantity,
|
||||||
AfterQuantity: e.AfterQuantity,
|
// AfterQuantity: e.AfterQuantity,
|
||||||
Note: e.Note,
|
Note: e.Notes,
|
||||||
ProductWarehouseId: e.ProductWarehouseId,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
||||||
}
|
}
|
||||||
@@ -136,6 +136,6 @@ func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
|
|||||||
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
|
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
|
||||||
return AdjustmentDetailDTO{
|
return AdjustmentDetailDTO{
|
||||||
AdjustmentListDTO: ToAdjustmentListDTO(e),
|
AdjustmentListDTO: ToAdjustmentListDTO(e),
|
||||||
UpdatedAt: e.UpdatedAt,
|
// UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
)
|
)
|
||||||
@@ -21,10 +21,11 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
|||||||
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
productRepo := rproduct.NewProductRepository(db)
|
productRepo := rproduct.NewProductRepository(db)
|
||||||
|
|
||||||
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
|
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate, projectFlockKandangRepo)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
AdjustmentRoutes(router, userService, adjustmentService)
|
AdjustmentRoutes(router, userService, adjustmentService)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package adjustments
|
package adjustments
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers"
|
||||||
adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
|
adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,7 +13,7 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme
|
|||||||
ctrl := controller.NewAdjustmentController(s)
|
ctrl := controller.NewAdjustmentController(s)
|
||||||
|
|
||||||
route := v1.Group("/adjustments")
|
route := v1.Group("/adjustments")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// Standard CRUD routes following master data pattern
|
// Standard CRUD routes following master data pattern
|
||||||
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
|
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
|
||||||
route.Post("/", ctrl.Adjustment) // Create adjustment
|
route.Post("/", ctrl.Adjustment) // Create adjustment
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
@@ -11,13 +16,10 @@ import (
|
|||||||
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/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"
|
||||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdjustmentService interface {
|
type AdjustmentService interface {
|
||||||
@@ -27,22 +29,24 @@ type AdjustmentService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type adjustmentService struct {
|
type adjustmentService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
StockLogsRepository stockLogsRepo.StockLogRepository
|
StockLogsRepository stockLogsRepo.StockLogRepository
|
||||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
||||||
ProductRepo productRepo.ProductRepository
|
ProductRepo productRepo.ProductRepository
|
||||||
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService {
|
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) AdjustmentService {
|
||||||
return &adjustmentService{
|
return &adjustmentService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
StockLogsRepository: stockLogsRepo,
|
StockLogsRepository: stockLogsRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
ProductRepo: productRepo,
|
ProductRepo: productRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +70,7 @@ func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if stockLog.LogType != entity.LogTypeAdjustment {
|
if stockLog.LoggableType != entity.LogTypeAdjustment {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,12 +109,16 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
}
|
}
|
||||||
if !isProductWarehouseExist {
|
if !isProductWarehouseExist {
|
||||||
|
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
newPW := &entity.ProductWarehouse{
|
newPW := &entity.ProductWarehouse{
|
||||||
ProductId: uint(req.ProductID),
|
ProductId: uint(req.ProductID),
|
||||||
WarehouseId: uint(req.WarehouseID),
|
WarehouseId: uint(req.WarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
CreatedBy: actorID,
|
ProjectFlockKandangId: &projectFlockKandangID,
|
||||||
|
// CreatedBy: 1, // TODO: should Get from auth middleware
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
||||||
@@ -120,6 +128,23 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||||
|
ctx,
|
||||||
|
uint(req.ProductID),
|
||||||
|
uint(req.WarehouseID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get product warehouse for project flock check: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
ctx,
|
||||||
|
s.StockLogsRepository.DB(),
|
||||||
|
[]uint{pw.Id},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -128,25 +153,23 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
afterQuantity := productWarehouse.Quantity
|
afterQuantity := productWarehouse.Quantity
|
||||||
|
newLog := &entity.StockLog{
|
||||||
|
// TransactionType: transactionType,
|
||||||
|
LoggableType: entity.LogTypeAdjustment,
|
||||||
|
LoggableId: 0,
|
||||||
|
Notes: req.Note,
|
||||||
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
|
CreatedBy: actorID, // TODO: should Get from auth middleware
|
||||||
|
}
|
||||||
if transactionType == entity.TransactionTypeIncrease {
|
if transactionType == entity.TransactionTypeIncrease {
|
||||||
afterQuantity += req.Quantity
|
afterQuantity += req.Quantity
|
||||||
|
newLog.Increase = afterQuantity
|
||||||
} else {
|
} else {
|
||||||
if productWarehouse.Quantity < req.Quantity {
|
if productWarehouse.Quantity < req.Quantity {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
|
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
|
||||||
}
|
}
|
||||||
afterQuantity -= req.Quantity
|
afterQuantity -= req.Quantity
|
||||||
}
|
newLog.Decrease = afterQuantity
|
||||||
|
|
||||||
newLog := &entity.StockLog{
|
|
||||||
TransactionType: transactionType,
|
|
||||||
Quantity: req.Quantity,
|
|
||||||
BeforeQuantity: productWarehouse.Quantity,
|
|
||||||
AfterQuantity: afterQuantity,
|
|
||||||
LogType: entity.LogTypeAdjustment,
|
|
||||||
LogId: 0,
|
|
||||||
Note: req.Note,
|
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
||||||
@@ -172,6 +195,32 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return s.GetOne(c, createdLogId)
|
return s.GetOne(c, createdLogId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||||
|
}
|
||||||
|
|
||||||
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint(projectFlockKandang.Id), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
|
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
|
||||||
if err := s.Validate.Struct(query); err != nil {
|
if err := s.Validate.Struct(query); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -200,7 +249,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
|||||||
|
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
|
|
||||||
db = db.Where("log_type = ?", entity.LogTypeAdjustment)
|
db = db.Where("loggable_type = ?", entity.LogTypeAdjustment)
|
||||||
|
|
||||||
if query.TransactionType != "" {
|
if query.TransactionType != "" {
|
||||||
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||||
|
|||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MarketingDeliveryProductRepository interface {
|
|
||||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
|
||||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
|
||||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarketingDeliveryProductRepositoryImpl struct {
|
|
||||||
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
|
|
||||||
return &MarketingDeliveryProductRepositoryImpl{
|
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
|
|
||||||
var deliveryProduct entity.MarketingDeliveryProduct
|
|
||||||
if err := r.DB().WithContext(ctx).Where("marketing_product_id = ?", marketingProductID).First(&deliveryProduct).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &deliveryProduct, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
|
||||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
|
||||||
|
|
||||||
// Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct
|
|
||||||
// Filter: hanya ambil yang sudah memiliki delivery_date (delivery date tidak null)
|
|
||||||
if err := r.DB().WithContext(ctx).
|
|
||||||
Preload("MarketingProduct").
|
|
||||||
Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id").
|
|
||||||
Where("mp.marketing_id = ?", marketingId).
|
|
||||||
Where("marketing_delivery_products.delivery_date IS NOT NULL").
|
|
||||||
Order("marketing_delivery_products.id ASC").
|
|
||||||
Find(&deliveryProducts).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return deliveryProducts, nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
// entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductStockController struct {
|
||||||
|
ProductStockService service.ProductStockService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductStockController(productStockService service.ProductStockService) *ProductStockController {
|
||||||
|
return &ProductStockController{
|
||||||
|
ProductStockService: productStockService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProductStockController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ProductStockService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ProductStockListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all productStocks successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToProductStockListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProductStockController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := u.ProductStockService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved product successfully",
|
||||||
|
Data: dto.ToProductStockDetailDTO(*res),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||||
|
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
|
||||||
|
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type ProductStockRelationDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductStockListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Brand string `json:"brand"`
|
||||||
|
Sku *string `json:"sku,omitempty"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
|
SellingPrice *float64 `json:"selling_price,omitempty"`
|
||||||
|
Tax *float64 `json:"tax,omitempty"`
|
||||||
|
ExpiryPeriod *int `json:"expiry_period,omitempty"`
|
||||||
|
Flags []string `json:"flags"`
|
||||||
|
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||||
|
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Suppliers []SupplierDTO `json:"suppliers,omitempty"`
|
||||||
|
ProductWarehouses []ProductWarehouseDTO `json:"product_warehouses,omitempty"`
|
||||||
|
TotalStock float64 `json:"total_stock"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductStockDetailDTO struct {
|
||||||
|
ProductStockListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupplierDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductWarehouseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
ProductId uint `json:"product_id"`
|
||||||
|
WarehouseId uint `json:"warehouse_id"`
|
||||||
|
WarehouseName string `json:"warehouse_name"`
|
||||||
|
Location *locationDTO.LocationRelationDTO `json:"location"`
|
||||||
|
CurrentStock float64 `json:"current_stock"`
|
||||||
|
StockLogs []StockLogDetailDTO `json:"stock_logs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockLogDetailDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Increase float64 `json:"increase"`
|
||||||
|
Decrease float64 `json:"decrease"`
|
||||||
|
LoggableType string `json:"loggable_type"`
|
||||||
|
LoggableId uint `json:"loggable_id"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
CreatedBy uint `json:"created_by"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
func ToProductStockListDTO(e entity.Product) ProductStockListDTO {
|
||||||
|
var createdUser *userDTO.UserRelationDTO
|
||||||
|
if e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryRef *productCategoryDTO.ProductCategoryRelationDTO
|
||||||
|
if e.ProductCategory.Id != 0 {
|
||||||
|
mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory)
|
||||||
|
categoryRef = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := make([]string, len(e.Flags))
|
||||||
|
for i, f := range e.Flags {
|
||||||
|
flags[i] = f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var uomRef *uomDTO.UomRelationDTO
|
||||||
|
if e.Uom.Id != 0 {
|
||||||
|
mapped := uomDTO.ToUomRelationDTO(e.Uom)
|
||||||
|
uomRef = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProductStockListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Name: e.Name,
|
||||||
|
Flags: flags,
|
||||||
|
Uom: uomRef,
|
||||||
|
Brand: e.Brand,
|
||||||
|
Sku: e.Sku,
|
||||||
|
ProductPrice: e.ProductPrice,
|
||||||
|
SellingPrice: e.SellingPrice,
|
||||||
|
Tax: e.Tax,
|
||||||
|
ExpiryPeriod: e.ExpiryPeriod,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
ProductCategory: categoryRef,
|
||||||
|
Suppliers: mapSupplierDTOs(e.ProductSuppliers),
|
||||||
|
TotalStock: calculateTotalStock(e.ProductWarehouses),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProductStockListDTOs(e []entity.Product) []ProductStockListDTO {
|
||||||
|
result := make([]ProductStockListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToProductStockListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProductStockDetailDTO(e entity.Product) ProductStockDetailDTO {
|
||||||
|
base := ToProductStockListDTO(e)
|
||||||
|
base.ProductWarehouses = mapProductWarehouseDTOs(e.ProductWarehouses)
|
||||||
|
|
||||||
|
return ProductStockDetailDTO{
|
||||||
|
ProductStockListDTO: base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
func mapSupplierDTOs(src []entity.ProductSupplier) []SupplierDTO {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]SupplierDTO, 0, len(src))
|
||||||
|
for _, ps := range src {
|
||||||
|
if ps.Supplier.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, SupplierDTO{
|
||||||
|
Id: ps.Supplier.Id,
|
||||||
|
Name: ps.Supplier.Name,
|
||||||
|
Alias: ps.Supplier.Alias,
|
||||||
|
Category: ps.Supplier.Category,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapProductWarehouseDTOs(src []entity.ProductWarehouse) []ProductWarehouseDTO {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return []ProductWarehouseDTO{}
|
||||||
|
}
|
||||||
|
result := make([]ProductWarehouseDTO, 0, len(src))
|
||||||
|
for _, pw := range src {
|
||||||
|
dto := ProductWarehouseDTO{
|
||||||
|
Id: pw.Id,
|
||||||
|
ProductId: pw.ProductId,
|
||||||
|
WarehouseId: pw.WarehouseId,
|
||||||
|
CurrentStock: pw.Quantity,
|
||||||
|
StockLogs: mapStockLogs(pw.StockLogs),
|
||||||
|
}
|
||||||
|
if pw.Warehouse.Id != 0 {
|
||||||
|
dto.WarehouseName = pw.Warehouse.Name
|
||||||
|
if pw.Warehouse.Location != nil {
|
||||||
|
mapped := locationDTO.ToLocationRelationDTO(*pw.Warehouse.Location)
|
||||||
|
dto.Location = &mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, dto)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapStockLogs(src []entity.StockLog) []StockLogDetailDTO {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return []StockLogDetailDTO{}
|
||||||
|
}
|
||||||
|
result := make([]StockLogDetailDTO, 0, len(src))
|
||||||
|
for _, log := range src {
|
||||||
|
var notes *string
|
||||||
|
if log.Notes != "" {
|
||||||
|
n := log.Notes
|
||||||
|
notes = &n
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, StockLogDetailDTO{
|
||||||
|
Id: log.Id,
|
||||||
|
Increase: log.Increase,
|
||||||
|
Decrease: log.Decrease,
|
||||||
|
LoggableType: log.LoggableType,
|
||||||
|
LoggableId: log.LoggableId,
|
||||||
|
Notes: notes,
|
||||||
|
ProductWarehouseId: log.ProductWarehouseId,
|
||||||
|
CreatedBy: log.CreatedBy,
|
||||||
|
CreatedUser: mapCreatedUser(log.CreatedUser),
|
||||||
|
CreatedAt: log.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapCreatedUser(user *entity.User) *userDTO.UserRelationDTO {
|
||||||
|
if user == nil || user.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mapped := userDTO.ToUserRelationDTO(*user)
|
||||||
|
return &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateTotalStock(productWarehouses []entity.ProductWarehouse) float64 {
|
||||||
|
var total float64
|
||||||
|
for _, pw := range productWarehouses {
|
||||||
|
total += pw.Quantity
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package productStocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
sProductStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
|
||||||
|
|
||||||
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductStockModule struct{}
|
||||||
|
|
||||||
|
func (ProductStockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
productRepo := rProduct.NewProductRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
productStockService := sProductStock.NewProductStockService(productRepo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
ProductStockRoutes(router, userService, productStockService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
// "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
// "gorm.io/gorm"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// type ProductStockRepository interface {
|
||||||
|
// repository.BaseRepository[entity.ProductStock]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type ProductStockRepositoryImpl struct {
|
||||||
|
// *repository.BaseRepositoryImpl[entity.ProductStock]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func NewProductStockRepository(db *gorm.DB) ProductStockRepository {
|
||||||
|
// return &ProductStockRepositoryImpl{
|
||||||
|
// BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductStock](db),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package productStocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/controllers"
|
||||||
|
productStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProductStockRoutes(v1 fiber.Router, u user.UserService, s productStock.ProductStockService) {
|
||||||
|
ctrl := controller.NewProductStockController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/product-stocks")
|
||||||
|
|
||||||
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations"
|
||||||
|
productRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductStockService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.Product, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type productStockService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
ProductRepository productRepository.ProductRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductStockService(
|
||||||
|
productRepo productRepository.ProductRepository,
|
||||||
|
validate *validator.Validate,
|
||||||
|
) ProductStockService {
|
||||||
|
return &productStockService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
ProductRepository: productRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s productStockService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("Uom").
|
||||||
|
Preload("ProductCategory").
|
||||||
|
Preload("Flags").
|
||||||
|
Preload("ProductWarehouses").
|
||||||
|
Preload("ProductWarehouses.Warehouse").
|
||||||
|
Preload("ProductWarehouses.Warehouse.Location").
|
||||||
|
Preload("ProductWarehouses.Warehouse.Location.Area").
|
||||||
|
Preload("ProductWarehouses.StockLogs", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("created_at ASC")
|
||||||
|
}).
|
||||||
|
Preload("ProductWarehouses.StockLogs.CreatedUser").
|
||||||
|
Preload("ProductSuppliers").
|
||||||
|
Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("suppliers.name ASC")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = s.withRelations(db)
|
||||||
|
if params.Search != "" {
|
||||||
|
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||||
|
}
|
||||||
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get productStocks: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return productStocks, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) {
|
||||||
|
product, err := s.ProductRepository.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get product by id: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return product, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
@@ -98,8 +98,8 @@ func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNeste
|
|||||||
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
|
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
|
||||||
dto := ProductWarehouseListDTO{
|
dto := ProductWarehouseListDTO{
|
||||||
ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e),
|
ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e),
|
||||||
CreatedAt: e.CreatedAt,
|
// CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
// UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map Product relation jika ada
|
// Map Product relation jika ada
|
||||||
@@ -140,13 +140,13 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map CreatedUser relation jika ada
|
// Map CreatedUser relation jika ada
|
||||||
if e.CreatedUser.Id != 0 {
|
// if e.CreatedUser.Id != 0 {
|
||||||
user := UserRelationDTO{
|
// user := UserRelationDTO{
|
||||||
Id: e.CreatedUser.Id,
|
// Id: e.CreatedUser.Id,
|
||||||
Username: e.CreatedUser.Name,
|
// Username: e.CreatedUser.Name,
|
||||||
}
|
// }
|
||||||
dto.CreatedUser = &user
|
// dto.CreatedUser = &user
|
||||||
}
|
// }
|
||||||
|
|
||||||
return dto
|
return dto
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-12
@@ -27,7 +27,7 @@ type ProductWarehouseRepository interface {
|
|||||||
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
|
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
|
||||||
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint) (uint, error)
|
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseRepositoryImpl struct {
|
type ProductWarehouseRepositoryImpl struct {
|
||||||
@@ -151,7 +151,7 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
|
|||||||
}
|
}
|
||||||
if err := base.Model(&entity.ProductWarehouse{}).
|
if err := base.Model(&entity.ProductWarehouse{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Update("quantity", gorm.Expr("COALESCE(quantity,0) + ?", delta)).Error; err != nil {
|
Update("qty", gorm.Expr("COALESCE(qty,0) + ?", delta)).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affec
|
|||||||
var emptyIDs []uint
|
var emptyIDs []uint
|
||||||
if err := r.DB().WithContext(ctx).
|
if err := r.DB().WithContext(ctx).
|
||||||
Model(&entity.ProductWarehouse{}).
|
Model(&entity.ProductWarehouse{}).
|
||||||
Where("id IN ? AND COALESCE(quantity,0) <= 0", ids).
|
Where("id IN ? AND COALESCE(qty,0) <= 0", ids).
|
||||||
Pluck("id", &emptyIDs).Error; err != nil {
|
Pluck("id", &emptyIDs).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -199,10 +199,21 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
productID uint,
|
productID uint,
|
||||||
warehouseID uint,
|
warehouseID uint,
|
||||||
|
projectFlockKandangID *uint,
|
||||||
createdBy uint,
|
createdBy uint,
|
||||||
) (uint, error) {
|
) (uint, error) {
|
||||||
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
// Backfill project_flock_kandang_id when it's missing and caller provides one.
|
||||||
|
if projectFlockKandangID != nil && (record.ProjectFlockKandangId == nil || *record.ProjectFlockKandangId == 0) {
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.ProductWarehouse{}).
|
||||||
|
Where("id = ?", record.Id).
|
||||||
|
Update("project_flock_kandang_id", *projectFlockKandangID).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
record.ProjectFlockKandangId = projectFlockKandangID
|
||||||
|
}
|
||||||
return record.Id, nil
|
return record.Id, nil
|
||||||
}
|
}
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -210,14 +221,15 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
entity := &entity.ProductWarehouse{
|
entity := &entity.ProductWarehouse{
|
||||||
ProductId: productID,
|
ProductId: productID,
|
||||||
WarehouseId: warehouseID,
|
WarehouseId: warehouseID,
|
||||||
Quantity: 0,
|
ProjectFlockKandangId: projectFlockKandangID,
|
||||||
CreatedBy: uint(createdBy),
|
Quantity: 0,
|
||||||
}
|
// CreatedBy: uint(createdBy),
|
||||||
if entity.CreatedBy == 0 {
|
|
||||||
entity.CreatedBy = 1
|
|
||||||
}
|
}
|
||||||
|
// if entity.CreatedBy == 0 {
|
||||||
|
// entity.CreatedBy = 1
|
||||||
|
// }
|
||||||
|
|
||||||
if err := r.CreateOne(ctx, entity, nil); err != nil {
|
if err := r.CreateOne(ctx, entity, nil); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -257,8 +269,11 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con
|
|||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||||
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
||||||
Order("product_warehouses.created_at DESC").
|
Order("product_warehouses.id DESC").
|
||||||
Preload("Product").Preload("Warehouse").
|
Preload("Product").
|
||||||
|
Preload("Product.ProductCategory").
|
||||||
|
Preload("Product.Uom").
|
||||||
|
Preload("Warehouse").
|
||||||
Find(&productWarehouses).Error
|
Find(&productWarehouses).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Warehouse.Location").
|
Preload("Warehouse.Location").
|
||||||
Preload("Warehouse.Area").
|
Preload("Warehouse.Area").
|
||||||
Preload("Warehouse.Kandang").
|
Preload("Warehouse.Kandang").
|
||||||
Preload("CreatedUser")
|
Preload("ProjectFlockKandang")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
|
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
|
||||||
@@ -104,7 +104,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
|||||||
|
|
||||||
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
|
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
|
||||||
|
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("product_warehouses.id DESC")
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
|
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
|
||||||
|
productStocks "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks"
|
||||||
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
|
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
|
||||||
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
|
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
@@ -21,6 +22,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
|
|||||||
|
|
||||||
adjustments.AdjustmentModule{},
|
adjustments.AdjustmentModule{},
|
||||||
transfers.TransferModule{},
|
transfers.TransferModule{},
|
||||||
|
productStocks.ProductStockModule{},
|
||||||
// MODULE REGISTRY
|
// MODULE REGISTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||||
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
||||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -25,8 +27,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
supplierRepo := rSupplier.NewSupplierRepository(db)
|
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo)
|
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
TransferRoutes(router, userService, transferService)
|
TransferRoutes(router, userService, transferService)
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
|
||||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/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"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,9 +40,11 @@ type transferService struct {
|
|||||||
StockLogsRepository rStockLogs.StockLogRepository
|
StockLogsRepository rStockLogs.StockLogRepository
|
||||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
SupplierRepo rSupplier.SupplierRepository
|
SupplierRepo rSupplier.SupplierRepository
|
||||||
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository) TransferService {
|
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) TransferService {
|
||||||
return &transferService{
|
return &transferService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -48,6 +55,8 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
|||||||
StockLogsRepository: stockLogsRepo,
|
StockLogsRepository: stockLogsRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
SupplierRepo: supplierRepo,
|
SupplierRepo: supplierRepo,
|
||||||
|
WarehouseRepo: warehouseRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
@@ -111,6 +120,8 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
||||||
|
// Validasi stok di gudang asal harus exist dan mencukupi
|
||||||
|
pwIDs := make([]uint, 0, len(req.Products))
|
||||||
|
|
||||||
// Validasi stok di gudang asal harus exist dan mencukupi
|
// Validasi stok di gudang asal harus exist dan mencukupi
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
@@ -126,7 +137,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
if sourcePW.Quantity < product.ProductQty {
|
if sourcePW.Quantity < product.ProductQty {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
||||||
}
|
}
|
||||||
|
pwIDs = append(pwIDs, sourcePW.Id)
|
||||||
}
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
c.Context(),
|
||||||
|
s.StockTransferRepo.DB(),
|
||||||
|
pwIDs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -271,15 +291,18 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
||||||
|
|
||||||
// create stock log for decrease (source)
|
// create stock log for decrease (source)
|
||||||
beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
|
// beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
|
||||||
decreaseLog := &entity.StockLog{
|
decreaseLog := &entity.StockLog{
|
||||||
TransactionType: entity.TransactionTypeDecrease,
|
// TransactionType: entity.TransactionTypeDecrease,
|
||||||
Quantity: product.ProductQty,
|
// Quantity: product.ProductQty,
|
||||||
BeforeQuantity: beforeQty,
|
// BeforeQuantity: beforeQty,
|
||||||
AfterQuantity: sourcePW.Quantity,
|
// AfterQuantity: sourcePW.Qty,
|
||||||
LogType: entity.LogTypeTransfer,
|
// LogType: entity.LogTypeTransfer,
|
||||||
LogId: uint(entityTransfer.Id),
|
// LogId: uint(entityTransfer.Id),
|
||||||
Note: "",
|
Decrease: product.ProductQty,
|
||||||
|
Notes: "",
|
||||||
|
LoggableType: entity.LogTypeTransfer,
|
||||||
|
LoggableId: uint(entityTransfer.Id),
|
||||||
ProductWarehouseId: sourcePW.Id,
|
ProductWarehouseId: sourcePW.Id,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
@@ -298,11 +321,17 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
// Jika belum ada record untuk produk di gudang tujuan, buat baru
|
// Jika belum ada record untuk produk di gudang tujuan, buat baru
|
||||||
|
ctx := c.Context()
|
||||||
|
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
destPW = &entity.ProductWarehouse{
|
destPW = &entity.ProductWarehouse{
|
||||||
ProductId: uint(product.ProductID),
|
ProductId: uint(product.ProductID),
|
||||||
WarehouseId: uint(req.DestinationWarehouseID),
|
WarehouseId: uint(req.DestinationWarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
CreatedBy: actorID,
|
ProjectFlockKandangId: &projectFlockKandangID,
|
||||||
|
// CreatedBy: 1, // TODO: should Get from auth middleware
|
||||||
}
|
}
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
|
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
|
||||||
@@ -319,15 +348,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
||||||
|
|
||||||
// create stock log for increase (destination)
|
// create stock log for increase (destination)
|
||||||
beforeDestQty := destPW.Quantity - product.ProductQty
|
// beforeDestQty := destPW.Quantity - product.ProductQty
|
||||||
increaseLog := &entity.StockLog{
|
increaseLog := &entity.StockLog{
|
||||||
TransactionType: entity.TransactionTypeIncrease,
|
// TransactionType: entity.TransactionTypeIncrease,
|
||||||
Quantity: product.ProductQty,
|
// Quantity: product.ProductQty,
|
||||||
BeforeQuantity: beforeDestQty,
|
// BeforeQuantity: beforeDestQty,
|
||||||
AfterQuantity: destPW.Quantity,
|
// AfterQuantity: destPW.Qty,
|
||||||
LogType: entity.LogTypeTransfer,
|
Increase: product.ProductQty,
|
||||||
LogId: uint(entityTransfer.Id),
|
LoggableType: entity.LogTypeTransfer,
|
||||||
Note: "",
|
LoggableId: uint(entityTransfer.Id),
|
||||||
|
Notes: "",
|
||||||
ProductWarehouseId: destPW.Id,
|
ProductWarehouseId: destPW.Id,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
@@ -353,3 +383,29 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||||
|
}
|
||||||
|
|
||||||
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint(projectFlockKandang.Id), nil
|
||||||
|
}
|
||||||
|
|||||||
+6
-6
@@ -4,9 +4,9 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -23,7 +23,7 @@ func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersSer
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
query := &validation.DeliveryOrderQuery{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
MarketingId: uint(c.QueryInt("marketing_id", 0)),
|
MarketingId: uint(c.QueryInt("marketing_id", 0)),
|
||||||
@@ -76,7 +76,7 @@ func (u *DeliveryOrdersController) GetOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
|
func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Create)
|
req := new(validation.DeliveryOrderCreate)
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
@@ -97,7 +97,7 @@ func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
|
func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Update)
|
req := new(validation.DeliveryOrderUpdate)
|
||||||
param := c.Params("id")
|
param := c.Params("id")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
+3
-3
@@ -3,9 +3,9 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package delivery_orderss
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
|
||||||
sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
|
||||||
rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeliveryOrdersModule struct{}
|
|
||||||
|
|
||||||
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
|
||||||
marketingRepo := rMarketing.NewMarketingRepository(db)
|
|
||||||
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
|
|
||||||
marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db)
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
|
||||||
|
|
||||||
// Register workflow steps for MARKETINGS approval
|
|
||||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
|
||||||
|
|
||||||
DeliveryOrdersRoutes(router, userService, deliveryOrdersService)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package delivery_orderss
|
|
||||||
|
|
||||||
import (
|
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers"
|
|
||||||
deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) {
|
|
||||||
ctrl := controller.NewDeliveryOrdersController(s)
|
|
||||||
|
|
||||||
v1.Get("/", ctrl.GetAll)
|
|
||||||
v1.Get("/:id", ctrl.GetOne)
|
|
||||||
|
|
||||||
// Sisanya di group /delivery-orders
|
|
||||||
route := v1.Group("/delivery-orders")
|
|
||||||
|
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Post("/", ctrl.CreateOne)
|
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
|
||||||
|
|
||||||
}
|
|
||||||
+21
-15
@@ -24,7 +24,7 @@ type MarketingListDTO struct {
|
|||||||
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
||||||
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
||||||
SoDocs string `json:"so_docs"`
|
SoDocs string `json:"so_docs"`
|
||||||
SalesOrder []MarketingProductDTO `json:"sales_order"`
|
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
|
||||||
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -36,13 +36,14 @@ type MarketingDetailDTO struct {
|
|||||||
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
||||||
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
||||||
SoDocs string `json:"so_docs"`
|
SoDocs string `json:"so_docs"`
|
||||||
SalesOrder []MarketingProductDTO `json:"sales_order"`
|
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
|
||||||
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"`
|
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"`
|
||||||
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarketingDeliveryProductDTO struct {
|
type MarketingDeliveryProductDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
MarketingProductId uint `json:"marketing_product_id"`
|
MarketingProductId uint `json:"marketing_product_id"`
|
||||||
@@ -73,7 +74,7 @@ type DeliveryGroupDTO struct {
|
|||||||
Deliveries []DeliveryItemDTO `json:"deliveries"`
|
Deliveries []DeliveryItemDTO `json:"deliveries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarketingProductDTO struct {
|
type DeliveryMarketingProductDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
MarketingId uint `json:"marketing_id"`
|
MarketingId uint `json:"marketing_id"`
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
@@ -95,14 +96,14 @@ func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO {
|
func ToDeliveryMarketingProductDTO(e entity.MarketingProduct) DeliveryMarketingProductDTO {
|
||||||
var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO
|
var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO
|
||||||
if e.ProductWarehouse.Id != 0 {
|
if e.ProductWarehouse.Id != 0 {
|
||||||
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
|
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
|
||||||
productWarehouse = &mapped
|
productWarehouse = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
return MarketingProductDTO{
|
return DeliveryMarketingProductDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
MarketingId: e.MarketingId,
|
MarketingId: e.MarketingId,
|
||||||
ProductWarehouseId: e.ProductWarehouseId,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
@@ -155,11 +156,11 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
|
|||||||
latestApproval = mapped
|
latestApproval = mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
var salesOrderProducts []MarketingProductDTO
|
var salesOrderProducts []DeliveryMarketingProductDTO
|
||||||
if len(marketing.Products) > 0 {
|
if len(marketing.Products) > 0 {
|
||||||
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
|
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
|
||||||
for i, product := range marketing.Products {
|
for i, product := range marketing.Products {
|
||||||
salesOrderProducts[i] = ToMarketingProductDTO(product)
|
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,11 +196,11 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity
|
|||||||
salesPerson = mapped
|
salesPerson = mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
var salesOrderProducts []MarketingProductDTO
|
var salesOrderProducts []DeliveryMarketingProductDTO
|
||||||
if len(marketing.Products) > 0 {
|
if len(marketing.Products) > 0 {
|
||||||
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
|
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
|
||||||
for i, product := range marketing.Products {
|
for i, product := range marketing.Products {
|
||||||
salesOrderProducts[i] = ToMarketingProductDTO(product)
|
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,15 +320,20 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i := range groups {
|
for i := range groups {
|
||||||
if groups[i].DeliveryDate != nil {
|
groups[i].DoNumber = GenerateDeliveryOrderNumber(soNumber, groups[i].DeliveryDate, groups[i].Warehouse.Id)
|
||||||
dateStr := groups[i].DeliveryDate.Format("20060102")
|
|
||||||
groups[i].DoNumber = fmt.Sprintf("%s-%s-%d", soNumber, dateStr, groups[i].Warehouse.Id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateDeliveryOrderNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string {
|
||||||
|
dateStr := ""
|
||||||
|
if deliveryDate != nil {
|
||||||
|
dateStr = deliveryDate.Format("20060102")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId)
|
||||||
|
}
|
||||||
|
|
||||||
func getVehicleNumber(e entity.MarketingProduct) string {
|
func getVehicleNumber(e entity.MarketingProduct) string {
|
||||||
if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" {
|
if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" {
|
||||||
return e.DeliveryProduct.VehicleNumber
|
return e.DeliveryProduct.VehicleNumber
|
||||||
@@ -1,13 +1,52 @@
|
|||||||
package marketing
|
package marketing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
|
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarketingModule struct{}
|
type MarketingModule struct{}
|
||||||
|
|
||||||
func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
RegisterRoutes(router, db, validate)
|
// Initialize repositories
|
||||||
|
marketingRepo := repository.NewMarketingRepository(db)
|
||||||
|
marketingProductRepo := repository.NewMarketingProductRepository(db)
|
||||||
|
marketingDeliveryProductRepo := repository.NewMarketingDeliveryProductRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
customerRepo := rCustomer.NewCustomerRepository(db)
|
||||||
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
|
||||||
|
// Initialize approval service
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
|
// Register workflow steps for marketing approval
|
||||||
|
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
// Initialize services
|
||||||
|
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate)
|
||||||
|
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingRepository interface {
|
||||||
|
repository.BaseRepository[entity.Marketing]
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
|
GetNextSequence(ctx context.Context) (uint, error)
|
||||||
|
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.Marketing]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarketingRepository(db *gorm.DB) MarketingRepository {
|
||||||
|
return &MarketingRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.Marketing](ctx, r.DB(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) {
|
||||||
|
var maxID uint
|
||||||
|
if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return maxID + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
||||||
|
db := tx
|
||||||
|
if db == nil {
|
||||||
|
db = r.DB()
|
||||||
|
}
|
||||||
|
|
||||||
|
var soNumber string
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Raw("SELECT generate_so_number()").
|
||||||
|
Scan(&soNumber).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate SO number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return soNumber, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingDeliveryProductRepository interface {
|
||||||
|
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
||||||
|
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
||||||
|
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
|
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingDeliveryProductRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
|
||||||
|
return &MarketingDeliveryProductRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||||
|
|
||||||
|
// JOIN digunakan untuk filter WHERE clause ke ProjectFlockID yang berada 3 level relasi atas
|
||||||
|
// Entity relations digunakan di Preload (callback) untuk load data, bukan untuk filter
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||||
|
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
|
||||||
|
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||||
|
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||||
|
Distinct("marketing_delivery_products.*")
|
||||||
|
|
||||||
|
if callback != nil {
|
||||||
|
db = callback(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveryProducts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||||
|
|
||||||
|
// JOIN untuk filter by marketing_id yang ada di related table
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||||
|
Where("marketing_products.marketing_id = ?", marketingId)
|
||||||
|
|
||||||
|
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveryProducts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
|
||||||
|
var deliveryProduct entity.MarketingDeliveryProduct
|
||||||
|
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Where("marketing_product_id = ?", marketingProductID).
|
||||||
|
First(&deliveryProduct).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &deliveryProduct, nil
|
||||||
|
}
|
||||||
@@ -1,27 +1,31 @@
|
|||||||
package marketing
|
package marketing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/controllers"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders"
|
|
||||||
deliveryOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss"
|
|
||||||
// MODULE IMPORTS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrdersService service.SalesOrdersService, deliveryOrdersService service.DeliveryOrdersService) {
|
||||||
group := router.Group("/marketing")
|
salesOrdersCtrl := controller.NewSalesOrdersController(salesOrdersService)
|
||||||
|
deliveryOrdersCtrl := controller.NewDeliveryOrdersController(deliveryOrdersService)
|
||||||
|
|
||||||
allModules := []modules.Module{
|
route := router.Group("/marketing")
|
||||||
salesOrderss.SalesOrdersModule{},
|
route.Use(m.Auth(userService))
|
||||||
deliveryOrderss.DeliveryOrdersModule{},
|
|
||||||
// MODULE REGISTRY
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range allModules {
|
route.Get("/", deliveryOrdersCtrl.GetAll)
|
||||||
m.RegisterRoutes(group, db, validate)
|
route.Get("/:id", deliveryOrdersCtrl.GetOne)
|
||||||
}
|
route.Delete("/:id", salesOrdersCtrl.DeleteOne)
|
||||||
|
|
||||||
|
route.Post("/sales-orders", salesOrdersCtrl.CreateOne)
|
||||||
|
route.Patch("/sales-orders/:id", salesOrdersCtrl.UpdateOne)
|
||||||
|
route.Post("/sales-orders/approvals", salesOrdersCtrl.Approval)
|
||||||
|
|
||||||
|
route.Get("/delivery-orders", deliveryOrdersCtrl.GetAll)
|
||||||
|
route.Get("/delivery-orders/:id", deliveryOrdersCtrl.GetOne)
|
||||||
|
route.Post("/delivery-orders", deliveryOrdersCtrl.CreateOne)
|
||||||
|
route.Patch("/delivery-orders/:id", deliveryOrdersCtrl.UpdateOne)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package sales_orders
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
|
||||||
rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
|
||||||
sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
|
||||||
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SalesOrdersModule struct{}
|
|
||||||
|
|
||||||
func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
|
||||||
marketingRepo := rSalesOrders.NewMarketingRepository(db)
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
|
||||||
customerRepo := rCustomer.NewCustomerRepository(db)
|
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
|
||||||
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
|
|
||||||
|
|
||||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
salesOrdersService := sSalesOrders.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, validate)
|
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
|
||||||
|
|
||||||
SalesOrdersRoutes(router, userService, salesOrdersService)
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MarketingDeliveryProductRepository interface {
|
|
||||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarketingDeliveryProductRepositoryImpl struct {
|
|
||||||
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
|
|
||||||
return &MarketingDeliveryProductRepositoryImpl{
|
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MarketingRepository interface {
|
|
||||||
repository.BaseRepository[entity.Marketing]
|
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
|
||||||
GetNextSequence(ctx context.Context) (uint, error)
|
|
||||||
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarketingRepositoryImpl struct {
|
|
||||||
*repository.BaseRepositoryImpl[entity.Marketing]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMarketingRepository(db *gorm.DB) MarketingRepository {
|
|
||||||
return &MarketingRepositoryImpl{
|
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
|
||||||
return repository.Exists[entity.Marketing](ctx, r.DB(), id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) {
|
|
||||||
var maxID uint
|
|
||||||
if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return maxID + 1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
|
||||||
return r.generateSequentialNumber(ctx, tx, "so_number", utils.MarketingSoNumberPrefix, utils.MarketingNumberPadding)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseNumericSuffix(value, prefix string) (int, bool) {
|
|
||||||
if !strings.HasPrefix(value, prefix) {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
suffix := strings.TrimPrefix(value, prefix)
|
|
||||||
if suffix == "" {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
trimmed := strings.TrimLeft(suffix, "0")
|
|
||||||
if trimmed == "" {
|
|
||||||
trimmed = "0"
|
|
||||||
}
|
|
||||||
number, err := strconv.Atoi(trimmed)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return number, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
|
|
||||||
var count int64
|
|
||||||
if err := db.WithContext(ctx).
|
|
||||||
Model(&entity.Marketing{}).
|
|
||||||
Where(fmt.Sprintf("%s = ?", column), value).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return count > 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
|
|
||||||
|
|
||||||
db := tx
|
|
||||||
if db == nil {
|
|
||||||
db = r.DB()
|
|
||||||
}
|
|
||||||
|
|
||||||
var values []string
|
|
||||||
err := db.WithContext(ctx).
|
|
||||||
Model(&entity.Marketing{}).
|
|
||||||
Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%").
|
|
||||||
Select(column).
|
|
||||||
Order(fmt.Sprintf("%s DESC", column)).
|
|
||||||
Limit(20).
|
|
||||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
||||||
Pluck(column, &values).Error
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
next := 1
|
|
||||||
for _, value := range values {
|
|
||||||
if number, ok := parseNumericSuffix(value, prefix); ok {
|
|
||||||
next = number + 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxAttempts = 20
|
|
||||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
|
||||||
candidate := fmt.Sprintf("%s%0*d", prefix, padding, next)
|
|
||||||
exists, err := r.numberExists(ctx, db, column, candidate)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return candidate, nil
|
|
||||||
}
|
|
||||||
next++
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("unable to generate unique %s", column)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package sales_orders
|
|
||||||
|
|
||||||
import (
|
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
|
|
||||||
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) {
|
|
||||||
ctrl := controller.NewSalesOrdersController(s)
|
|
||||||
|
|
||||||
v1.Delete("/:id", ctrl.DeleteOne)
|
|
||||||
route := v1.Group("/sales-orders")
|
|
||||||
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Post("/", ctrl.CreateOne)
|
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
|
||||||
|
|
||||||
route.Post("/approvals", ctrl.Approval)
|
|
||||||
}
|
|
||||||
+29
-16
@@ -8,13 +8,12 @@ import (
|
|||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -24,10 +23,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DeliveryOrdersService interface {
|
type DeliveryOrdersService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type deliveryOrdersService struct {
|
type deliveryOrdersService struct {
|
||||||
@@ -35,14 +34,14 @@ type deliveryOrdersService struct {
|
|||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
MarketingRepo marketingRepo.MarketingRepository
|
MarketingRepo marketingRepo.MarketingRepository
|
||||||
MarketingProductRepo marketingRepo.MarketingProductRepository
|
MarketingProductRepo marketingRepo.MarketingProductRepository
|
||||||
MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository
|
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeliveryOrdersService(
|
func NewDeliveryOrdersService(
|
||||||
marketingRepo marketingRepo.MarketingRepository,
|
marketingRepo marketingRepo.MarketingRepository,
|
||||||
marketingProductRepo marketingRepo.MarketingProductRepository,
|
marketingProductRepo marketingRepo.MarketingProductRepository,
|
||||||
marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository,
|
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) DeliveryOrdersService {
|
) DeliveryOrdersService {
|
||||||
@@ -86,7 +85,7 @@ func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketin
|
|||||||
return &responseDTO, nil
|
return &responseDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) {
|
func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -165,7 +164,7 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDeta
|
|||||||
return &responseDTO, nil
|
return &responseDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) {
|
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -200,7 +199,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
||||||
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
|
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
|
||||||
@@ -223,6 +222,14 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
c.Context(),
|
||||||
|
dbTransaction,
|
||||||
|
[]uint{foundMarketingProduct.ProductWarehouseId},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -259,7 +266,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
approvalAction := entity.ApprovalActionApproved
|
approvalAction := entity.ApprovalActionApproved
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
@@ -287,7 +293,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
return s.getMarketingWithDeliveries(c, req.MarketingId)
|
return s.getMarketingWithDeliveries(c, req.MarketingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) {
|
func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -301,7 +307,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
|||||||
err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
||||||
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
|
|
||||||
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
|
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -321,6 +327,13 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
|||||||
if foundMarketingProduct == nil {
|
if foundMarketingProduct == nil {
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||||
}
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
c.Context(),
|
||||||
|
dbTransaction,
|
||||||
|
[]uint{foundMarketingProduct.ProductWarehouseId},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
+79
-21
@@ -10,11 +10,12 @@ import (
|
|||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
|
||||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/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"
|
||||||
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
@@ -33,24 +34,29 @@ type SalesOrdersService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type salesOrdersService struct {
|
type salesOrdersService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
MarketingRepo repository.MarketingRepository
|
MarketingRepo repository.MarketingRepository
|
||||||
CustomerRepo customerRepo.CustomerRepository
|
CustomerRepo customerRepo.CustomerRepository
|
||||||
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||||
UserRepo userRepo.UserRepository
|
UserRepo userRepo.UserRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService {
|
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo warehouseRepo.WarehouseRepository,
|
||||||
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
|
||||||
return &salesOrdersService{
|
return &salesOrdersService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
MarketingRepo: marketingRepo,
|
MarketingRepo: marketingRepo,
|
||||||
CustomerRepo: customerRepo,
|
CustomerRepo: customerRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
UserRepo: userRepo,
|
UserRepo: userRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
WarehouseRepo: warehouseRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +131,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
|
|
||||||
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
|
invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
marketing = &entity.Marketing{
|
marketing = &entity.Marketing{
|
||||||
@@ -141,10 +147,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(req.MarketingProducts) > 0 {
|
if len(req.MarketingProducts) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(req.MarketingProducts))
|
||||||
for _, product := range req.MarketingProducts {
|
for _, product := range req.MarketingProducts {
|
||||||
|
if product.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, product.ProductWarehouseId)
|
||||||
|
}
|
||||||
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
|
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,13 +228,25 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(req.MarketingProducts) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(req.MarketingProducts))
|
||||||
|
for _, item := range req.MarketingProducts {
|
||||||
|
if item.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, item.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
|
invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
|
|
||||||
updateBody := make(map[string]any)
|
updateBody := make(map[string]any)
|
||||||
if req.CustomerId != 0 {
|
if req.CustomerId != 0 {
|
||||||
@@ -368,7 +394,18 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
|
||||||
}
|
}
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(marketing.Products))
|
||||||
|
for _, p := range marketing.Products {
|
||||||
|
if p.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, p.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
@@ -459,6 +496,27 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
|
|||||||
fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber))
|
fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
marketing, mErr := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Products")
|
||||||
|
})
|
||||||
|
if mErr != nil {
|
||||||
|
if errors.Is(mErr, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("SalesOrders %d not found", id))
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order for project validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(marketing.Products))
|
||||||
|
for _, p := range marketing.Products {
|
||||||
|
if p.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, p.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
@@ -527,7 +585,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
|
|||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo rInvMarketingDeliveryProduct.MarketingDeliveryProductRepository) error {
|
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
|
||||||
|
|
||||||
marketingProduct := &entity.MarketingProduct{
|
marketingProduct := &entity.MarketingProduct{
|
||||||
MarketingId: marketingId,
|
MarketingId: marketingId,
|
||||||
+4
-4
@@ -11,22 +11,22 @@ type DeliveryProduct struct {
|
|||||||
VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"`
|
VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Create struct {
|
type DeliveryOrderCreate struct {
|
||||||
MarketingId uint `json:"marketing_id" validate:"required,gt=0"`
|
MarketingId uint `json:"marketing_id" validate:"required,gt=0"`
|
||||||
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"`
|
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type DeliveryOrderUpdate struct {
|
||||||
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"`
|
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type DeliveryOrderQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
|
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Approve struct {
|
type DeliveryOrderApprove struct {
|
||||||
Action string `json:"action" validate:"required_strict"`
|
Action string `json:"action" validate:"required_strict"`
|
||||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
@@ -18,13 +18,14 @@ type NonstockRelationDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NonstockListDTO struct {
|
type NonstockListDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Flags []string `json:"flags"`
|
Flags []string `json:"flags"`
|
||||||
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NonstockDetailDTO struct {
|
type NonstockDetailDTO struct {
|
||||||
@@ -76,6 +77,7 @@ func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO {
|
|||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
Uom: uomRef,
|
Uom: uomRef,
|
||||||
|
Suppliers: toNonstockSupplierDTOs(e.NonstockSuppliers),
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
@@ -95,3 +97,23 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
|
|||||||
NonstockListDTO: ToNonstockListDTO(e),
|
NonstockListDTO: ToNonstockListDTO(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
|
||||||
|
if len(relations) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
|
||||||
|
for _, relation := range relations {
|
||||||
|
if relation.Supplier.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user