mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 15:25:43 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4da37731c |
@@ -3,7 +3,7 @@ root = "."
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
|
cmd = "go build -o ./tmp/main ./cmd/api"
|
||||||
bin = "tmp/main"
|
bin = "tmp/main"
|
||||||
full_bin = "APP_ENV=dev ./tmp/main"
|
full_bin = "APP_ENV=dev ./tmp/main"
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.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/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
|
||||||
@@ -46,10 +45,8 @@ require (
|
|||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
@@ -73,7 +70,6 @@ require (
|
|||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/philhofer/fwd v1.1.2 // indirect
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
@@ -98,8 +94,4 @@ require (
|
|||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
|
||||||
modernc.org/memory v1.5.0 // indirect
|
|
||||||
modernc.org/sqlite v1.23.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -65,18 +65,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -94,8 +88,6 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
@@ -192,9 +184,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
@@ -355,12 +344,4 @@ gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
|||||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
|
||||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
|
||||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
|
||||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
|
||||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
|
||||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
|
||||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
|
||||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package capabilities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
||||||
|
// to toggle features. Only permissions recognized by the application are exposed.
|
||||||
|
func FromPermissions(perms []string) map[string]bool {
|
||||||
|
if len(perms) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]bool)
|
||||||
|
for _, perm := range perms {
|
||||||
|
if key, ok := normalizeAndAllow(perm); ok {
|
||||||
|
out[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAndAllow(perm string) (string, bool) {
|
||||||
|
perm = strings.ToLower(strings.TrimSpace(perm))
|
||||||
|
if perm == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if _, ok := allowed[perm]; !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return perm, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowed = map[string]struct{}{
|
||||||
|
recordings.PermissionRecordingRead: {},
|
||||||
|
recordings.PermissionRecordingCreate: {},
|
||||||
|
recordings.PermissionRecordingUpdate: {},
|
||||||
|
recordings.PermissionRecordingDelete: {},
|
||||||
|
}
|
||||||
@@ -84,9 +84,8 @@ func (r *approvalRepositoryImpl) LatestByTargets(
|
|||||||
result := make(map[uint]entity.Approval, len(approvableIDs))
|
result := make(map[uint]entity.Approval, len(approvableIDs))
|
||||||
|
|
||||||
q := r.DB().WithContext(ctx).
|
q := r.DB().WithContext(ctx).
|
||||||
Select("DISTINCT ON (approvable_id) *").
|
|
||||||
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
|
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
|
||||||
Order("approvable_id, action_at DESC")
|
Order("action_at DESC")
|
||||||
|
|
||||||
if modifier != nil {
|
if modifier != nil {
|
||||||
q = modifier(q)
|
q = modifier(q)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -10,59 +9,45 @@ import (
|
|||||||
|
|
||||||
// Exists reports whether a record with the given ID exists for type T.
|
// Exists reports whether a record with the given ID exists for type T.
|
||||||
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
|
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
|
||||||
var marker int
|
var count int64
|
||||||
err := db.WithContext(ctx).
|
if err := db.WithContext(ctx).
|
||||||
Model(new(T)).
|
Model(new(T)).
|
||||||
Select("1").
|
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Limit(1).
|
Count(&count).Error; err != nil {
|
||||||
Take(&marker).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
|
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
|
||||||
|
var count int64
|
||||||
q := db.WithContext(ctx).
|
q := db.WithContext(ctx).
|
||||||
Model(new(T)).
|
Model(new(T)).
|
||||||
Select("1").
|
|
||||||
Where("name = ?", name).
|
Where("name = ?", name).
|
||||||
Where("deleted_at IS NULL")
|
Where("deleted_at IS NULL")
|
||||||
if excludeID != nil {
|
if excludeID != nil {
|
||||||
q = q.Where("id <> ?", *excludeID)
|
q = q.Where("id <> ?", *excludeID)
|
||||||
}
|
}
|
||||||
var marker int
|
if err := q.Count(&count).Error; err != nil {
|
||||||
if err := q.Limit(1).Take(&marker).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
|
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
|
||||||
if field == "" {
|
if field == "" {
|
||||||
return false, fmt.Errorf("field is required")
|
return false, fmt.Errorf("field is required")
|
||||||
}
|
}
|
||||||
|
var count int64
|
||||||
q := db.WithContext(ctx).
|
q := db.WithContext(ctx).
|
||||||
Model(new(T)).
|
Model(new(T)).
|
||||||
Select("1").
|
|
||||||
Where(fmt.Sprintf("%s = ?", field), value).
|
Where(fmt.Sprintf("%s = ?", field), value).
|
||||||
Where("deleted_at IS NULL")
|
Where("deleted_at IS NULL")
|
||||||
if excludeID != nil {
|
if excludeID != nil {
|
||||||
q = q.Where("id <> ?", *excludeID)
|
q = q.Where("id <> ?", *excludeID)
|
||||||
}
|
}
|
||||||
var marker int
|
if err := q.Count(&count).Error; err != nil {
|
||||||
if err := q.Limit(1).Take(&marker).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,14 +63,13 @@ func (r *StockAllocationRepositoryImpl) ReleaseByUsable(
|
|||||||
updates["note"] = *note
|
updates["note"] = *note
|
||||||
}
|
}
|
||||||
|
|
||||||
baseDB := r.DB()
|
q := r.DB().WithContext(ctx).
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
|
||||||
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
|
||||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Dipakai untuk semua module yang butuh cek:
|
|
||||||
// "PW ini → warehouse → kandang → project_flock_kandang sudah closing atau belum"
|
|
||||||
func EnsureProjectFlockNotClosedForProductWarehouses(
|
|
||||||
ctx context.Context,
|
|
||||||
db *gorm.DB,
|
|
||||||
productWarehouseIDs []uint,
|
|
||||||
) error {
|
|
||||||
if len(productWarehouseIDs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(db)
|
|
||||||
wRepo := warehouseRepo.NewWarehouseRepository(db)
|
|
||||||
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
|
||||||
|
|
||||||
seenPW := make(map[uint]struct{})
|
|
||||||
seenKandang := make(map[uint]struct{})
|
|
||||||
|
|
||||||
for _, pwID := range productWarehouseIDs {
|
|
||||||
if pwID == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seenPW[pwID]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seenPW[pwID] = struct{}{}
|
|
||||||
|
|
||||||
pw, err := pwRepo.GetByID(ctx, pwID, nil)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Product warehouse %d tidak ditemukan", pwID))
|
|
||||||
}
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
|
||||||
}
|
|
||||||
|
|
||||||
wh, err := wRepo.GetByID(ctx, uint(pw.WarehouseId), nil)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Warehouse %d tidak ditemukan", pw.WarehouseId))
|
|
||||||
}
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warehouse tanpa kandang → bukan kandang produksi → skip
|
|
||||||
if wh.KandangId == nil || *wh.KandangId == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
kandangID := uint(*wh.KandangId)
|
|
||||||
if _, ok := seenKandang[kandangID]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seenKandang[kandangID] = struct{}{}
|
|
||||||
|
|
||||||
pfk, err := pfkRepo.GetActiveByKandangID(ctx, kandangID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
// nggak ada project aktif untuk kandang ini → aman
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
|
||||||
}
|
|
||||||
// INTI RULE: kalau aktif tapi sudah punya ClosedAt → anggap "project sudah closing"
|
|
||||||
if pfk != nil && pfk.ClosedAt != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnsureProjectFlockNotClosedByProjectFlockKandangID(
|
|
||||||
ctx context.Context,
|
|
||||||
db *gorm.DB,
|
|
||||||
pfkIDs []uint,
|
|
||||||
) error {
|
|
||||||
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
|
||||||
|
|
||||||
seen := make(map[uint]struct{})
|
|
||||||
for _, id := range pfkIDs {
|
|
||||||
if id == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[id]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[id] = struct{}{}
|
|
||||||
|
|
||||||
pfk, err := pfkRepo.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Project flock kandang %d tidak ditemukan", id))
|
|
||||||
}
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
|
||||||
}
|
|
||||||
|
|
||||||
if pfk.ClosedAt != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -505,25 +505,12 @@ func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWa
|
|||||||
|
|
||||||
var lots []stockLot
|
var lots []stockLot
|
||||||
for key, cfg := range configs {
|
for key, cfg := range configs {
|
||||||
|
selectStmt := fmt.Sprintf(
|
||||||
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
"%s AS id, %s AS available_qty, %s AS created_at",
|
||||||
|
cfg.Columns.ID,
|
||||||
var selectStmt string
|
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||||
if usesNumericTime {
|
cfg.Columns.CreatedAt,
|
||||||
|
)
|
||||||
selectStmt = fmt.Sprintf(
|
|
||||||
"%s AS id, %s AS available_qty, '1970-01-01 00:00:00 UTC'::timestamp AS created_at",
|
|
||||||
cfg.Columns.ID,
|
|
||||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
selectStmt = fmt.Sprintf(
|
|
||||||
"%s AS id, %s AS available_qty, %s AS created_at",
|
|
||||||
cfg.Columns.ID,
|
|
||||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
|
||||||
cfg.Columns.CreatedAt,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []struct {
|
var rows []struct {
|
||||||
ID uint
|
ID uint
|
||||||
|
|||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_project_flock_kandangs_closed_at;
|
|
||||||
ALTER TABLE project_flock_kandangs
|
|
||||||
DROP COLUMN IF EXISTS closed_at;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
ALTER TABLE project_flock_kandangs
|
|
||||||
ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_flock_kandangs_closed_at
|
|
||||||
ON project_flock_kandangs (closed_at);
|
|
||||||
+1
-1
@@ -20,7 +20,7 @@ ALTER TABLE product_warehouses
|
|||||||
|
|
||||||
-- Restore audit/soft-delete columns
|
-- Restore audit/soft-delete columns
|
||||||
ALTER TABLE product_warehouses
|
ALTER TABLE product_warehouses
|
||||||
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id),
|
ADD COLUMN IF NOT EXISTS created_by BIGINT NOT NULL REFERENCES users (id),
|
||||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
|
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 updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|||||||
-33
@@ -1,33 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
-- Remove grading details from recording_eggs
|
|
||||||
ALTER TABLE recording_eggs
|
|
||||||
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
|
||||||
|
|
||||||
ALTER TABLE recording_eggs
|
|
||||||
DROP COLUMN IF EXISTS weight;
|
|
||||||
|
|
||||||
ALTER TABLE recording_eggs
|
|
||||||
ADD CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0);
|
|
||||||
|
|
||||||
-- Restore grading_eggs table for rollback scenarios
|
|
||||||
CREATE TABLE grading_eggs (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
recording_egg_id BIGINT NOT NULL,
|
|
||||||
qty NUMERIC(15,3) NOT NULL,
|
|
||||||
grade VARCHAR,
|
|
||||||
created_by BIGINT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
|
|
||||||
CONSTRAINT fk_grading_eggs_recording_egg
|
|
||||||
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
|
|
||||||
CONSTRAINT fk_grading_eggs_created_by
|
|
||||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
|
||||||
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_grading_eggs_recording_egg
|
|
||||||
ON grading_eggs (recording_egg_id);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
-- Remove separate grading table and move grading details into recording_eggs
|
|
||||||
DROP INDEX IF EXISTS idx_grading_eggs_recording_egg;
|
|
||||||
DROP TABLE IF EXISTS grading_eggs;
|
|
||||||
|
|
||||||
ALTER TABLE recording_eggs
|
|
||||||
ADD COLUMN IF NOT EXISTS weight NUMERIC(10,3);
|
|
||||||
|
|
||||||
ALTER TABLE recording_eggs
|
|
||||||
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
|
||||||
|
|
||||||
ALTER TABLE recording_eggs
|
|
||||||
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
|
|
||||||
qty >= 0 AND (weight IS NULL OR weight >= 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE purchase_items
|
|
||||||
DROP CONSTRAINT fk_purchase_items_expense_nonstock;
|
|
||||||
END IF;
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE purchase_items
|
|
||||||
DROP CONSTRAINT fk_purchase_items_project_flock_kandang;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_purchase_items_expense_nonstock_id;
|
|
||||||
DROP INDEX IF EXISTS idx_purchase_items_project_flock_kandang_id;
|
|
||||||
|
|
||||||
ALTER TABLE purchase_items
|
|
||||||
DROP COLUMN IF EXISTS expense_nonstock_id,
|
|
||||||
DROP COLUMN IF EXISTS project_flock_kandang_id,
|
|
||||||
ALTER COLUMN vehicle_number DROP NOT NULL,
|
|
||||||
ALTER COLUMN vehicle_number TYPE VARCHAR USING vehicle_number;
|
|
||||||
|
|
||||||
ALTER TABLE purchases
|
|
||||||
ALTER COLUMN pr_number TYPE VARCHAR USING pr_number,
|
|
||||||
ALTER COLUMN po_number TYPE VARCHAR USING po_number,
|
|
||||||
ALTER COLUMN created_at DROP DEFAULT,
|
|
||||||
ALTER COLUMN updated_at DROP DEFAULT;
|
|
||||||
|
|
||||||
ALTER TABLE purchases
|
|
||||||
ADD COLUMN credit_term INT NOT NULL DEFAULT 0,
|
|
||||||
ADD COLUMN grand_total NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
ALTER TABLE purchases
|
|
||||||
ALTER COLUMN credit_term DROP DEFAULT,
|
|
||||||
ALTER COLUMN grand_total DROP DEFAULT;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
-- Adjust purchases table to new purchasing schema
|
|
||||||
ALTER TABLE purchases
|
|
||||||
ALTER COLUMN pr_number TYPE VARCHAR(50) USING LEFT(pr_number, 50),
|
|
||||||
ALTER COLUMN po_number TYPE VARCHAR(50) USING LEFT(po_number, 50),
|
|
||||||
ALTER COLUMN created_at SET DEFAULT now(),
|
|
||||||
ALTER COLUMN updated_at SET DEFAULT now();
|
|
||||||
|
|
||||||
ALTER TABLE purchases
|
|
||||||
DROP COLUMN IF EXISTS credit_term,
|
|
||||||
DROP COLUMN IF EXISTS grand_total;
|
|
||||||
|
|
||||||
-- Bring purchase_items in line with new requirements
|
|
||||||
ALTER TABLE purchase_items
|
|
||||||
ADD COLUMN IF NOT EXISTS expense_nonstock_id BIGINT,
|
|
||||||
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
|
|
||||||
|
|
||||||
UPDATE purchase_items
|
|
||||||
SET vehicle_number = ''
|
|
||||||
WHERE vehicle_number IS NULL;
|
|
||||||
|
|
||||||
ALTER TABLE purchase_items
|
|
||||||
ALTER COLUMN vehicle_number TYPE VARCHAR(10) USING LEFT(vehicle_number, 10),
|
|
||||||
ALTER COLUMN vehicle_number SET NOT NULL;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
|
||||||
) THEN
|
|
||||||
EXECUTE
|
|
||||||
'ALTER TABLE purchase_items
|
|
||||||
ADD CONSTRAINT fk_purchase_items_expense_nonstock
|
|
||||||
FOREIGN KEY (expense_nonstock_id)
|
|
||||||
REFERENCES expense_nonstocks(id)
|
|
||||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
|
||||||
) THEN
|
|
||||||
EXECUTE
|
|
||||||
'ALTER TABLE purchase_items
|
|
||||||
ADD CONSTRAINT fk_purchase_items_project_flock_kandang
|
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
|
||||||
REFERENCES project_flock_kandangs(id)
|
|
||||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_expense_nonstock_id
|
|
||||||
ON purchase_items (expense_nonstock_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_project_flock_kandang_id
|
|
||||||
ON purchase_items (project_flock_kandang_id);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- Drop function and sequence for sales order numbers
|
|
||||||
DROP FUNCTION IF EXISTS generate_so_number();
|
|
||||||
DROP SEQUENCE IF EXISTS so_number_seq;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
-- Create sequence for sales order numbers
|
|
||||||
CREATE SEQUENCE so_number_seq START WITH 1 INCREMENT BY 1;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION generate_so_number()
|
|
||||||
RETURNS VARCHAR AS $$
|
|
||||||
DECLARE
|
|
||||||
next_val INTEGER;
|
|
||||||
BEGIN
|
|
||||||
next_val := nextval('so_number_seq');
|
|
||||||
RETURN 'SO-' || LPAD(next_val::TEXT, 5, '0');
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE purchases
|
|
||||||
DROP COLUMN IF EXISTS credit_term;
|
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
ALTER TABLE purchases
|
|
||||||
ADD COLUMN IF NOT EXISTS credit_term INT NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
ALTER TABLE purchases
|
|
||||||
ALTER COLUMN credit_term DROP DEFAULT;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_payments_bank_id;
|
|
||||||
DROP INDEX IF EXISTS payments_party_polymorphic;
|
|
||||||
DROP TABLE IF EXISTS payments;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS payments (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
payment_code VARCHAR(50) NOT NULL,
|
|
||||||
reference_number VARCHAR(100) NULL,
|
|
||||||
transaction_type VARCHAR(50),
|
|
||||||
party_type VARCHAR(50) NOT NULL,
|
|
||||||
party_id BIGINT NOT NULL,
|
|
||||||
payment_date TIMESTAMPTZ NOT NULL,
|
|
||||||
payment_method VARCHAR(20) NOT NULL,
|
|
||||||
bank_id BIGINT NULL REFERENCES banks(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
|
||||||
direction VARCHAR(5) NOT NULL,
|
|
||||||
nominal NUMERIC(15, 3) NOT NULL,
|
|
||||||
notes TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ NULL,
|
|
||||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
CREATE INDEX payments_party_polymorphic ON payments (party_type, party_id);
|
|
||||||
CREATE INDEX idx_payments_bank_id ON payments (bank_id);
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
r record;
|
|
||||||
trigger_name text;
|
|
||||||
BEGIN
|
|
||||||
FOR r IN
|
|
||||||
SELECT table_schema, table_name
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE column_name = 'deleted_at'
|
|
||||||
AND table_schema = 'public'
|
|
||||||
GROUP BY table_schema, table_name
|
|
||||||
LOOP
|
|
||||||
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
|
||||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
DROP FUNCTION IF EXISTS soft_delete_handle_fk();
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
|
|
||||||
DECLARE
|
|
||||||
fk record;
|
|
||||||
child_column text;
|
|
||||||
parent_column text;
|
|
||||||
parent_value text;
|
|
||||||
child_has_deleted_at boolean;
|
|
||||||
ref_exists boolean;
|
|
||||||
sql text;
|
|
||||||
BEGIN
|
|
||||||
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
|
||||||
FOR fk IN
|
|
||||||
SELECT conrelid::regclass AS child_table,
|
|
||||||
conkey AS child_cols,
|
|
||||||
confkey AS parent_cols,
|
|
||||||
confdeltype
|
|
||||||
FROM pg_constraint
|
|
||||||
WHERE contype = 'f'
|
|
||||||
AND confrelid = TG_RELID
|
|
||||||
LOOP
|
|
||||||
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
|
|
||||||
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
|
|
||||||
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
|
|
||||||
CONTINUE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT attname INTO child_column
|
|
||||||
FROM pg_attribute
|
|
||||||
WHERE attrelid = fk.child_table
|
|
||||||
AND attnum = fk.child_cols[1]
|
|
||||||
AND NOT attisdropped;
|
|
||||||
|
|
||||||
SELECT attname INTO parent_column
|
|
||||||
FROM pg_attribute
|
|
||||||
WHERE attrelid = TG_RELID
|
|
||||||
AND attnum = fk.parent_cols[1]
|
|
||||||
AND NOT attisdropped;
|
|
||||||
|
|
||||||
EXECUTE format('SELECT ($1).%I', parent_column)
|
|
||||||
INTO parent_value
|
|
||||||
USING OLD;
|
|
||||||
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM pg_attribute
|
|
||||||
WHERE attrelid = fk.child_table
|
|
||||||
AND attname = 'deleted_at'
|
|
||||||
AND NOT attisdropped
|
|
||||||
) INTO child_has_deleted_at;
|
|
||||||
|
|
||||||
IF fk.confdeltype IN ('r', 'a') THEN
|
|
||||||
sql := format(
|
|
||||||
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)',
|
|
||||||
fk.child_table,
|
|
||||||
child_column,
|
|
||||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
|
||||||
);
|
|
||||||
EXECUTE sql INTO ref_exists USING parent_value;
|
|
||||||
IF ref_exists THEN
|
|
||||||
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
|
|
||||||
TG_TABLE_NAME, fk.child_table;
|
|
||||||
END IF;
|
|
||||||
ELSIF fk.confdeltype = 'n' THEN
|
|
||||||
sql := format(
|
|
||||||
'UPDATE %s SET %I = NULL WHERE %I = $1 %s',
|
|
||||||
fk.child_table,
|
|
||||||
child_column,
|
|
||||||
child_column,
|
|
||||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
|
||||||
);
|
|
||||||
EXECUTE sql USING parent_value;
|
|
||||||
ELSIF fk.confdeltype = 'c' THEN
|
|
||||||
IF child_has_deleted_at THEN
|
|
||||||
sql := format(
|
|
||||||
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL',
|
|
||||||
fk.child_table,
|
|
||||||
child_column
|
|
||||||
);
|
|
||||||
EXECUTE sql USING parent_value;
|
|
||||||
ELSE
|
|
||||||
sql := format(
|
|
||||||
'DELETE FROM %s WHERE %I = $1',
|
|
||||||
fk.child_table,
|
|
||||||
child_column
|
|
||||||
);
|
|
||||||
EXECUTE sql USING parent_value;
|
|
||||||
END IF;
|
|
||||||
ELSIF fk.confdeltype = 'd' THEN
|
|
||||||
sql := format(
|
|
||||||
'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s',
|
|
||||||
fk.child_table,
|
|
||||||
child_column,
|
|
||||||
child_column,
|
|
||||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
|
||||||
);
|
|
||||||
EXECUTE sql USING parent_value;
|
|
||||||
END IF;
|
|
||||||
END LOOP;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
r record;
|
|
||||||
trigger_name text;
|
|
||||||
BEGIN
|
|
||||||
FOR r IN
|
|
||||||
SELECT table_schema, table_name
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE column_name = 'deleted_at'
|
|
||||||
AND table_schema = 'public'
|
|
||||||
GROUP BY table_schema, table_name
|
|
||||||
LOOP
|
|
||||||
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
|
||||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
|
||||||
EXECUTE format(
|
|
||||||
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
|
|
||||||
trigger_name,
|
|
||||||
r.table_schema,
|
|
||||||
r.table_name
|
|
||||||
);
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP SEQUENCE IF EXISTS payments_code_seq;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CREATE SEQUENCE IF NOT EXISTS payments_code_seq START WITH 1 INCREMENT BY 1;
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
-- Rollback: restore document columns to expenses table
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS document_path JSON;
|
|
||||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS realization_document_path JSON;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- Delete document columns from expenses table since we now use Document service with polymorphic relations
|
|
||||||
ALTER TABLE expenses DROP COLUMN IF EXISTS document_path;
|
|
||||||
ALTER TABLE expenses DROP COLUMN IF EXISTS realization_document_path;
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- Rollback: Remove FIFO fields and restore qty column
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
-- STEP 1: Drop indexes
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_fifo_lookup;
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_pending_qty;
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_usage_qty;
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_created_at;
|
|
||||||
|
|
||||||
-- STEP 2: Drop constraints
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP CONSTRAINT IF EXISTS chk_marketing_delivery_products_fifo_nonneg;
|
|
||||||
|
|
||||||
-- STEP 3: Restore qty column from usage_qty data
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
|
|
||||||
|
|
||||||
-- Migrate data back from usage_qty to qty
|
|
||||||
UPDATE marketing_delivery_products
|
|
||||||
SET qty = usage_qty
|
|
||||||
WHERE qty = 0;
|
|
||||||
|
|
||||||
-- STEP 4: Drop FIFO columns
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP COLUMN IF EXISTS usage_qty,
|
|
||||||
DROP COLUMN IF EXISTS pending_qty,
|
|
||||||
DROP COLUMN IF EXISTS created_at;
|
|
||||||
-58
@@ -1,58 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- Add FIFO fields to marketing_delivery_products
|
|
||||||
-- This migration adds fields needed for FIFO stock management
|
|
||||||
-- and removes the old qty field in favor of FIFO-based allocation
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
-- STEP 0: Drop orphan indexes from previous migration
|
|
||||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_deleted_at;
|
|
||||||
|
|
||||||
-- STEP 1: Add created_at column (required for FIFO ordering)
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
|
||||||
|
|
||||||
-- STEP 2: Add FIFO tracking fields
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD COLUMN IF NOT EXISTS usage_qty NUMERIC(15, 3) DEFAULT 0,
|
|
||||||
ADD COLUMN IF NOT EXISTS pending_qty NUMERIC(15, 3) DEFAULT 0;
|
|
||||||
|
|
||||||
-- STEP 3: Migrate data from old qty to usage_qty for existing records
|
|
||||||
-- This preserves existing quantity data as allocated quantity
|
|
||||||
UPDATE marketing_delivery_products
|
|
||||||
SET
|
|
||||||
usage_qty = COALESCE(qty, 0),
|
|
||||||
pending_qty = 0
|
|
||||||
WHERE usage_qty = 0;
|
|
||||||
|
|
||||||
-- STEP 4: Drop the old qty column (replaced by usage_qty + pending_qty)
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP COLUMN IF EXISTS qty;
|
|
||||||
|
|
||||||
-- STEP 5: Make FIFO fields NOT NULL
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ALTER COLUMN usage_qty SET NOT NULL,
|
|
||||||
ALTER COLUMN pending_qty SET NOT NULL,
|
|
||||||
ALTER COLUMN created_at SET NOT NULL;
|
|
||||||
|
|
||||||
-- STEP 6: Add constraints to ensure non-negative values
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD CONSTRAINT chk_marketing_delivery_products_fifo_nonneg CHECK (
|
|
||||||
usage_qty >= 0 AND
|
|
||||||
pending_qty >= 0
|
|
||||||
);
|
|
||||||
|
|
||||||
-- STEP 7: Create indexes for FIFO operations
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_created_at
|
|
||||||
ON marketing_delivery_products(created_at DESC);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_usage_qty
|
|
||||||
ON marketing_delivery_products(usage_qty)
|
|
||||||
WHERE usage_qty > 0;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_pending_qty
|
|
||||||
ON marketing_delivery_products(pending_qty)
|
|
||||||
WHERE pending_qty > 0;
|
|
||||||
|
|
||||||
-- Composite index for FIFO lookups
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_fifo_lookup
|
|
||||||
ON marketing_delivery_products(marketing_product_id, created_at DESC);
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
-- Remove foreign key constraint
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_product_warehouse;
|
|
||||||
|
|
||||||
-- Drop product_warehouse_id column
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
DROP COLUMN IF EXISTS product_warehouse_id;
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
-- Add product_warehouse_id column to marketing_delivery_products
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD COLUMN IF NOT EXISTS product_warehouse_id INT NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
-- Fill product_warehouse_id from marketing_products
|
|
||||||
UPDATE marketing_delivery_products mdp
|
|
||||||
SET product_warehouse_id = mp.product_warehouse_id
|
|
||||||
FROM marketing_products mp
|
|
||||||
WHERE mdp.marketing_product_id = mp.id
|
|
||||||
AND mdp.product_warehouse_id = 0;
|
|
||||||
|
|
||||||
-- Set NOT NULL constraint
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ALTER COLUMN product_warehouse_id SET NOT NULL;
|
|
||||||
|
|
||||||
-- Add foreign key constraint
|
|
||||||
ALTER TABLE marketing_delivery_products
|
|
||||||
ADD CONSTRAINT fk_marketing_delivery_products_product_warehouse
|
|
||||||
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id);
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
-- Drop indexes
|
|
||||||
DROP INDEX IF EXISTS idx_standard_growth_details_standard_week;
|
|
||||||
DROP INDEX IF EXISTS idx_production_standard_details_standard_week;
|
|
||||||
DROP INDEX IF EXISTS idx_production_standards_project_category;
|
|
||||||
DROP INDEX IF EXISTS idx_production_standards_deleted_at;
|
|
||||||
|
|
||||||
-- Drop tables (in reverse order due to foreign keys)
|
|
||||||
DROP TABLE IF EXISTS standard_growth_details;
|
|
||||||
DROP TABLE IF EXISTS production_standard_details;
|
|
||||||
DROP TABLE IF EXISTS production_standards;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
-- Create production_standards table
|
|
||||||
CREATE TABLE IF NOT EXISTS production_standards (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
project_category VARCHAR(20) NOT NULL CHECK (project_category IN ('GROWING', 'LAYING')),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ,
|
|
||||||
created_by BIGINT
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create index for deleted_at (soft delete)
|
|
||||||
CREATE INDEX idx_production_standards_deleted_at ON production_standards(deleted_at);
|
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke users
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE production_standards
|
|
||||||
ADD CONSTRAINT fk_production_standards_created_by
|
|
||||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Index
|
|
||||||
CREATE INDEX idx_production_standards_created_by ON production_standards(created_by);
|
|
||||||
|
|
||||||
-- Create production_standard_details table
|
|
||||||
CREATE TABLE IF NOT EXISTS production_standard_details (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
production_standard_id BIGINT NOT NULL,
|
|
||||||
week INT NOT NULL,
|
|
||||||
target_hen_day_production NUMERIC(15, 3),
|
|
||||||
target_hen_house_production NUMERIC(15, 3),
|
|
||||||
target_egg_weight NUMERIC(15, 3),
|
|
||||||
target_egg_mass NUMERIC(15, 3),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke production_standards
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
|
|
||||||
ALTER TABLE production_standard_details
|
|
||||||
ADD CONSTRAINT fk_production_standard_details_standard
|
|
||||||
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Create unique constraint for standard_id + week
|
|
||||||
CREATE UNIQUE INDEX idx_production_standard_details_standard_week
|
|
||||||
ON production_standard_details(production_standard_id, week);
|
|
||||||
|
|
||||||
-- Create standard_growth_details table
|
|
||||||
CREATE TABLE IF NOT EXISTS standard_growth_details (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
production_standard_id BIGINT NOT NULL,
|
|
||||||
target_mean_bw NUMERIC(15, 3),
|
|
||||||
max_depletion NUMERIC(15, 3),
|
|
||||||
min_uniformity NUMERIC(15, 3) NOT NULL,
|
|
||||||
week INT NOT NULL,
|
|
||||||
feed_intake NUMERIC(15, 3),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
created_by BIGINT
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke production_standards
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
|
|
||||||
ALTER TABLE standard_growth_details
|
|
||||||
ADD CONSTRAINT fk_standard_growth_details_standard
|
|
||||||
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke users
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE standard_growth_details
|
|
||||||
ADD CONSTRAINT fk_standard_growth_details_created_by
|
|
||||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Create unique constraint for standard_id + week
|
|
||||||
CREATE UNIQUE INDEX idx_standard_growth_details_standard_week
|
|
||||||
ON standard_growth_details(production_standard_id, week);
|
|
||||||
|
|
||||||
-- Index
|
|
||||||
CREATE INDEX idx_standard_growth_details_created_by ON standard_growth_details(created_by);
|
|
||||||
|
|
||||||
-- Create index for project_category
|
|
||||||
CREATE INDEX idx_production_standards_project_category ON production_standards(project_category);
|
|
||||||
@@ -588,7 +588,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagAyamAfkir},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Mati",
|
Name: "Ayam Mati",
|
||||||
@@ -597,7 +596,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagAyamMati},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Culling",
|
Name: "Ayam Culling",
|
||||||
@@ -606,7 +604,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagAyamCulling},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Konsumsi Baik",
|
Name: "Telur Konsumsi Baik",
|
||||||
@@ -615,7 +612,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Unit",
|
Uom: "Unit",
|
||||||
Category: "Telur",
|
Category: "Telur",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagTelurUtuh},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Pecah",
|
Name: "Telur Pecah",
|
||||||
@@ -624,7 +620,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Uom: "Unit",
|
Uom: "Unit",
|
||||||
Category: "Telur",
|
Category: "Telur",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
Flags: []utils.FlagType{utils.FlagTelurPecah},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "281 SPECIAL STARTER",
|
Name: "281 SPECIAL STARTER",
|
||||||
@@ -637,16 +632,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||||
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
|
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "Ayam Layer",
|
|
||||||
Brand: "-",
|
|
||||||
Sku: "LYR0001",
|
|
||||||
Uom: "Ekor",
|
|
||||||
Category: "Pullet",
|
|
||||||
Price: 20000,
|
|
||||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
|
||||||
Flags: []utils.FlagType{utils.FlagLayer},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, seed := range seeds {
|
for _, seed := range seeds {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -12,6 +13,8 @@ type Expense struct {
|
|||||||
SupplierId uint64 `gorm:""`
|
SupplierId uint64 `gorm:""`
|
||||||
Category string `gorm:"type:varchar(50);not null"`
|
Category string `gorm:"type:varchar(50);not null"`
|
||||||
PoNumber string `gorm:"type:varchar(50)"`
|
PoNumber string `gorm:"type:varchar(50)"`
|
||||||
|
DocumentPath sql.NullString `gorm:"type:json"`
|
||||||
|
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
|
||||||
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
|
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
|
||||||
TransactionDate time.Time `gorm:"type:date;not null"`
|
TransactionDate time.Time `gorm:"type:date;not null"`
|
||||||
Notes string `gorm:"type:text;column:notes"`
|
Notes string `gorm:"type:text;column:notes"`
|
||||||
@@ -20,10 +23,8 @@ type Expense struct {
|
|||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||||
Documents []Document `gorm:"foreignKey:DocumentableId;references:Id"`
|
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
||||||
RealizationDocuments []Document `gorm:"foreignKey:DocumentableId;references:Id"`
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Initial struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
ReferenceNumber string `gorm:"type:varchar(100);not null"`
|
|
||||||
TransactionType string `gorm:"type:varchar(50);not null"`
|
|
||||||
InitialBalanceType string `gorm:"type:varchar(20);not null"`
|
|
||||||
PartyType string `gorm:"type:varchar(50);not null;index:initials_party_polymorphic,priority:1"`
|
|
||||||
PartyId uint `gorm:"not null;index:initials_party_polymorphic,priority:2"`
|
|
||||||
BankId *uint `gorm:"index"`
|
|
||||||
Direction string `gorm:"type:varchar(5);not null"`
|
|
||||||
Nominal float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
Notes string `gorm:"type:text;not null"`
|
|
||||||
CreatedBy uint `gorm:"index" json:"-"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
|
|
||||||
Bank Bank `gorm:"foreignKey:BankId;references:Id"`
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
|
|
||||||
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,5 @@ type Kandang struct {
|
|||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||||
Warehouses []Warehouse `gorm:"foreignKey:KandangId;references:Id"`
|
|
||||||
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MarketingDeliveryProduct struct {
|
type MarketingDeliveryProduct struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||||
ProductWarehouseId uint `gorm:"not null"`
|
Qty float64 `gorm:"type:numeric(15,3)"`
|
||||||
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||||
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||||
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||||
VehicleNumber string `gorm:"type:varchar(50)"`
|
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||||
|
|
||||||
// FIFO Fields
|
|
||||||
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
|
||||||
PendingQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
|
||||||
CreatedAt *time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
|
|
||||||
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Payment struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
PaymentCode string `gorm:"type:varchar(50);not null"`
|
|
||||||
ReferenceNumber *string `gorm:"type:varchar(100)"`
|
|
||||||
TransactionType string `gorm:"type:varchar(50)"`
|
|
||||||
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
|
|
||||||
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
|
|
||||||
PaymentDate time.Time `gorm:"not null"`
|
|
||||||
PaymentMethod string `gorm:"type:varchar(20);not null"`
|
|
||||||
BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
|
|
||||||
Direction string `gorm:"type:varchar(5);not null"`
|
|
||||||
Nominal float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
Notes string `gorm:"type:text;not null"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
CreatedBy uint `gorm:"index" json:"-"`
|
|
||||||
|
|
||||||
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
|
|
||||||
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
|
||||||
}
|
|
||||||
@@ -8,8 +8,7 @@ type ProductWarehouse struct {
|
|||||||
Quantity float64 `gorm:"column:qty;type:numeric(15,3);default:0"`
|
Quantity float64 `gorm:"column:qty;type:numeric(15,3);default:0"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
StockLogs []StockLog `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
StockLogs []StockLog `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProductionStandard struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
Name string `gorm:"type:varchar(100);uniqueIndex;not null"`
|
|
||||||
ProjectCategory string `gorm:"type:varchar(20);not null"`
|
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
DeletedAt *time.Time `gorm:"type:timestamptz"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
ProductionStandardDetails []ProductionStandardDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
|
||||||
StandardGrowthDetails []StandardGrowthDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProductionStandardDetail struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
ProductionStandardId uint `gorm:"not null"`
|
|
||||||
Week int `gorm:"not null"`
|
|
||||||
TargetHenDayProduction *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
TargetEggWeight *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
TargetEggMass *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
|
|
||||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
|
||||||
}
|
|
||||||
@@ -5,13 +5,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectBudget struct {
|
type ProjectBudget struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockId uint `gorm:"not null"`
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
NonstockId uint `gorm:"not null"`
|
Price float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
|
|
||||||
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"`
|
||||||
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ type ProjectFlock struct {
|
|||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
||||||
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||||
Budgets []ProjectBudget `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
|
||||||
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ 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"`
|
||||||
ClosedAt *time.Time `gorm:"index"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
|
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
|||||||
@@ -5,18 +5,19 @@ 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 `gorm:"column:credit_term;not null;default:0"`
|
CreditTerm *int
|
||||||
DueDate *time.Time
|
DueDate *time.Time
|
||||||
|
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
Notes *string
|
Notes *string
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
|||||||
@@ -5,25 +5,22 @@ 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
|
||||||
ProjectFlockKandangId *uint
|
ReceivedDate *time.Time
|
||||||
ReceivedDate *time.Time
|
TravelNumber *string
|
||||||
TravelNumber *string
|
TravelNumberDocs *string
|
||||||
TravelNumberDocs *string
|
VehicleNumber *string
|
||||||
VehicleNumber *string
|
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
|
||||||
ExpenseNonstockId *uint64
|
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
|
||||||
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||||
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
|
|||||||
@@ -7,11 +7,24 @@ type RecordingEgg struct {
|
|||||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
Qty int `gorm:"column:qty;not null"`
|
Qty int `gorm:"column:qty;not null"`
|
||||||
Weight *float64 `gorm:"column:weight"`
|
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GradingEgg struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
|
||||||
|
Qty float64 `gorm:"column:qty;not null"`
|
||||||
|
Grade string `gorm:"column:grade;type:varchar(50)"`
|
||||||
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|
||||||
|
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StandardGrowthDetail struct {
|
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
|
||||||
ProductionStandardId uint `gorm:"not null"`
|
|
||||||
TargetMeanBw *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
MaxDepletion *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
MinUniformity float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
Week int `gorm:"not null"`
|
|
||||||
FeedIntake *float64 `gorm:"type:numeric(15,3)"`
|
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
|
||||||
|
|
||||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
|
||||||
}
|
|
||||||
@@ -20,5 +20,4 @@ type StockTransfer struct {
|
|||||||
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
||||||
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
||||||
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ package entities
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogTypeAdjustment = "ADJUSTMENT"
|
||||||
|
LogTypeTransfer = "TRANSFER"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TransactionTypeIncrease = "INCREASE"
|
||||||
|
TransactionTypeDecrease = "DECREASE"
|
||||||
|
)
|
||||||
|
|
||||||
type StockLog struct {
|
type StockLog struct {
|
||||||
Id uint `gorm:"primaryKey;column:id"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
||||||
|
|||||||
@@ -4,21 +4,20 @@ import "time"
|
|||||||
|
|
||||||
// DETAIL EKSPEDISI
|
// DETAIL EKSPEDISI
|
||||||
type StockTransferDelivery struct {
|
type StockTransferDelivery struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
StockTransferId uint64
|
StockTransferId uint64
|
||||||
SupplierId uint64
|
SupplierId uint64
|
||||||
VehiclePlate string
|
VehiclePlate string
|
||||||
DriverName string
|
DriverName string
|
||||||
DocumentNumber string
|
DocumentNumber string
|
||||||
DocumentPath string
|
DocumentPath string
|
||||||
ShippingCostItem float64
|
ShippingCostItem float64
|
||||||
ShippingCostTotal float64
|
ShippingCostTotal float64
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
// Relations
|
// Relations
|
||||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||||
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
||||||
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
||||||
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
}
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Transaction struct {
|
|
||||||
Id uint `gorm:"primaryKey"`
|
|
||||||
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,14 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -90,6 +91,7 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl
|
|||||||
|
|
||||||
c.Locals(authContextLocalsKey, ctx)
|
c.Locals(authContextLocalsKey, ctx)
|
||||||
c.Locals(authUserLocalsKey, user)
|
c.Locals(authUserLocalsKey, user)
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,12 +106,11 @@ 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
|
||||||
return 1, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthDetails returns the full authentication context (token, claims, user).
|
// AuthDetails returns the full authentication context (token, claims, user).
|
||||||
@@ -198,71 +199,3 @@ func hasAllScopes(have, required []string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
|
||||||
func RequirePermissions(perms ...string) fiber.Handler {
|
|
||||||
required := canonicalPermissions(perms)
|
|
||||||
return func(c *fiber.Ctx) error {
|
|
||||||
if len(required) == 0 {
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
|
||||||
}
|
|
||||||
|
|
||||||
userPerms := ctx.permissionSet()
|
|
||||||
if len(userPerms) == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, perm := range required {
|
|
||||||
if _, has := userPerms[perm]; !has {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPermission reports whether the current request context includes the given permission.
|
|
||||||
func HasPermission(c *fiber.Ctx, perm string) bool {
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
perm = canonicalPermission(perm)
|
|
||||||
if perm == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, has := ctx.permissionSet()[perm]
|
|
||||||
return has
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AuthContext) permissionSet() map[string]struct{} {
|
|
||||||
if a == nil || a.Permissions == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return a.Permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermissions(perms []string) []string {
|
|
||||||
out := make([]string, 0, len(perms))
|
|
||||||
seen := make(map[string]struct{}, len(perms))
|
|
||||||
for _, perm := range perms {
|
|
||||||
if canonical := canonicalPermission(perm); canonical != "" {
|
|
||||||
if _, ok := seen[canonical]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[canonical] = struct{}{}
|
|
||||||
out = append(out, canonical)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermission(perm string) string {
|
|
||||||
return strings.ToLower(strings.TrimSpace(perm))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,208 +1,75 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
// project-flock
|
import (
|
||||||
const (
|
"strings"
|
||||||
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
|
||||||
P_ProjectFlockKandangsCheckClosing = "lti.production.project_flock_kandangs.closing.detail"
|
|
||||||
P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list"
|
|
||||||
P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail"
|
|
||||||
|
|
||||||
P_ProjectFlockGetAll = "lti.production.project_flocks.list"
|
"github.com/gofiber/fiber/v2"
|
||||||
P_ProjectFlockCreate = "lti.production.project_flocks.create"
|
|
||||||
P_ProjectFlockGetOne = "lti.production.project_flocks.detail"
|
|
||||||
P_ProjectFlockUpdate = "lti.production.project_flocks.update"
|
|
||||||
P_ProjectFlockDelete = "lti.production.project_flocks.delete"
|
|
||||||
P_ProjectFlockApprove = "lti.production.project_flocks.approve"
|
|
||||||
P_ProjectFlockLookup = "lti.production.project_flocks.lookup"
|
|
||||||
P_ProjectFlockNextPeriod = "lti.production.project_flocks.next_period"
|
|
||||||
P_ProjectFlockResubmit = "lti.production.project_flocks.resubmit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
||||||
P_ExpenseGetAll = "lti.expense.list"
|
func RequirePermissions(perms ...string) fiber.Handler {
|
||||||
P_ExpenseCreateOne = "lti.expense.create"
|
required := canonicalPermissions(perms)
|
||||||
P_ExpenseUpdateOne = "lti.expense.update"
|
return func(c *fiber.Ctx) error {
|
||||||
P_ExpenseGetOne = "lti.expense.detail"
|
if len(required) == 0 {
|
||||||
P_ExpenseDeleteOne = "lti.expense.delete"
|
return c.Next()
|
||||||
P_ExpenseApprovalManager = "lti.expense.approve.manager"
|
}
|
||||||
P_ExpenseApprovalFinance = "lti.expense.approve.finance"
|
|
||||||
P_ExpenseCreateRealizations = "lti.expense.create.realization"
|
|
||||||
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
|
|
||||||
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
|
|
||||||
P_ExpenseDocument = "lti.expense.document"
|
|
||||||
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
P_AdjustmentGetAll = "lti.inventory.list"
|
|
||||||
P_AdjustmentCreate = "lti.inventory.create"
|
|
||||||
P_AdjustmentGetOne = "lti.inventory.detail"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
P_ApprovalGetAll = "lti.approval.list"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
P_ReportExpenseGetAll = "lti.repport.expense.list"
|
|
||||||
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
|
|
||||||
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
ctx, ok := AuthDetails(c)
|
||||||
P_ProductStockGetAll = "lti.inventory.product_stock.list"
|
if !ok || ctx == nil {
|
||||||
P_ProductStockGetOne = "lti.inventory.product_stock.detail"
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
P_ProductWarehousekGetAll = "lti.inventory.product_warehouses.list"
|
}
|
||||||
P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
P_ClosingGetAll = "lti.closing.list"
|
|
||||||
P_ClosingDetail = "lti.closing.detail"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
userPerms := ctx.permissionSet()
|
||||||
P_TransferGetAll = "lti.inventory.transfer.list"
|
if len(userPerms) == 0 {
|
||||||
P_TransferGetOne = "lti.inventory.transfer.detail"
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
P_TransferCreateOne = "lti.inventory.transfer.create"
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
for _, perm := range required {
|
||||||
P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list"
|
if _, has := userPerms[perm]; !has {
|
||||||
P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail"
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create"
|
}
|
||||||
P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update"
|
}
|
||||||
P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete"
|
|
||||||
P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve"
|
|
||||||
P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
return c.Next()
|
||||||
P_DeliveryGetAll = "lti.marketing.delivery_order.list"
|
}
|
||||||
P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
|
}
|
||||||
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
|
|
||||||
P_DeliveryCreateOne = "lti.marketing.delivery_order.Create"
|
|
||||||
P_SalesOrderDelete = "lti.marketing.sales_order.delete"
|
|
||||||
P_SalesOrderApproval = "lti.marketing.sales_order.approve"
|
|
||||||
P_SalesOrderCreateOne = "lti.marketing.sales_order.create"
|
|
||||||
P_SalesOrderUpdateOne = "lti.marketing.sales_order.update"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
// HasPermission reports whether the current request context includes the given permission.
|
||||||
P_AreaGetAll = "lti.master.area.list"
|
func HasPermission(c *fiber.Ctx, perm string) bool {
|
||||||
P_AreaGetOne = "lti.master.area.detail"
|
ctx, ok := AuthDetails(c)
|
||||||
P_AreaCreateOne = "lti.master.area.create"
|
if !ok || ctx == nil {
|
||||||
P_AreaUpdateOne = "lti.master.area.update"
|
return false
|
||||||
P_AreaDeleteOne = "lti.master.area.delete"
|
}
|
||||||
|
perm = canonicalPermission(perm)
|
||||||
|
if perm == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, has := ctx.permissionSet()[perm]
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
P_BanksGetAll = "lti.master.banks.list"
|
func (a *AuthContext) permissionSet() map[string]struct{} {
|
||||||
P_BanksGetOne = "lti.master.banks.detail"
|
if a == nil || a.Permissions == nil {
|
||||||
P_BanksCreateOne = "lti.master.banks.create"
|
return nil
|
||||||
P_BanksUpdateOne = "lti.master.banks.update"
|
}
|
||||||
P_BanksDeleteOne = "lti.master.banks.delete"
|
return a.Permissions
|
||||||
|
}
|
||||||
|
|
||||||
P_CustomerGetAll = "lti.master.customer.list"
|
func canonicalPermissions(perms []string) []string {
|
||||||
P_CustomerGetOne = "lti.master.customer.detail"
|
out := make([]string, 0, len(perms))
|
||||||
P_CustomerCreateOne = "lti.master.customer.create"
|
seen := make(map[string]struct{}, len(perms))
|
||||||
P_CustomerUpdateOne = "lti.master.customer.update"
|
for _, perm := range perms {
|
||||||
P_CustomerDeleteOne = "lti.master.customer.delete"
|
if canonical := canonicalPermission(perm); canonical != "" {
|
||||||
|
if _, ok := seen[canonical]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[canonical] = struct{}{}
|
||||||
|
out = append(out, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
P_FcrGetAll = "lti.master.fcr.list"
|
func canonicalPermission(perm string) string {
|
||||||
P_FcrGetOne = "lti.master.fcr.detail"
|
return strings.ToLower(strings.TrimSpace(perm))
|
||||||
P_FcrCreateOne = "lti.master.fcr.create"
|
}
|
||||||
P_FcrUpdateOne = "lti.master.fcr.update"
|
|
||||||
P_FcrDeleteOne = "lti.master.fcr.delete"
|
|
||||||
|
|
||||||
P_FlocksGetAll = "lti.master.flocks.list"
|
|
||||||
P_FlocksGetOne = "lti.master.flocks.detail"
|
|
||||||
P_FlocksCreateOne = "lti.master.flocks.create"
|
|
||||||
P_FlocksUpdateOne = "lti.master.flocks.update"
|
|
||||||
P_FlocksDeleteOne = "lti.master.flocks.delete"
|
|
||||||
|
|
||||||
P_KandangsGetAll = "lti.master.kandangs.list"
|
|
||||||
P_KandangsGetOne = "lti.master.kandangs.detail"
|
|
||||||
P_KandangsCreateOne = "lti.master.kandangs.create"
|
|
||||||
P_KandangsUpdateOne = "lti.master.kandangs.update"
|
|
||||||
P_KandangsDeleteOne = "lti.master.kandangs.delete"
|
|
||||||
|
|
||||||
P_LocationsGetAll = "lti.master.locations.list"
|
|
||||||
P_LocationsGetOne = "lti.master.locations.detail"
|
|
||||||
P_LocationsCreateOne = "lti.master.locations.create"
|
|
||||||
P_LocationsUpdateOne = "lti.master.locations.update"
|
|
||||||
P_LocationsDeleteOne = "lti.master.locations.delete"
|
|
||||||
|
|
||||||
P_NonstocksGetAll = "lti.master.nonstocks.list"
|
|
||||||
P_NonstocksGetOne = "lti.master.nonstocks.detail"
|
|
||||||
P_NonstocksCreateOne = "lti.master.nonstocks.create"
|
|
||||||
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
|
|
||||||
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
|
|
||||||
|
|
||||||
P_ProductCategoriesGetAll = "lti.master.Product_categories.list"
|
|
||||||
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail"
|
|
||||||
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create"
|
|
||||||
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update"
|
|
||||||
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete"
|
|
||||||
|
|
||||||
P_ProductsGetAll = "lti.master.Products.list"
|
|
||||||
P_ProductsGetOne = "lti.master.Products.detail"
|
|
||||||
P_ProductsCreateOne = "lti.master.Products.create"
|
|
||||||
P_ProductsUpdateOne = "lti.master.Products.update"
|
|
||||||
P_ProductsDeleteOne = "lti.master.Products.delete"
|
|
||||||
|
|
||||||
P_SuppliersGetAll = "lti.master.suppliers.list"
|
|
||||||
P_SuppliersGetOne = "lti.master.suppliers.detail"
|
|
||||||
P_SuppliersCreateOne = "lti.master.suppliers.create"
|
|
||||||
P_SuppliersUpdateOne = "lti.master.suppliers.update"
|
|
||||||
P_SuppliersDeleteOne = "lti.master.suppliers.delete"
|
|
||||||
|
|
||||||
P_UomsGetAll = "lti.master.uoms.list"
|
|
||||||
P_UomsGetOne = "lti.master.uoms.detail"
|
|
||||||
P_UomsCreateOne = "lti.master.uoms.create"
|
|
||||||
P_UomsUpdateOne = "lti.master.uoms.update"
|
|
||||||
P_UomsDeleteOne = "lti.master.uoms.delete"
|
|
||||||
|
|
||||||
P_WarehousesGetAll = "lti.master.warehouses.list"
|
|
||||||
P_WarehousesGetOne = "lti.master.warehouses.detail"
|
|
||||||
P_WarehousesCreateOne = "lti.master.warehouses.create"
|
|
||||||
P_WarehousesUpdateOne = "lti.master.warehouses.update"
|
|
||||||
P_WarehousesDeleteOne = "lti.master.warehouses.delete"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
P_ChickinsCreateOne = "lti.production.chickins.create"
|
|
||||||
P_ChickinsGetOne = "lti.production.chickins.detail"
|
|
||||||
P_ChickinsApproval = "lti.production.chickins.approve"
|
|
||||||
)
|
|
||||||
|
|
||||||
// recording
|
|
||||||
const (
|
|
||||||
P_RecordingGetAll = "lti.production.recording.list"
|
|
||||||
P_RecordingGetOne = "lti.production.recording.detail"
|
|
||||||
P_RecordingCreateOne = "lti.production.recording.create"
|
|
||||||
P_RecordingUpdateOne = "lti.production.recording.update"
|
|
||||||
P_RecordingDeleteOne = "lti.production.recording.delete"
|
|
||||||
P_RecordingNextDay = "lti.production.recording.next_day"
|
|
||||||
P_RecordingApproval = "lti.production.recording.approve"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
P_PurchaseGetAll = "lti.Purchase.list"
|
|
||||||
P_PurchaseGetOne = "lti.Purchase.detail"
|
|
||||||
P_PurchaseCreateOne = "lti.Purchase.create"
|
|
||||||
P_PurchaseUpdateOne = "lti.Purchase.update"
|
|
||||||
P_PurchaseDeleteOne = "lti.Purchase.delete"
|
|
||||||
P_PurchaseItemDeleteOne = "lti.Purchase.delete.item"
|
|
||||||
P_PurchaseReceive = "lti.Purchase.receive"
|
|
||||||
P_PurchaseApprovalStaff = "lti.Purchase.approve.staff"
|
|
||||||
P_PurchaseApprovalManager = "lti.Purchase.approve.manager"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
P_FinanceGetAll = "lti.finance.list"
|
|
||||||
P_FinanceGetOne = "lti.finance.detail"
|
|
||||||
P_FinanceCreateOne = "lti.finance.create"
|
|
||||||
P_FinanceUpdateOne = "lti.finance.update"
|
|
||||||
P_FinanceDeleteOne = "lti.finance.delete"
|
|
||||||
P_FinanceApproval = "lti.finance.approve"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
P_UserGetAll = "lti.users.list"
|
|
||||||
P_UserGetOne = "lti.users.detail"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalServic
|
|||||||
route := v1.Group("/approvals")
|
route := v1.Group("/approvals")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll,m.RequirePermissions(m.P_ApprovalGetAll))
|
route.Get("/", ctrl.GetAll)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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"
|
||||||
@@ -14,14 +13,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClosingController struct {
|
type ClosingController struct {
|
||||||
ClosingService service.ClosingService
|
ClosingService service.ClosingService
|
||||||
SapronakService service.SapronakService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
func NewClosingController(closingService service.ClosingService) *ClosingController {
|
||||||
return &ClosingController{
|
return &ClosingController{
|
||||||
ClosingService: closingService,
|
ClosingService: closingService,
|
||||||
SapronakService: sapronakService,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,17 +39,17 @@ func (u *ClosingController) GetAll(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.SuccessWithPaginate[dto.ClosingListItemDTO]{
|
JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Retrieved closing projects list successfully",
|
Message: "Get all closings 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: result,
|
Data: dto.ToClosingListDTOs(result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,11 +57,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 || id <= 0 {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingService.GetProjectFlockByID(c, uint(id))
|
result, err := u.ClosingService.GetOne(c, uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -73,281 +70,28 @@ 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: "Retrieved closing information successfully",
|
Message: "Get closing successfully",
|
||||||
Data: result,
|
Data: dto.ToClosingListDTO(*result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
func (u *ClosingController) GetSapronakReport(c *fiber.Ctx) error {
|
||||||
param := c.Params("projectFlockId")
|
query := &validation.SapronakQuery{
|
||||||
|
ProjectFlockID: uint(c.QueryInt("project_flock_id", 0)),
|
||||||
id, err := strconv.Atoi(param)
|
KandangID: uint(c.QueryInt("kandang_id", 0)),
|
||||||
if err != nil || id <= 0 {
|
Status: c.Query("status"),
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
}
|
||||||
}
|
|
||||||
|
result, err := u.ClosingService.GetSapronakReport(c, query)
|
||||||
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
return c.Status(fiber.StatusOK).
|
JSON(response.Success{
|
||||||
JSON(response.Success{
|
Code: fiber.StatusOK,
|
||||||
Code: fiber.StatusOK,
|
Status: "success",
|
||||||
Status: "success",
|
Message: "Get sapronak report successfully",
|
||||||
Message: "Retrieved project information successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("project_flock_id")
|
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get closing penjualan successfully",
|
|
||||||
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("project_flock_id")
|
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get overhead successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("projectFlockId")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
|
||||||
}
|
|
||||||
|
|
||||||
query := &validation.ClosingSapronakQuery{
|
|
||||||
Type: strings.ToLower(c.Query("type")),
|
|
||||||
Page: c.QueryInt("page", 1),
|
|
||||||
Limit: c.QueryInt("limit", 10),
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, totalResults, err := u.ClosingService.GetClosingSapronak(c, uint(id), query)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.SuccessWithPaginate[dto.ClosingSapronakItemDTO]{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Retrieved closing report (sapronak) successfully",
|
|
||||||
Meta: response.Meta{
|
|
||||||
Page: query.Page,
|
|
||||||
Limit: query.Limit,
|
|
||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
|
||||||
TotalResults: totalResults,
|
|
||||||
},
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("project_flock_id")
|
|
||||||
flag := c.Query("flag", "")
|
|
||||||
|
|
||||||
projectID, err := strconv.Atoi(param)
|
|
||||||
if err != nil || projectID <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.SapronakService.GetSapronakByProject(c, uint(projectID), flag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := dto.ToSapronakProjectAggregatedFromReports(result, flag)
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get perhitungan sapronak per project successfully",
|
|
||||||
Data: payload,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
|
|
||||||
projectParam := c.Params("project_flock_id")
|
|
||||||
kandangParam := c.Params("project_flock_kandang_id")
|
|
||||||
flag := c.Query("flag", "")
|
|
||||||
|
|
||||||
projectID, err := strconv.Atoi(projectParam)
|
|
||||||
if err != nil || projectID <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
|
||||||
}
|
|
||||||
pfkID, err := strconv.Atoi(kandangParam)
|
|
||||||
if err != nil || pfkID <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID), flag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := dto.ToSapronakProjectAggregatedFromReport(result, flag)
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get perhitungan sapronak per kandang successfully",
|
|
||||||
Data: payload,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("project_flock_id")
|
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get closing keuangan successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("project_flock_id")
|
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(param)
|
|
||||||
if err != nil || projectFlockID <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
var projectFlockKandangID *uint
|
|
||||||
if raw := c.Query("project_flock_kandang_id"); raw != "" {
|
|
||||||
idInt, convErr := strconv.Atoi(raw)
|
|
||||||
if convErr != nil || idInt <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
|
||||||
}
|
|
||||||
idUint := uint(idInt)
|
|
||||||
projectFlockKandangID = &idUint
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), projectFlockKandangID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get expedition HPP successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetExpeditionHPPByKandang(c *fiber.Ctx) error {
|
|
||||||
projectParam := c.Params("project_flock_id")
|
|
||||||
kandangParam := c.Params("project_flock_kandang_id")
|
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(projectParam)
|
|
||||||
if err != nil || projectFlockID <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
pfkID, err := strconv.Atoi(kandangParam)
|
|
||||||
if err != nil || pfkID <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
kandangID := uint(pfkID)
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), &kandangID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get expedition HPP successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("projectFlockId")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Retrieved production data successfully",
|
|
||||||
Data: result,
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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"
|
||||||
@@ -27,147 +26,6 @@ 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingPurchaseDTO struct {
|
|
||||||
InitialPopulation int `json:"initial_population"`
|
|
||||||
ClaimCulling int `json:"claim_culling"`
|
|
||||||
FinalPopulation int `json:"final_population"`
|
|
||||||
FeedIn float64 `json:"feed_in"`
|
|
||||||
FeedUsed float64 `json:"feed_used"`
|
|
||||||
FeedUsedPerHead float64 `json:"feed_used_per_head"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingSalesDTO struct {
|
|
||||||
SalesPopulation int `json:"sales_population"`
|
|
||||||
SalesWeight float64 `json:"sales_weight"`
|
|
||||||
AverageWeight float64 `json:"average_weight"`
|
|
||||||
AverageSellingPrice float64 `json:"chicken_average_selling_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingEggSalesDTO struct {
|
|
||||||
EggPieces int `json:"egg_pieces"`
|
|
||||||
EggMassKg float64 `json:"egg_mass_kg"`
|
|
||||||
AverageEggWeightKg float64 `json:"average_egg_weight_kg"`
|
|
||||||
AverageSellingPrice float64 `json:"egg_average_selling_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingPerformanceDTO struct {
|
|
||||||
Depletion float64 `json:"depletion"`
|
|
||||||
Age float64 `json:"age_day"`
|
|
||||||
MortalityStd float64 `json:"mortality_std"`
|
|
||||||
MortalityAct float64 `json:"mortality_act"`
|
|
||||||
DeffMortality float64 `json:"deff_mortality"`
|
|
||||||
FcrStd float64 `json:"fcr_std"`
|
|
||||||
FcrAct float64 `json:"fcr_act"`
|
|
||||||
DeffFcr float64 `json:"deff_fcr"`
|
|
||||||
Awg float64 `json:"awg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingSalesGroupDTO struct {
|
|
||||||
Chicken ClosingSalesDTO `json:"chicken"`
|
|
||||||
Egg *ClosingEggSalesDTO `json:"egg,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingProductionReportDTO struct {
|
|
||||||
Purchase ClosingPurchaseDTO `json:"purchase"`
|
|
||||||
Sales ClosingSalesGroupDTO `json:"sales"`
|
|
||||||
Performance ClosingPerformanceDTO `json:"performance"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
|
||||||
history := project.KandangHistory
|
|
||||||
|
|
||||||
period := maxPeriod(history)
|
|
||||||
kandangCount := len(history)
|
|
||||||
population := sumPopulation(history)
|
|
||||||
populationInt := int(population)
|
|
||||||
|
|
||||||
return ClosingSummaryDTO{
|
|
||||||
FlockID: project.Id,
|
|
||||||
Period: period,
|
|
||||||
// JenisProduk: project.Category,
|
|
||||||
// LabelPopulasi: "",
|
|
||||||
Population: populationInt,
|
|
||||||
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
|
||||||
ProjectType: project.Category,
|
|
||||||
ActiveHouseCount: kandangCount,
|
|
||||||
ActiveHouseLabel: fmt.Sprintf("%d Kandang", kandangCount),
|
|
||||||
SalesPaymentStatus: "Tempo",
|
|
||||||
// StatusPembayaranMitra: "",
|
|
||||||
StatusProject: statusProject,
|
|
||||||
StatusClosing: statusClosing,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToClosingListItemDTO(project entity.ProjectFlock, projectStatus string) ClosingListItemDTO {
|
|
||||||
shedCount := len(project.KandangHistory)
|
|
||||||
|
|
||||||
return ClosingListItemDTO{
|
|
||||||
Id: project.Id,
|
|
||||||
LocationID: project.LocationId,
|
|
||||||
LocationName: project.Location.Name,
|
|
||||||
ProjectCategory: project.Category,
|
|
||||||
Period: maxPeriod(project.KandangHistory),
|
|
||||||
ClosingDate: "17-Nov-2025",
|
|
||||||
ShedLabel: fmt.Sprintf("%d Kandang", shedCount),
|
|
||||||
ShedCount: shedCount,
|
|
||||||
SalesPaidAmount: 21993726,
|
|
||||||
SalesRemainingAmount: 11075919,
|
|
||||||
SalesPaymentStatus: "Lunas",
|
|
||||||
ProjectStatus: projectStatus,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func maxPeriod(history []entity.ProjectFlockKandang) int {
|
|
||||||
max := 0
|
|
||||||
for _, h := range history {
|
|
||||||
if h.Period > max {
|
|
||||||
max = h.Period
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPopulation(history []entity.ProjectFlockKandang) float64 {
|
|
||||||
var total float64
|
|
||||||
for _, h := range history {
|
|
||||||
for _, chickin := range h.Chickins {
|
|
||||||
total += chickin.UsageQty + chickin.PendingUsageQty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
|
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
|
||||||
@@ -204,20 +62,3 @@ func ToClosingDetailDTO(e entity.ProjectFlock) ClosingDetailDTO {
|
|||||||
ClosingListDTO: ToClosingListDTO(e),
|
ClosingListDTO: ToClosingListDTO(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CalculateAgeFromChickinDataProduksi(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
|
|
||||||
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
|
|
||||||
for _, chickin := range projectFlockKandang.Chickins {
|
|
||||||
if chickin.ChickInDate.Before(earliestChickinDate) {
|
|
||||||
earliestChickinDate = chickin.ChickInDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
|
|
||||||
ageInWeeks := ageInDays / 7
|
|
||||||
return ageInWeeks
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
// ExpeditionCostItemDTO merepresentasikan biaya ekspedisi per vendor.
|
|
||||||
type ExpeditionCostItemDTO struct {
|
|
||||||
Id uint64 `json:"id"`
|
|
||||||
ExpeditionVendorName string `json:"expedition_vendor_name"`
|
|
||||||
HPPAmount float64 `json:"hpp_amount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpeditionHPPDTO adalah struktur response utama untuk HPP Ekspedisi.
|
|
||||||
type ExpeditionHPPDTO struct {
|
|
||||||
ExpeditionCosts []ExpeditionCostItemDTO `json:"expedition_costs"`
|
|
||||||
TotalHPPAmount float64 `json:"total_hpp_amount"`
|
|
||||||
}
|
|
||||||
@@ -1,568 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// === CONSTANTS ===
|
|
||||||
const (
|
|
||||||
HPPGroupPengeluaran = "HPP dan Pengeluaran"
|
|
||||||
HPPGroupBahanBaku = "HPP dan Bahan Baku"
|
|
||||||
HPPLabelOverhead = "Pengeluaran Overhead"
|
|
||||||
HPPLabelEkspedisi = "Beban Ekspedisi"
|
|
||||||
HPPSummaryLabel = "HPP"
|
|
||||||
|
|
||||||
PLSalesTypeChicken = "Penjualan Ayam Besar"
|
|
||||||
PLSalesTypeEgg = "Penjualan Telur"
|
|
||||||
|
|
||||||
PLItemTypeSapronak = "Pembelian Sapronak"
|
|
||||||
PLItemTypeOverhead = "Pengeluaran Overhead"
|
|
||||||
PLItemTypeEkspedisi = "Beban Ekspedisi"
|
|
||||||
|
|
||||||
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
|
|
||||||
PLSummaryLabelSubTotal = "SUB TOTAL"
|
|
||||||
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
|
|
||||||
|
|
||||||
PurchaseLabelPrefix = "Pembelian "
|
|
||||||
)
|
|
||||||
|
|
||||||
// === CONTEXT STRUCTS ===
|
|
||||||
|
|
||||||
type CalculationContext struct {
|
|
||||||
TotalPopulation float64
|
|
||||||
TotalWeightProduced float64
|
|
||||||
TotalDepletion float64
|
|
||||||
TotalWeightSold float64
|
|
||||||
ActualPopulation float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingKeuanganInput struct {
|
|
||||||
ProjectFlockCategory string
|
|
||||||
PurchaseItems []entities.PurchaseItem
|
|
||||||
Budgets []entities.ProjectBudget
|
|
||||||
Realizations []entities.ExpenseRealization
|
|
||||||
DeliveryProducts []entities.MarketingDeliveryProduct
|
|
||||||
Chickins []entities.ProjectChickin
|
|
||||||
TotalWeightProduced float64
|
|
||||||
TotalDepletion float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// === BASE METRICS ===
|
|
||||||
|
|
||||||
type FinancialMetrics struct {
|
|
||||||
RpPerBird float64 `json:"rp_per_bird"`
|
|
||||||
RpPerKg float64 `json:"rp_per_kg"`
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Comparison struct {
|
|
||||||
Budgeting FinancialMetrics `json:"budgeting"`
|
|
||||||
Realization FinancialMetrics `json:"realization"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP PURCHASES PACKAGE ===
|
|
||||||
|
|
||||||
type HppItem struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Comparison
|
|
||||||
}
|
|
||||||
|
|
||||||
type HppGroup struct {
|
|
||||||
GroupName string `json:"group_name"`
|
|
||||||
Data []HppItem `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SummaryHpp struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
Comparison
|
|
||||||
}
|
|
||||||
|
|
||||||
type HppPurchasesSection struct {
|
|
||||||
Hpp []HppGroup `json:"hpp"`
|
|
||||||
SummaryHpp SummaryHpp `json:"summary_hpp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === PROFIT LOSS PACKAGE ===
|
|
||||||
|
|
||||||
type PLItem struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
FinancialMetrics
|
|
||||||
}
|
|
||||||
|
|
||||||
type PLSummaryItem struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
FinancialMetrics
|
|
||||||
}
|
|
||||||
|
|
||||||
type PLSummaryGroup struct {
|
|
||||||
GrossProfit PLSummaryItem `json:"gross_profit"`
|
|
||||||
SubTotal PLSummaryItem `json:"sub_total"`
|
|
||||||
NetProfit PLSummaryItem `json:"net_profit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfitLossData struct {
|
|
||||||
Penjualan []PLItem `json:"penjualan"`
|
|
||||||
Pembelian []PLItem `json:"pembelian"`
|
|
||||||
Overhead PLItem `json:"overhead"`
|
|
||||||
Ekspedisi PLItem `json:"ekspedisi"`
|
|
||||||
Summary PLSummaryGroup `json:"summary"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfitLossSection struct {
|
|
||||||
Data ProfitLossData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === RESPONSE DTO (ROOT) ===
|
|
||||||
|
|
||||||
type ReportResponse struct {
|
|
||||||
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
|
|
||||||
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === MAPPER FUNCTIONS ===
|
|
||||||
|
|
||||||
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
|
||||||
return FinancialMetrics{
|
|
||||||
RpPerBird: rpPerBird,
|
|
||||||
RpPerKg: rpPerKg,
|
|
||||||
Amount: amount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
|
|
||||||
return Comparison{
|
|
||||||
Budgeting: budgeting,
|
|
||||||
Realization: realization,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP PENGELUARAN (from Purchase Items) ===
|
|
||||||
|
|
||||||
func getFlagLabel(flagType utils.FlagType) string {
|
|
||||||
return PurchaseLabelPrefix + string(flagType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
|
|
||||||
flags := []utils.FlagType{
|
|
||||||
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
|
|
||||||
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
|
|
||||||
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
|
|
||||||
}
|
|
||||||
|
|
||||||
items := []HppItem{}
|
|
||||||
seenFlags := make(map[utils.FlagType]bool)
|
|
||||||
|
|
||||||
for _, item := range purchaseItems {
|
|
||||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, flag := range item.Product.Flags {
|
|
||||||
flagType := utils.FlagType(flag.Name)
|
|
||||||
|
|
||||||
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
|
|
||||||
amount := sumPurchasesByFlag(purchaseItems, flagType)
|
|
||||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
items = append(items, HppItem{
|
|
||||||
Type: getFlagLabel(flagType),
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
|
||||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
seenFlags[flagType] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
|
|
||||||
|
|
||||||
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
|
|
||||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
return HppItem{
|
|
||||||
Type: HPPLabelOverhead,
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
|
|
||||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
|
|
||||||
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
return HppItem{
|
|
||||||
Type: HPPLabelEkspedisi,
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
|
||||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
|
|
||||||
items := []HppItem{}
|
|
||||||
|
|
||||||
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
|
||||||
realizationAmount := getOperationalExpenses(realizations)
|
|
||||||
|
|
||||||
if budgetAmount > 0 || realizationAmount > 0 {
|
|
||||||
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
|
||||||
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
|
|
||||||
|
|
||||||
return HppGroup{
|
|
||||||
GroupName: HPPGroupBahanBaku,
|
|
||||||
Data: items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HPP SUMMARY ===
|
|
||||||
|
|
||||||
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) SummaryHpp {
|
|
||||||
purchaseTotal := sumPurchaseTotal(purchaseItems)
|
|
||||||
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
|
||||||
totalBudget := purchaseTotal + budgetTotal
|
|
||||||
|
|
||||||
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
|
|
||||||
|
|
||||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
|
||||||
|
|
||||||
return SummaryHpp{
|
|
||||||
Label: label,
|
|
||||||
Comparison: ToComparison(
|
|
||||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
|
||||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppPurchasesSection {
|
|
||||||
hppGroups := []HppGroup{
|
|
||||||
{
|
|
||||||
GroupName: HPPGroupPengeluaran,
|
|
||||||
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
|
|
||||||
},
|
|
||||||
ToHppBahanBakuGroup(budgets, realizations, ctx),
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, ctx)
|
|
||||||
|
|
||||||
return HppPurchasesSection{
|
|
||||||
Hpp: hppGroups,
|
|
||||||
SummaryHpp: summaryHpp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === PROFIT & LOSS ===
|
|
||||||
|
|
||||||
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
|
|
||||||
return PLItem{
|
|
||||||
Type: itemType,
|
|
||||||
FinancialMetrics: metrics,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
|
|
||||||
return PLSummaryItem{
|
|
||||||
Label: label,
|
|
||||||
FinancialMetrics: metrics,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem {
|
|
||||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced)
|
|
||||||
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
|
|
||||||
for _, item := range items {
|
|
||||||
totalAmount += item.Amount
|
|
||||||
totalPerBird += item.RpPerBird
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem {
|
|
||||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold)
|
|
||||||
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem {
|
|
||||||
items := []PLItem{}
|
|
||||||
|
|
||||||
categorized := categorizeDeliveriesBySalesType(deliveryProducts)
|
|
||||||
|
|
||||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
|
|
||||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
|
||||||
telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg])
|
|
||||||
|
|
||||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
|
||||||
items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx))
|
|
||||||
} else {
|
|
||||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
|
||||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
|
||||||
purchaseAmount := sumPurchaseTotal(purchases)
|
|
||||||
bopAmount := getOperationalExpenses(realizations)
|
|
||||||
totalCost := purchaseAmount + bopAmount
|
|
||||||
|
|
||||||
return []PLItem{
|
|
||||||
createPLItemWithMetrics(PLItemTypeSapronak, totalCost, ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
|
||||||
realizationAmount := getOperationalExpenses(realizations)
|
|
||||||
return []PLItem{
|
|
||||||
createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
|
||||||
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
|
||||||
return []PLItem{
|
|
||||||
createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup {
|
|
||||||
totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems)
|
|
||||||
totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems)
|
|
||||||
totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems)
|
|
||||||
totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems)
|
|
||||||
|
|
||||||
grossProfit := totalPenjualan - totalPembelian
|
|
||||||
grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird
|
|
||||||
|
|
||||||
totalOtherExpenses := totalOverhead + totalEkspedisi
|
|
||||||
totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird
|
|
||||||
|
|
||||||
netProfit := grossProfit - totalOtherExpenses
|
|
||||||
netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird
|
|
||||||
|
|
||||||
return PLSummaryGroup{
|
|
||||||
GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)),
|
|
||||||
SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)),
|
|
||||||
NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData {
|
|
||||||
summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
|
||||||
|
|
||||||
totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead)
|
|
||||||
totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi)
|
|
||||||
|
|
||||||
return ProfitLossData{
|
|
||||||
Penjualan: penjualanItems,
|
|
||||||
Pembelian: pembelianItems,
|
|
||||||
Overhead: totalOverhead,
|
|
||||||
Ekspedisi: totalEkspedisi,
|
|
||||||
Summary: summary,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
|
|
||||||
return ProfitLossSection{
|
|
||||||
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func aggregatePLItems(items []PLItem, label string) PLItem {
|
|
||||||
totalAmount, totalPerBird := sumPLItems(items)
|
|
||||||
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
|
|
||||||
return ReportResponse{
|
|
||||||
HppPurchases: hppPurchases,
|
|
||||||
ProfitLoss: profitLoss,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
|
|
||||||
var totalPopulation float64
|
|
||||||
var totalWeightSold float64
|
|
||||||
|
|
||||||
for _, chickin := range input.Chickins {
|
|
||||||
totalPopulation += chickin.UsageQty
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, delivery := range input.DeliveryProducts {
|
|
||||||
totalWeightSold += delivery.TotalWeight
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := CalculationContext{
|
|
||||||
TotalPopulation: totalPopulation,
|
|
||||||
TotalWeightProduced: input.TotalWeightProduced,
|
|
||||||
TotalDepletion: input.TotalDepletion,
|
|
||||||
TotalWeightSold: totalWeightSold,
|
|
||||||
ActualPopulation: totalPopulation - input.TotalDepletion,
|
|
||||||
}
|
|
||||||
|
|
||||||
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, ctx)
|
|
||||||
penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx)
|
|
||||||
pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx)
|
|
||||||
overheadItems := ToOverheadItems(input.Realizations, ctx)
|
|
||||||
ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx)
|
|
||||||
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
|
||||||
|
|
||||||
return ToReportResponse(hppSection, plSection)
|
|
||||||
}
|
|
||||||
|
|
||||||
// === HELPER FUNCTIONS ===
|
|
||||||
|
|
||||||
func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) {
|
|
||||||
if totalPopulation > 0 {
|
|
||||||
rpPerBird = amount / totalPopulation
|
|
||||||
}
|
|
||||||
if totalWeightSold > 0 {
|
|
||||||
rpPerKg = amount / totalWeightSold
|
|
||||||
}
|
|
||||||
return rpPerBird, rpPerKg
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool {
|
|
||||||
for _, flag := range flags {
|
|
||||||
if strings.ToUpper(flag.Name) == string(flagType) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool {
|
|
||||||
return func(item *entities.PurchaseItem) bool {
|
|
||||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasProductFlag(item.Product.Flags, flagType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
|
||||||
return func(realization *entities.ExpenseRealization) bool {
|
|
||||||
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
|
||||||
hasFlag := filterRealizationByNonstockFlag(flagType)
|
|
||||||
return func(realization *entities.ExpenseRealization) bool {
|
|
||||||
return !hasFlag(realization)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 {
|
|
||||||
amount := 0.0
|
|
||||||
for i := range items {
|
|
||||||
if filter(&items[i]) {
|
|
||||||
amount += extractor(&items[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return amount
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 {
|
|
||||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 {
|
|
||||||
return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType))
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 {
|
|
||||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true })
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 {
|
|
||||||
return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 {
|
|
||||||
return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 {
|
|
||||||
return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
|
|
||||||
}
|
|
||||||
|
|
||||||
func isChickenProductFlag(flagType utils.FlagType) bool {
|
|
||||||
switch flagType {
|
|
||||||
case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer,
|
|
||||||
utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEggProductFlag(flagType utils.FlagType) bool {
|
|
||||||
switch flagType {
|
|
||||||
case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah,
|
|
||||||
utils.FlagTelurPutih, utils.FlagTelurRetak:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSalesTypeFromProductFlags(product *entities.Product) string {
|
|
||||||
if product == nil || len(product.Flags) == 0 {
|
|
||||||
return PLSalesTypeChicken
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, flag := range product.Flags {
|
|
||||||
flagType := utils.FlagType(strings.ToUpper(flag.Name))
|
|
||||||
|
|
||||||
if isEggProductFlag(flagType) {
|
|
||||||
return PLSalesTypeEgg
|
|
||||||
}
|
|
||||||
if isChickenProductFlag(flagType) {
|
|
||||||
return PLSalesTypeChicken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PLSalesTypeChicken
|
|
||||||
}
|
|
||||||
|
|
||||||
func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct {
|
|
||||||
categorized := make(map[string][]entities.MarketingDeliveryProduct)
|
|
||||||
|
|
||||||
for _, delivery := range deliveries {
|
|
||||||
product := delivery.MarketingProduct.ProductWarehouse.Product
|
|
||||||
salesType := getSalesTypeFromProductFlags(&product)
|
|
||||||
|
|
||||||
categorized[salesType] = append(categorized[salesType], delivery)
|
|
||||||
}
|
|
||||||
|
|
||||||
return categorized
|
|
||||||
}
|
|
||||||
|
|
||||||
func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 {
|
|
||||||
amount := 0.0
|
|
||||||
for _, delivery := range deliveries {
|
|
||||||
amount += delivery.TotalPrice
|
|
||||||
}
|
|
||||||
return amount
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
|
||||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
|
||||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
|
||||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// === Response DTO ===
|
|
||||||
type SalesDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
RealizationDate time.Time `json:"realization_date"`
|
|
||||||
Age int `json:"age"`
|
|
||||||
DoNumber string `json:"do_number"`
|
|
||||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
|
||||||
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
|
||||||
Qty float64 `json:"qty"`
|
|
||||||
Weight float64 `json:"weight"`
|
|
||||||
AvgWeight float64 `json:"avg_weight"`
|
|
||||||
Price float64 `json:"price"`
|
|
||||||
TotalPrice float64 `json:"total_price"`
|
|
||||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
|
||||||
PaymentStatus string `json:"payment_status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PenjualanRealisasiResponseDTO struct {
|
|
||||||
Sales []SalesDTO `json:"sales"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions ===
|
|
||||||
|
|
||||||
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
|
||||||
|
|
||||||
age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate)
|
|
||||||
|
|
||||||
var product *productDTO.ProductRelationDTO
|
|
||||||
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
|
||||||
mapped := productDTO.ToProductRelationDTO(e.MarketingProduct.ProductWarehouse.Product)
|
|
||||||
product = &mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
var customer *customerDTO.CustomerRelationDTO
|
|
||||||
if e.MarketingProduct.Marketing.Customer.Id != 0 {
|
|
||||||
mapped := customerDTO.ToCustomerRelationDTO(e.MarketingProduct.Marketing.Customer)
|
|
||||||
customer = &mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
var kandang *kandangDTO.KandangRelationDTO
|
|
||||||
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang.Id != 0 {
|
|
||||||
mapped := kandangDTO.ToKandangRelationDTO(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang)
|
|
||||||
kandang = &mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
|
|
||||||
|
|
||||||
return SalesDTO{
|
|
||||||
Id: e.Id,
|
|
||||||
RealizationDate: *e.DeliveryDate,
|
|
||||||
Age: age,
|
|
||||||
DoNumber: doNumber,
|
|
||||||
Product: product,
|
|
||||||
Customer: customer,
|
|
||||||
Qty: e.UsageQty, // Show allocated quantity from FIFO
|
|
||||||
Weight: e.TotalWeight,
|
|
||||||
AvgWeight: e.AvgWeight,
|
|
||||||
Price: e.UnitPrice,
|
|
||||||
TotalPrice: e.TotalPrice,
|
|
||||||
Kandang: kandang,
|
|
||||||
PaymentStatus: "Paid",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
|
||||||
result := make([]SalesDTO, len(e))
|
|
||||||
for i, r := range e {
|
|
||||||
result[i] = ToSalesDTO(r)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
|
||||||
|
|
||||||
return PenjualanRealisasiResponseDTO{
|
|
||||||
|
|
||||||
Sales: ToSalesDTOs(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int {
|
|
||||||
if len(realisasi) > 0 {
|
|
||||||
for _, item := range realisasi {
|
|
||||||
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
|
||||||
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
|
|
||||||
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
|
|
||||||
for _, chickin := range projectFlockKandang.Chickins {
|
|
||||||
if chickin.ChickInDate.Before(earliestChickinDate) {
|
|
||||||
earliestChickinDate = chickin.ChickInDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
|
|
||||||
ageInWeeks := ageInDays / 7
|
|
||||||
return ageInWeeks
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
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, totalActualPopulation 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, totalActualPopulation)
|
|
||||||
|
|
||||||
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, totalActualPopulation),
|
|
||||||
},
|
|
||||||
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, totalActualPopulation float64) float64 {
|
|
||||||
if totalActualPopulation > 0 {
|
|
||||||
return totalPrice / totalActualPopulation
|
|
||||||
}
|
|
||||||
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 ""
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SapronakDetailDTO struct {
|
|
||||||
ProductID uint `json:"product_id"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
Flag string `json:"flag"`
|
|
||||||
Tanggal *time.Time `json:"tanggal,omitempty"`
|
|
||||||
NoReferensi string `json:"no_referensi,omitempty"`
|
|
||||||
JenisTransaksi string `json:"jenis_transaksi,omitempty"`
|
|
||||||
QtyMasuk float64 `json:"qty_masuk"`
|
|
||||||
QtyKeluar float64 `json:"qty_keluar"`
|
|
||||||
Harga float64 `json:"harga"`
|
|
||||||
Nilai float64 `json:"nilai"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SapronakGroupDTO struct {
|
|
||||||
Flag string `json:"flag"`
|
|
||||||
Items []SapronakDetailDTO `json:"items"`
|
|
||||||
TotalMasuk float64 `json:"total_masuk"`
|
|
||||||
TotalKeluar float64 `json:"total_keluar"`
|
|
||||||
SaldoAkhir float64 `json:"saldo_akhir"`
|
|
||||||
TotalNilai float64 `json:"total_nilai"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SapronakItemDTO struct {
|
|
||||||
ProductID uint `json:"product_id"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
Flag string `json:"flag"`
|
|
||||||
IncomingQty float64 `json:"incoming_qty"`
|
|
||||||
IncomingValue float64 `json:"incoming_value"`
|
|
||||||
UsageQty float64 `json:"usage_qty"`
|
|
||||||
UsageValue float64 `json:"usage_value"`
|
|
||||||
RemainingQty float64 `json:"remaining_qty"`
|
|
||||||
AveragePrice float64 `json:"average_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SapronakReportDTO struct {
|
|
||||||
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
|
||||||
ProjectFlockID uint `json:"project_flock_id"`
|
|
||||||
ProjectName string `json:"project_name"`
|
|
||||||
KandangID uint `json:"kandang_id"`
|
|
||||||
KandangName string `json:"kandang_name"`
|
|
||||||
Period int `json:"period"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
StartDate *time.Time `json:"start_date,omitempty"`
|
|
||||||
EndDate *time.Time `json:"end_date,omitempty"`
|
|
||||||
TotalIncomingValue float64 `json:"total_incoming_value"`
|
|
||||||
TotalUsageValue float64 `json:"total_usage_value"`
|
|
||||||
Items []SapronakItemDTO `json:"items"`
|
|
||||||
Groups []SapronakGroupDTO `json:"groups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplified view for project-level sapronak response
|
|
||||||
type SapronakCategoryRowDTO struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Date string `json:"date"`
|
|
||||||
ReferenceNumber string `json:"reference_number"`
|
|
||||||
QtyIn float64 `json:"qty_in"`
|
|
||||||
QtyOut float64 `json:"qty_out"`
|
|
||||||
QtyUsed float64 `json:"qty_used"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
ProductCategory string `json:"product_category"`
|
|
||||||
UnitPrice float64 `json:"unit_price"`
|
|
||||||
TotalAmount float64 `json:"total_amount"`
|
|
||||||
Notes string `json:"notes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SapronakCategoryTotalDTO struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
QtyIn float64 `json:"qty_in"`
|
|
||||||
QtyOut float64 `json:"qty_out"`
|
|
||||||
QtyUsed float64 `json:"qty_used"`
|
|
||||||
AvgUnitPrice float64 `json:"avg_unit_price"`
|
|
||||||
TotalAmount float64 `json:"total_amount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SapronakCategoryDTO struct {
|
|
||||||
Rows []SapronakCategoryRowDTO `json:"rows"`
|
|
||||||
Total SapronakCategoryTotalDTO `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SapronakProjectAggregatedDTO struct {
|
|
||||||
Doc *SapronakCategoryDTO `json:"doc,omitempty"`
|
|
||||||
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
|
|
||||||
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
|
|
||||||
Pullet *SapronakCategoryDTO `json:"pullet,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingSapronakItemDTO struct {
|
|
||||||
Id uint64 `json:"id"`
|
|
||||||
Date string `json:"date"`
|
|
||||||
ReferenceNumber string `json:"reference_number"`
|
|
||||||
TransactionType string `json:"transaction_type"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
ProductCategory string `json:"product_category"`
|
|
||||||
ProductSubCategory string `json:"product_sub_category"`
|
|
||||||
SourceWarehouse string `json:"source_warehouse"`
|
|
||||||
DestinationWarehouse string `json:"destination_warehouse,omitempty"`
|
|
||||||
// Destination string `json:"destination,omitempty"`
|
|
||||||
Quantity float64 `json:"quantity"`
|
|
||||||
Unit string `json:"unit"`
|
|
||||||
FormattedQuantity string `json:"formatted_quantity"`
|
|
||||||
Notes string `json:"notes"`
|
|
||||||
SortDate time.Time `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClosingSapronakDTO struct {
|
|
||||||
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
|
|
||||||
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions for Aggregated Sapronak Response ===
|
|
||||||
|
|
||||||
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
|
||||||
result := SapronakProjectAggregatedDTO{}
|
|
||||||
|
|
||||||
if len(reports) == 0 {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
rep := reports[0]
|
|
||||||
return ToSapronakProjectAggregatedFromReport(&rep, flag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
|
||||||
result := SapronakProjectAggregatedDTO{}
|
|
||||||
|
|
||||||
if report == nil {
|
|
||||||
report = &SapronakReportDTO{}
|
|
||||||
}
|
|
||||||
|
|
||||||
filter := strings.ToUpper(strings.TrimSpace(flag))
|
|
||||||
|
|
||||||
byFlag := map[string]**SapronakCategoryDTO{}
|
|
||||||
if filter == "" || filter == "DOC" {
|
|
||||||
result.Doc = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
|
||||||
byFlag["DOC"] = &result.Doc
|
|
||||||
}
|
|
||||||
if filter == "" || filter == "OVK" {
|
|
||||||
result.Ovk = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
|
||||||
byFlag["OVK"] = &result.Ovk
|
|
||||||
}
|
|
||||||
if filter == "" || filter == "PAKAN" {
|
|
||||||
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
|
||||||
byFlag["PAKAN"] = &result.Pakan
|
|
||||||
}
|
|
||||||
if filter == "" || filter == "PULLET" {
|
|
||||||
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
|
||||||
byFlag["PULLET"] = &result.Pullet
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDate := func(t *time.Time) string {
|
|
||||||
if t == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return t.Format("02-Jan-2006")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, group := range report.Groups {
|
|
||||||
flagKey := strings.ToUpper(group.Flag)
|
|
||||||
ptr := byFlag[flagKey]
|
|
||||||
if ptr == nil || *ptr == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
target := *ptr
|
|
||||||
|
|
||||||
rowIndexByProduct := make(map[string]int)
|
|
||||||
|
|
||||||
getOrCreateRow := func(productKey string, base SapronakCategoryRowDTO) *SapronakCategoryRowDTO {
|
|
||||||
if idx, ok := rowIndexByProduct[productKey]; ok {
|
|
||||||
return &target.Rows[idx]
|
|
||||||
}
|
|
||||||
target.Rows = append(target.Rows, base)
|
|
||||||
idx := len(target.Rows) - 1
|
|
||||||
rowIndexByProduct[productKey] = idx
|
|
||||||
return &target.Rows[idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
for idx, item := range group.Items {
|
|
||||||
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
|
|
||||||
baseRow := SapronakCategoryRowDTO{
|
|
||||||
ID: idx + 1,
|
|
||||||
Date: formatDate(item.Tanggal),
|
|
||||||
ReferenceNumber: item.NoReferensi,
|
|
||||||
Description: item.ProductName,
|
|
||||||
ProductCategory: item.ProductName,
|
|
||||||
UnitPrice: item.Harga,
|
|
||||||
Notes: "-",
|
|
||||||
}
|
|
||||||
|
|
||||||
row := getOrCreateRow(productKey, baseRow)
|
|
||||||
|
|
||||||
switch strings.ToLower(item.JenisTransaksi) {
|
|
||||||
case "pembelian", "adjustment masuk", "mutasi masuk":
|
|
||||||
row.QtyIn += item.QtyMasuk
|
|
||||||
row.TotalAmount += item.Nilai
|
|
||||||
case "pemakaian", "adjustment keluar":
|
|
||||||
row.QtyUsed += item.QtyKeluar
|
|
||||||
case "mutasi keluar":
|
|
||||||
row.QtyOut += item.QtyKeluar
|
|
||||||
default:
|
|
||||||
row.QtyIn += item.QtyMasuk
|
|
||||||
row.TotalAmount += item.Nilai
|
|
||||||
}
|
|
||||||
|
|
||||||
if row.QtyIn > 0 {
|
|
||||||
row.UnitPrice = row.TotalAmount / row.QtyIn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range target.Rows {
|
|
||||||
target.Rows[i].ID = i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTotals := func(cat *SapronakCategoryDTO, label string) {
|
|
||||||
if cat == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var qtyIn, qtyOut, qtyUsed, total float64
|
|
||||||
for _, r := range cat.Rows {
|
|
||||||
qtyIn += r.QtyIn
|
|
||||||
qtyOut += r.QtyOut
|
|
||||||
qtyUsed += r.QtyUsed
|
|
||||||
total += r.TotalAmount
|
|
||||||
}
|
|
||||||
avg := 0.0
|
|
||||||
if qtyIn > 0 {
|
|
||||||
avg = total / qtyIn
|
|
||||||
}
|
|
||||||
cat.Total = SapronakCategoryTotalDTO{
|
|
||||||
Label: label,
|
|
||||||
QtyIn: qtyIn,
|
|
||||||
QtyOut: qtyOut,
|
|
||||||
QtyUsed: qtyUsed,
|
|
||||||
AvgUnitPrice: avg,
|
|
||||||
TotalAmount: total,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTotals(result.Doc, "TOTAL DOC")
|
|
||||||
buildTotals(result.Ovk, "TOTAL OVK")
|
|
||||||
buildTotals(result.Pakan, "TOTAL PAKAN")
|
|
||||||
buildTotals(result.Pullet, "TOTAL PULLET")
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -5,16 +5,9 @@ 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"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
|
||||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/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"
|
||||||
@@ -24,22 +17,11 @@ 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)
|
|
||||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
|
||||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||||
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
marketingRepo := rMarketings.NewMarketingRepository(db)
|
|
||||||
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
|
||||||
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
|
|
||||||
chickinRepo := rChickin.NewChickinRepository(db)
|
|
||||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
|
||||||
purchaseRepo := rPurchase.NewPurchaseRepository(db)
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
|
closingService := sClosing.NewClosingService(closingRepo, projectFlockKandangRepo, validate)
|
||||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ClosingRoutes(router, userService, closingService, sapronakService)
|
ClosingRoutes(router, userService, closingService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,13 @@
|
|||||||
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)
|
|
||||||
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
|
|
||||||
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
|
||||||
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
|
|
||||||
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
|
|
||||||
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
|
|
||||||
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
|
|
||||||
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, 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 {
|
||||||
@@ -42,765 +19,3 @@ 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 ExpeditionHPPRow struct {
|
|
||||||
SupplierName string `gorm:"column:supplier_name"`
|
|
||||||
TotalAmount float64 `gorm:"column:total_amount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
|
||||||
return 0, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var purchaseAgg struct {
|
|
||||||
TotalIn float64 `gorm:"column:total_in"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("purchase_items pi").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
|
|
||||||
Where("f.name = ?", "PAKAN").
|
|
||||||
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
|
|
||||||
Scan(&purchaseAgg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var usageAgg struct {
|
|
||||||
TotalUsed float64 `gorm:"column:total_used"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.DB().WithContext(ctx).
|
|
||||||
Table("recording_stocks rs").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
|
||||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Where("f.name = ?", "PAKAN").
|
|
||||||
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
|
|
||||||
Scan(&usageAgg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var agg struct {
|
|
||||||
Total float64 `gorm:"column:total_culling"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("recording_depletions rd").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
|
|
||||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Where("f.name = ?", utils.FlagAyamCulling).
|
|
||||||
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
|
|
||||||
Scan(&agg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return agg.Total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
|
||||||
return 0, 0, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var agg struct {
|
|
||||||
TotalWeight float64 `gorm:"column:total_weight"`
|
|
||||||
TotalQty float64 `gorm:"column:total_qty"`
|
|
||||||
TotalPrice float64 `gorm:"column:total_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("marketing_products mp").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
|
||||||
Scan(&agg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
|
||||||
return 0, 0, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var agg struct {
|
|
||||||
TotalWeight float64 `gorm:"column:total_weight"`
|
|
||||||
TotalQty float64 `gorm:"column:total_qty"`
|
|
||||||
TotalPrice float64 `gorm:"column:total_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("marketing_products mp").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
|
||||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Where("f.name IN ?", flagNames).
|
|
||||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
|
||||||
Scan(&agg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var agg struct {
|
|
||||||
TotalQty float64 `gorm:"column:total_qty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("recording_eggs re").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
|
|
||||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Where("f.name IN ?", flagNames).
|
|
||||||
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
|
|
||||||
Scan(&agg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return agg.TotalQty, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
|
|
||||||
if fcrID == 0 {
|
|
||||||
return []entity.FcrStandard{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var standards []entity.FcrStandard
|
|
||||||
if err := r.DB().WithContext(ctx).
|
|
||||||
Where("fcr_id = ?", fcrID).
|
|
||||||
Order("weight ASC").
|
|
||||||
Find(&standards).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return standards, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) {
|
|
||||||
db := r.DB().WithContext(ctx)
|
|
||||||
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil, fmt.Errorf("invalid project flock id")
|
|
||||||
}
|
|
||||||
|
|
||||||
query := db.
|
|
||||||
Table("expense_realizations AS er").
|
|
||||||
Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id").
|
|
||||||
Joins("JOIN expenses e ON e.id = ens.expense_id").
|
|
||||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id").
|
|
||||||
Joins("JOIN nonstocks n ON n.id = ens.nonstock_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
|
||||||
Joins("JOIN suppliers s ON s.id = e.supplier_id").
|
|
||||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
|
||||||
Where("e.category = ?", "BOP").
|
|
||||||
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
|
|
||||||
|
|
||||||
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
|
|
||||||
query = query.Where("pfk.id = ?", *projectFlockKandangID)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []ExpeditionHPPRow
|
|
||||||
err := query.
|
|
||||||
Select(
|
|
||||||
"e.supplier_id AS supplier_id, " +
|
|
||||||
"s.name AS supplier_name, " +
|
|
||||||
"SUM(er.qty * er.price) AS total_amount",
|
|
||||||
).
|
|
||||||
Group("e.supplier_id, s.name").
|
|
||||||
Scan(&rows).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows, 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, string(utils.StockLogTypeAdjustment), 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, string(utils.StockLogTypeTransfer), 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package closings
|
package closings
|
||||||
|
|
||||||
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/closings/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/controllers"
|
||||||
closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -9,11 +9,10 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
|
||||||
ctrl := controller.NewClosingController(s, sapronakSvc)
|
ctrl := controller.NewClosingController(s)
|
||||||
|
|
||||||
route := v1.Group("/closings")
|
route := v1.Group("/closings")
|
||||||
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)
|
||||||
@@ -21,15 +20,7 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
|
route.Get("/sapronak/report", ctrl.GetSapronakReport)
|
||||||
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
|
||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
|
|
||||||
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
|
|
||||||
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
|
|
||||||
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
|
|
||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
|
|
||||||
route.Get("/:projectFlockId/data-produksi", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
|
|
||||||
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,681 +0,0 @@
|
|||||||
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,14 +13,3 @@ 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"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type CountSapronakQuery struct {
|
type SapronakQuery struct {
|
||||||
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
|
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
|
||||||
KandangID uint `query:"kandang_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"`
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ func ConstantRoutes(v1 fiber.Router, s constant.ConstantService) {
|
|||||||
ctrl := controller.NewConstantController(s)
|
ctrl := controller.NewConstantController(s)
|
||||||
|
|
||||||
route := v1.Group("/constants")
|
route := v1.Group("/constants")
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,16 +151,12 @@ 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")
|
||||||
if categoryVal != "" {
|
req.Category = &categoryVal
|
||||||
req.Category = &categoryVal
|
|
||||||
}
|
|
||||||
|
|
||||||
supplierIDVal := c.FormValue("supplier_id")
|
supplierIDVal := c.FormValue("supplier_id")
|
||||||
if supplierIDVal != "" {
|
if supplierIDVal != "" {
|
||||||
@@ -316,18 +312,13 @@ func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
req.Documents = form.File["documents"]
|
req.Documents = form.File["documents"]
|
||||||
|
|
||||||
realizationDate := c.FormValue("realization_date")
|
req.RealizationDate = c.FormValue("realization_date")
|
||||||
if realizationDate != "" {
|
|
||||||
req.RealizationDate = &realizationDate
|
|
||||||
}
|
|
||||||
|
|
||||||
realizationsJSON := c.FormValue("realizations")
|
realizationsJSON := c.FormValue("realizations")
|
||||||
if realizationsJSON != "" {
|
if realizationsJSON != "" {
|
||||||
var realizations []validation.RealizationItem
|
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
|
||||||
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)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -40,8 +41,8 @@ type ExpenseListDTO struct {
|
|||||||
|
|
||||||
type ExpenseDetailDTO struct {
|
type ExpenseDetailDTO struct {
|
||||||
ExpenseBaseDTO
|
ExpenseBaseDTO
|
||||||
Documents []DocumentDTO `json:"documents"`
|
Documents []DocumentDTO `json:"documents,omitempty"`
|
||||||
RealizationDocs []DocumentDTO `json:"realization_docs"`
|
RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"`
|
||||||
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
|
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
|
||||||
TotalPengajuan float64 `json:"total_pengajuan"`
|
TotalPengajuan float64 `json:"total_pengajuan"`
|
||||||
TotalRealisasi float64 `json:"total_realisasi"`
|
TotalRealisasi float64 `json:"total_realisasi"`
|
||||||
@@ -178,20 +179,12 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
|||||||
var pengajuans []ExpenseNonstockDTO
|
var pengajuans []ExpenseNonstockDTO
|
||||||
var realisasi []ExpenseRealizationDTO
|
var realisasi []ExpenseRealizationDTO
|
||||||
|
|
||||||
// Map documents from Document service
|
if e.DocumentPath.Valid && e.DocumentPath.String != "" {
|
||||||
for _, doc := range e.Documents {
|
json.Unmarshal([]byte(e.DocumentPath.String), &documents)
|
||||||
documents = append(documents, DocumentDTO{
|
|
||||||
ID: uint64(doc.Id),
|
|
||||||
Path: doc.Path,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map realization documents from Document service
|
if e.RealizationDocumentPath.Valid && e.RealizationDocumentPath.String != "" {
|
||||||
for _, doc := range e.RealizationDocuments {
|
json.Unmarshal([]byte(e.RealizationDocumentPath.String), &realizationDocs)
|
||||||
realizationDocs = append(realizationDocs, DocumentDTO{
|
|
||||||
ID: uint64(doc.Id),
|
|
||||||
Path: doc.Path,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(e.Nonstocks) > 0 {
|
if len(e.Nonstocks) > 0 {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package expenses
|
package expenses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -12,6 +11,7 @@ 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"
|
||||||
|
|
||||||
@@ -32,20 +32,15 @@ func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
|
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
|
||||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
documentRepo := commonRepo.NewDocumentRepository(db)
|
|
||||||
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to create document service: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register workflow steps for EXPENSES approval
|
// Register workflow steps for EXPENSES approval
|
||||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, documentSvc, validate)
|
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ExpenseRoutes(router, userService, expenseService)
|
ExpenseRoutes(router, userService, expenseService)
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,8 +13,6 @@ 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 {
|
||||||
@@ -53,57 +49,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
|
|
||||||
"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/repports/validations"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,8 +12,6 @@ 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)
|
|
||||||
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRealizationRepositoryImpl struct {
|
type ExpenseRealizationRepositoryImpl struct {
|
||||||
@@ -34,104 +30,11 @@ 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).Where("expense_nonstock_id = ?", expenseNonstockID).First(&realization).Error
|
|
||||||
return &realization, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error) {
|
|
||||||
var realizations []entity.ExpenseRealization
|
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
Preload("ExpenseNonstock").
|
Where("expense_nonstock_id = ?", expenseNonstockID).
|
||||||
Preload("ExpenseNonstock.Nonstock").
|
First(&realization).Error
|
||||||
Preload("ExpenseNonstock.Nonstock.Uom").
|
if err != nil {
|
||||||
Preload("ExpenseNonstock.Nonstock.Flags").
|
return nil, err
|
||||||
Preload("ExpenseNonstock.Expense").
|
}
|
||||||
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
return &realization, nil
|
||||||
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
|
||||||
Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
|
|
||||||
Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id").
|
|
||||||
Where("project_flock_kandangs.project_flock_id = ? OR kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?)", projectFlockID, projectFlockID).
|
|
||||||
Find(&realizations).Error
|
|
||||||
return realizations, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
|
|
||||||
var realizations []entity.ExpenseRealization
|
|
||||||
var total int64
|
|
||||||
|
|
||||||
db := r.DB().WithContext(ctx).
|
|
||||||
Model(&entity.ExpenseRealization{}).
|
|
||||||
Preload("ExpenseNonstock", func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.
|
|
||||||
Preload("Expense").
|
|
||||||
Preload("Expense.Supplier").
|
|
||||||
Preload("Kandang").
|
|
||||||
Preload("Kandang.Location").
|
|
||||||
Preload("Nonstock").
|
|
||||||
Preload("Nonstock.Flags")
|
|
||||||
}).
|
|
||||||
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
|
||||||
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
|
||||||
Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id")
|
|
||||||
|
|
||||||
if filters.Search != "" {
|
|
||||||
db = db.Where("expenses.category LIKE ? OR expenses.reference_number LIKE ? OR expenses.po_number LIKE ? OR expenses.notes LIKE ? OR suppliers.name LIKE ?",
|
|
||||||
"%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%")
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters.Category != "" {
|
|
||||||
db = db.Where("expenses.category = ?", filters.Category)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters.SupplierId > 0 {
|
|
||||||
db = db.Where("expenses.supplier_id = ?", filters.SupplierId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters.KandangId > 0 {
|
|
||||||
db = db.Where("expense_nonstocks.kandang_id = ?", filters.KandangId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters.ProjectFlockKandangId > 0 {
|
|
||||||
db = db.Where("expense_nonstocks.project_flock_kandang_id = ?", filters.ProjectFlockKandangId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters.NonstockId > 0 {
|
|
||||||
db = db.Where("expense_nonstocks.nonstock_id = ?", filters.NonstockId)
|
|
||||||
}
|
|
||||||
|
|
||||||
locationID := filters.LocationId
|
|
||||||
areaID := filters.AreaId
|
|
||||||
|
|
||||||
if locationID > 0 || areaID > 0 {
|
|
||||||
db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id")
|
|
||||||
|
|
||||||
if locationID > 0 {
|
|
||||||
db = db.Where("kandangs.location_id = ?", uint(locationID))
|
|
||||||
}
|
|
||||||
|
|
||||||
if areaID > 0 {
|
|
||||||
db = db.Joins("JOIN locations ON locations.id = kandangs.location_id").
|
|
||||||
Where("locations.area_id = ?", uint(areaID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters.RealizationDate != "" {
|
|
||||||
if realizationDate, err := utils.ParseDateString(filters.RealizationDate); err == nil {
|
|
||||||
db = db.Where("DATE(expenses.realization_date) = ?", realizationDate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Count(&total).Error; err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.
|
|
||||||
Offset(offset).
|
|
||||||
Limit(limit).
|
|
||||||
Order("expense_realizations.created_at DESC").
|
|
||||||
Find(&realizations).Error; err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return realizations, total, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,24 +14,22 @@ 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)
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
route.Get("/",m.RequirePermissions(m.P_ExpenseGetAll), ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/",m.RequirePermissions(m.P_ExpenseCreateOne), ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
route.Get("/:id",m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Patch("/:id",m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne)
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
route.Delete("/:id",m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne)
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
route.Post("/approvals/manager",m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval)
|
route.Post("/approvals/manager", ctrl.Approval)
|
||||||
route.Post("/approvals/finance",m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval)
|
route.Post("/approvals/finance", ctrl.Approval)
|
||||||
route.Post("/:id/realizations",m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization)
|
route.Post("/:id/realizations", ctrl.CreateRealization)
|
||||||
route.Patch("/:id/realizations",m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
|
route.Patch("/:id/realizations", ctrl.UpdateRealization)
|
||||||
route.Post("/:id/complete",m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense)
|
route.Post("/:id/complete", ctrl.CompleteExpense)
|
||||||
route.Delete("/:id/documents/:documentId",m.RequirePermissions(m.P_ExpenseDocument), ctrl.DeleteDocument)
|
route.Delete("/:id/documents/:documentId", ctrl.DeleteDocument)
|
||||||
route.Delete("/:id/realization-documents/:documentId",m.RequirePermissions(m.P_ExpenseDocumentRealizations), ctrl.DeleteRealizationDocument)
|
route.Delete("/:id/realization-documents/:documentId", ctrl.DeleteRealizationDocument)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
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"
|
||||||
@@ -46,10 +48,9 @@ type expenseService struct {
|
|||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
RealizationRepository repository.ExpenseRealizationRepository
|
RealizationRepository repository.ExpenseRealizationRepository
|
||||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
DocumentSvc commonSvc.DocumentService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, validate *validator.Validate) ExpenseService {
|
func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) ExpenseService {
|
||||||
return &expenseService{
|
return &expenseService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -59,7 +60,6 @@ func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierR
|
|||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
RealizationRepository: realizationRepo,
|
RealizationRepository: realizationRepo,
|
||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
DocumentSvc: documentSvc,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +71,7 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Nonstocks.Realization").
|
Preload("Nonstocks.Realization").
|
||||||
Preload("Nonstocks.ProjectFlockKandang.Kandang.Location").
|
Preload("Nonstocks.ProjectFlockKandang.Kandang.Location").
|
||||||
Preload("Nonstocks.Kandang").
|
Preload("Nonstocks.Kandang").
|
||||||
Preload("Nonstocks.Kandang.Location").
|
Preload("Nonstocks.Kandang.Location")
|
||||||
Preload("Documents", func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Where("documentable_type = ?", string(utils.DocumentableTypeExpense))
|
|
||||||
}).
|
|
||||||
Preload("RealizationDocuments", func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Where("documentable_type = ?", string(utils.DocumentableTypeExpenseRealization))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) {
|
func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) {
|
||||||
@@ -189,16 +183,12 @@ 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 := GenerateExpenseReferenceNumber(c.Context(), expenseRepoTx)
|
referenceNumber, err := s.generateReferenceNumber(dbTransaction)
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID, err := middleware.ActorIDFromContext(c)
|
createdBy := uint64(1) //todo get from auth
|
||||||
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,
|
||||||
@@ -218,7 +208,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
|
|
||||||
var projectFlockKandangId *uint64
|
var projectFlockKandangId *uint64
|
||||||
|
|
||||||
if req.Category == string(utils.ExpenseCategoryBOP) {
|
if req.Category == "BOP" {
|
||||||
|
|
||||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -235,10 +225,10 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
|
|
||||||
nonstockId := costItem.NonstockID
|
nonstockId := costItem.NonstockID
|
||||||
var kandangId *uint64
|
var kandangId *uint64
|
||||||
if req.Category == string(utils.ExpenseCategoryNonBOP) {
|
if req.Category == "NON-BOP" {
|
||||||
id := uint64(expenseNonstock.KandangID)
|
id := uint64(expenseNonstock.KandangID)
|
||||||
kandangId = &id
|
kandangId = &id
|
||||||
} else if req.Category == string(utils.ExpenseCategoryBOP) {
|
} else if req.Category == "BOP" {
|
||||||
if projectFlockKandangId != nil {
|
if projectFlockKandangId != nil {
|
||||||
kandangId = &expenseNonstock.KandangID
|
kandangId = &expenseNonstock.KandangID
|
||||||
}
|
}
|
||||||
@@ -274,23 +264,9 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
if len(req.Documents) > 0 {
|
||||||
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
if err := s.processDocuments(c, expenseRepoTx, uint(expense.Id), req.Documents, false); err != nil {
|
||||||
for idx, file := range req.Documents {
|
return err
|
||||||
documentFiles = append(documentFiles, commonSvc.DocumentFile{
|
|
||||||
File: file,
|
|
||||||
Type: string(utils.DocumentTypeExpense),
|
|
||||||
Index: &idx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
|
||||||
DocumentableType: string(utils.DocumentableTypeExpense),
|
|
||||||
DocumentableID: expense.Id,
|
|
||||||
CreatedBy: &createdByUint,
|
|
||||||
Files: documentFiles,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense documents")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,9 +360,6 @@ 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,7 +377,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if categoryChanged {
|
if categoryChanged {
|
||||||
if currentExpense.Category == string(utils.ExpenseCategoryBOP) && newCategory == string(utils.ExpenseCategoryNonBOP) {
|
if currentExpense.Category == "BOP" && newCategory == "NON-BOP" {
|
||||||
|
|
||||||
var existingExpenseNonstocks []entity.ExpenseNonstock
|
var existingExpenseNonstocks []entity.ExpenseNonstock
|
||||||
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
||||||
@@ -419,7 +392,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if currentExpense.Category == string(utils.ExpenseCategoryNonBOP) && newCategory == string(utils.ExpenseCategoryBOP) {
|
} else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" {
|
||||||
|
|
||||||
var existingExpenseNonstocks []entity.ExpenseNonstock
|
var existingExpenseNonstocks []entity.ExpenseNonstock
|
||||||
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
||||||
@@ -431,9 +404,6 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -476,7 +446,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
for _, expenseNonstock := range *req.ExpenseNonstocks {
|
for _, expenseNonstock := range *req.ExpenseNonstocks {
|
||||||
var projectFlockKandangId *uint64
|
var projectFlockKandangId *uint64
|
||||||
|
|
||||||
if updatedExpense.Category == string(utils.ExpenseCategoryBOP) {
|
if updatedExpense.Category == "BOP" {
|
||||||
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
|
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
|
||||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -499,10 +469,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var kandangId *uint64
|
var kandangId *uint64
|
||||||
if updatedExpense.Category == string(utils.ExpenseCategoryNonBOP) {
|
if updatedExpense.Category == "NON-BOP" {
|
||||||
id := uint64(expenseNonstock.KandangID)
|
id := uint64(expenseNonstock.KandangID)
|
||||||
kandangId = &id
|
kandangId = &id
|
||||||
} else if updatedExpense.Category == string(utils.ExpenseCategoryBOP) {
|
} else if updatedExpense.Category == "BOP" {
|
||||||
if projectFlockKandangId != nil {
|
if projectFlockKandangId != nil {
|
||||||
kandangId = &expenseNonstock.KandangID
|
kandangId = &expenseNonstock.KandangID
|
||||||
}
|
}
|
||||||
@@ -526,10 +496,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID, err := middleware.ActorIDFromContext(c)
|
actorID := uint(1) // TODO: replace with authenticated user id
|
||||||
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
|
||||||
@@ -546,23 +513,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
if len(req.Documents) > 0 {
|
||||||
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
if err := s.processDocuments(c, expenseRepoTx, id, req.Documents, false); err != nil {
|
||||||
for idx, file := range req.Documents {
|
return err
|
||||||
documentFiles = append(documentFiles, commonSvc.DocumentFile{
|
|
||||||
File: file,
|
|
||||||
Type: string(utils.DocumentTypeExpense),
|
|
||||||
Index: &idx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
|
||||||
DocumentableType: string(utils.DocumentableTypeExpense),
|
|
||||||
DocumentableID: uint64(id),
|
|
||||||
CreatedBy: &actorID,
|
|
||||||
Files: documentFiles,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense documents")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,21 +543,7 @@ 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)
|
||||||
@@ -633,20 +572,6 @@ 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))
|
||||||
@@ -691,24 +616,9 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
if len(req.Documents) > 0 {
|
||||||
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
||||||
for idx, file := range req.Documents {
|
return err
|
||||||
documentFiles = append(documentFiles, commonSvc.DocumentFile{
|
|
||||||
File: file,
|
|
||||||
Type: string(utils.DocumentTypeExpenseRealization),
|
|
||||||
Index: &idx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
|
||||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
|
||||||
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
|
|
||||||
DocumentableID: uint64(expenseID),
|
|
||||||
CreatedBy: &actorID,
|
|
||||||
Files: documentFiles,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense realization documents")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,10 +655,7 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID, err := middleware.ActorIDFromContext(c)
|
actorID := uint(1) // TODO: replace with authenticated user id
|
||||||
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) {
|
||||||
@@ -805,19 +712,7 @@ 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")
|
||||||
@@ -837,10 +732,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 := req.Realizations == nil && len(req.Documents) > 0
|
updateDataOnly := len(req.Realizations) == 0 && len(req.Documents) > 0
|
||||||
|
|
||||||
if req.Realizations != nil {
|
if len(req.Realizations) > 0 {
|
||||||
for _, realizationItem := range *req.Realizations {
|
for _, realizationItem := range req.Realizations {
|
||||||
|
|
||||||
expenseNonstockID := realizationItem.ExpenseNonstockID
|
expenseNonstockID := realizationItem.ExpenseNonstockID
|
||||||
|
|
||||||
@@ -875,30 +770,9 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.RealizationDate != nil {
|
if len(req.Documents) > 0 {
|
||||||
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{"realization_date": *req.RealizationDate}, nil); err != nil {
|
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
return err
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
|
||||||
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
|
||||||
for idx, file := range req.Documents {
|
|
||||||
documentFiles = append(documentFiles, commonSvc.DocumentFile{
|
|
||||||
File: file,
|
|
||||||
Type: string(utils.DocumentTypeExpenseRealization),
|
|
||||||
Index: &idx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
|
||||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
|
||||||
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
|
|
||||||
DocumentableID: uint64(expenseID),
|
|
||||||
CreatedBy: &actorID,
|
|
||||||
Files: documentFiles,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense realization documents")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -933,6 +807,79 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
return responseDTO, nil
|
return responseDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx repository.ExpenseRepository, expenseID uint, documents []*multipart.FileHeader, isRealization bool) error {
|
||||||
|
|
||||||
|
if len(documents) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingDocuments []expenseDto.DocumentDTO
|
||||||
|
var fieldName string
|
||||||
|
|
||||||
|
if isRealization {
|
||||||
|
fieldName = "realization_document_path"
|
||||||
|
} else {
|
||||||
|
fieldName = "document_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document processing")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
var documentField sql.NullString
|
||||||
|
if isRealization {
|
||||||
|
documentField = expense.RealizationDocumentPath
|
||||||
|
} else {
|
||||||
|
documentField = expense.DocumentPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if documentField.Valid && documentField.String != "" {
|
||||||
|
if err := json.Unmarshal([]byte(documentField.String), &existingDocuments); err != nil {
|
||||||
|
existingDocuments = []expenseDto.DocumentDTO{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var startID uint64 = 1
|
||||||
|
if len(existingDocuments) > 0 {
|
||||||
|
|
||||||
|
maxID := uint64(0)
|
||||||
|
for _, doc := range existingDocuments {
|
||||||
|
if doc.ID > maxID {
|
||||||
|
maxID = doc.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startID = maxID + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, doc := range documents {
|
||||||
|
documentPath := doc.Filename
|
||||||
|
|
||||||
|
document := expenseDto.DocumentDTO{
|
||||||
|
ID: startID + uint64(i),
|
||||||
|
Path: documentPath,
|
||||||
|
}
|
||||||
|
existingDocuments = append(existingDocuments, document)
|
||||||
|
}
|
||||||
|
|
||||||
|
documentJSON, err := json.Marshal(existingDocuments)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{
|
||||||
|
fieldName: string(documentJSON),
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
|
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(ctx.Context(),
|
if err := commonSvc.EnsureRelations(ctx.Context(),
|
||||||
@@ -941,40 +888,62 @@ func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, document
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.DocumentSvc == nil {
|
if err := s.Repository.DB().WithContext(ctx.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
}
|
|
||||||
|
|
||||||
// Verify document exists and belongs to the expense
|
expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil)
|
||||||
var documentableType string
|
if err != nil {
|
||||||
if isRealization {
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document deletion")
|
||||||
documentableType = string(utils.DocumentableTypeExpenseRealization)
|
|
||||||
} else {
|
|
||||||
documentableType = string(utils.DocumentableTypeExpense)
|
|
||||||
}
|
|
||||||
|
|
||||||
documents, err := s.DocumentSvc.ListByTarget(ctx.Context(), documentableType, uint64(expenseID))
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve documents")
|
|
||||||
}
|
|
||||||
|
|
||||||
documentFound := false
|
|
||||||
var documentIDsToDelete []uint
|
|
||||||
for _, doc := range documents {
|
|
||||||
if uint64(doc.Id) == documentID {
|
|
||||||
documentFound = true
|
|
||||||
documentIDsToDelete = append(documentIDsToDelete, doc.Id)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !documentFound {
|
var existingDocuments []expenseDto.DocumentDTO
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Document not found")
|
var fieldName string
|
||||||
}
|
|
||||||
|
|
||||||
// Delete document from database and storage
|
if isRealization {
|
||||||
if err := s.DocumentSvc.DeleteDocuments(ctx.Context(), documentIDsToDelete, true); err != nil {
|
fieldName = "realization_document_path"
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete document")
|
if expense.RealizationDocumentPath.Valid && expense.RealizationDocumentPath.String != "" {
|
||||||
|
if err := json.Unmarshal([]byte(expense.RealizationDocumentPath.String), &existingDocuments); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing realization documents")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fieldName = "document_path"
|
||||||
|
if expense.DocumentPath.Valid && expense.DocumentPath.String != "" {
|
||||||
|
if err := json.Unmarshal([]byte(expense.DocumentPath.String), &existingDocuments); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing documents")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedDocuments []expenseDto.DocumentDTO
|
||||||
|
documentFound := false
|
||||||
|
|
||||||
|
for _, doc := range existingDocuments {
|
||||||
|
if doc.ID == documentID {
|
||||||
|
documentFound = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updatedDocuments = append(updatedDocuments, doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !documentFound {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Document not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentJSON, err := json.Marshal(updatedDocuments)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{
|
||||||
|
fieldName: string(documentJSON),
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -985,14 +954,11 @@ 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, err := middleware.ActorIDFromContext(c)
|
actorID := uint(1) // TODO: replace with authenticated user id
|
||||||
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)
|
||||||
@@ -1038,21 +1004,6 @@ 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(),
|
||||||
@@ -1099,6 +1050,17 @@ 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)
|
||||||
@@ -1122,45 +1084,13 @@ func (s *expenseService) validateExpenseNonstockRelation(ctx *fiber.Ctx, expense
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *expenseService) ensureProjectFlockNotClosedForExpense(
|
// func actorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||||
ctx context.Context,
|
// user, ok := authmiddleware.AuthenticatedUser(c)
|
||||||
expense *entity.Expense,
|
// if !ok || user == nil || user.Id == 0 {
|
||||||
) error {
|
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
// Kalau repo belum di-wire atau expense kosong → gak usah ngecek apa-apa
|
// }
|
||||||
if s.ProjectFlockKandangRepo == nil || expense == nil {
|
// return user.Id, nil
|
||||||
return nil
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[uint]struct{})
|
// return user.Id, nil
|
||||||
|
// }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
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:"omitempty,min=1,dive"`
|
Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RealizationItem struct {
|
type RealizationItem struct {
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/dto"
|
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/services"
|
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/validations"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InitialController struct {
|
|
||||||
InitialService service.InitialService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInitialController(initialService service.InitialService) *InitialController {
|
|
||||||
return &InitialController{
|
|
||||||
InitialService: initialService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *InitialController) GetOne(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.InitialService.GetOne(c, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get initial successfully",
|
|
||||||
Data: dto.ToInitialListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *InitialController) CreateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Create)
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.InitialService.CreateOne(c, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusCreated,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Create initial successfully",
|
|
||||||
Data: dto.ToInitialListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *InitialController) UpdateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Update)
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.InitialService.UpdateOne(c, req, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Update initial successfully",
|
|
||||||
Data: dto.ToInitialListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
|
||||||
bankDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
|
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// === DTO Structs ===
|
|
||||||
|
|
||||||
type InitialRelationDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
ReferenceNumber string `json:"reference_number"`
|
|
||||||
TransactionType string `json:"transaction_type"`
|
|
||||||
InitialBalanceType string `json:"initial_balance_type"`
|
|
||||||
InitialBalanceTypeLabel string `json:"initial_balance_type_label"`
|
|
||||||
Party Party `json:"party"`
|
|
||||||
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
|
|
||||||
Direction string `json:"direction"`
|
|
||||||
Nominal float64 `json:"nominal"`
|
|
||||||
Notes string `json:"notes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitialListDTO struct {
|
|
||||||
InitialRelationDTO
|
|
||||||
CreatedBy uint `json:"created_by"`
|
|
||||||
CreatedByUser userDTO.UserRelationDTO `json:"created_by_user,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitialDetailDTO struct {
|
|
||||||
InitialListDTO
|
|
||||||
}
|
|
||||||
|
|
||||||
type Party struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
AccountNumber string `json:"account_number"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions ===
|
|
||||||
|
|
||||||
func ToInitialRelationDTO(e entity.Payment) InitialRelationDTO {
|
|
||||||
reference := ""
|
|
||||||
if e.ReferenceNumber != nil {
|
|
||||||
reference = *e.ReferenceNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
initialBalanceType := initialBalanceTypeFromPayment(e)
|
|
||||||
return InitialRelationDTO{
|
|
||||||
Id: e.Id,
|
|
||||||
ReferenceNumber: reference,
|
|
||||||
TransactionType: transactionTypeLabel(e.TransactionType),
|
|
||||||
InitialBalanceType: initialBalanceType,
|
|
||||||
InitialBalanceTypeLabel: initialBalanceLabel(initialBalanceType),
|
|
||||||
Party: partyFromInitial(e),
|
|
||||||
Bank: bankFromInitial(e),
|
|
||||||
Direction: e.Direction,
|
|
||||||
Nominal: e.Nominal,
|
|
||||||
Notes: e.Notes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToInitialListDTO(e entity.Payment) InitialListDTO {
|
|
||||||
approval := approvalDTO.ApprovalRelationDTO{}
|
|
||||||
if e.LatestApproval != nil {
|
|
||||||
approval = approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
|
||||||
}
|
|
||||||
|
|
||||||
return InitialListDTO{
|
|
||||||
InitialRelationDTO: ToInitialRelationDTO(e),
|
|
||||||
CreatedBy: e.CreatedBy,
|
|
||||||
CreatedByUser: userFromInitial(e),
|
|
||||||
CreatedAt: e.CreatedAt,
|
|
||||||
UpdatedAt: e.UpdatedAt,
|
|
||||||
Approval: approval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToInitialListDTOs(e []entity.Payment) []InitialListDTO {
|
|
||||||
result := make([]InitialListDTO, len(e))
|
|
||||||
for i, r := range e {
|
|
||||||
result[i] = ToInitialListDTO(r)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToInitialDetailDTO(e entity.Payment) InitialDetailDTO {
|
|
||||||
return InitialDetailDTO{
|
|
||||||
InitialListDTO: ToInitialListDTO(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func partyFromInitial(e entity.Payment) Party {
|
|
||||||
party := Party{
|
|
||||||
Id: e.PartyId,
|
|
||||||
Type: e.PartyType,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch utils.PaymentParty(e.PartyType) {
|
|
||||||
case utils.PaymentPartyCustomer:
|
|
||||||
if e.Customer != nil && e.Customer.Id != 0 {
|
|
||||||
party.Name = e.Customer.Name
|
|
||||||
party.AccountNumber = e.Customer.AccountNumber
|
|
||||||
}
|
|
||||||
case utils.PaymentPartySupplier:
|
|
||||||
if e.Supplier != nil && e.Supplier.Id != 0 {
|
|
||||||
party.Name = e.Supplier.Name
|
|
||||||
if e.Supplier.AccountNumber != nil {
|
|
||||||
party.AccountNumber = *e.Supplier.AccountNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return party
|
|
||||||
}
|
|
||||||
|
|
||||||
func bankFromInitial(e entity.Payment) bankDTO.BankRelationDTO {
|
|
||||||
if e.BankWarehouse.Id == 0 {
|
|
||||||
return bankDTO.BankRelationDTO{}
|
|
||||||
}
|
|
||||||
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func userFromInitial(e entity.Payment) userDTO.UserRelationDTO {
|
|
||||||
if e.CreatedUser.Id == 0 {
|
|
||||||
return userDTO.UserRelationDTO{}
|
|
||||||
}
|
|
||||||
return userDTO.ToUserRelationDTO(e.CreatedUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transactionTypeLabel(transactionType string) string {
|
|
||||||
if strings.EqualFold(transactionType, string(utils.TransactionTypeSaldoAwal)) {
|
|
||||||
return "Saldo Awal"
|
|
||||||
}
|
|
||||||
return transactionType
|
|
||||||
}
|
|
||||||
|
|
||||||
func initialBalanceLabel(balanceType string) string {
|
|
||||||
switch strings.ToUpper(strings.TrimSpace(balanceType)) {
|
|
||||||
case "NEGATIVE":
|
|
||||||
return "Saldo Awal Negatif"
|
|
||||||
case "POSITIVE":
|
|
||||||
return "Saldo Awal Positif"
|
|
||||||
default:
|
|
||||||
return balanceType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initialBalanceTypeFromPayment(e entity.Payment) string {
|
|
||||||
if strings.EqualFold(e.Direction, "OUT") || e.Nominal < 0 {
|
|
||||||
return "NEGATIVE"
|
|
||||||
}
|
|
||||||
return "POSITIVE"
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package initials
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
rInitial "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/repositories"
|
|
||||||
sInitial "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/services"
|
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InitialModule struct{}
|
|
||||||
|
|
||||||
func (InitialModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
|
||||||
initialRepo := rInitial.NewInitialRepository(db)
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowInitial, utils.InitialApprovalSteps); err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to register initial approval workflow: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
initialService := sInitial.NewInitialService(initialRepo, approvalService, validate)
|
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
|
||||||
|
|
||||||
InitialRoutes(router, userService, initialService)
|
|
||||||
}
|
|
||||||
@@ -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 InitialRepository interface {
|
|
||||||
repository.BaseRepository[entity.Payment]
|
|
||||||
BankExists(ctx context.Context, bankId uint) (bool, error)
|
|
||||||
CustomerExists(ctx context.Context, customerId uint) (bool, error)
|
|
||||||
SupplierExists(ctx context.Context, supplierId uint) (bool, error)
|
|
||||||
NextPaymentSequence(ctx context.Context) (int64, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitialRepositoryImpl struct {
|
|
||||||
*repository.BaseRepositoryImpl[entity.Payment]
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInitialRepository(db *gorm.DB) InitialRepository {
|
|
||||||
return &InitialRepositoryImpl{
|
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Payment](db),
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *InitialRepositoryImpl) BankExists(ctx context.Context, bankId uint) (bool, error) {
|
|
||||||
return repository.Exists[entity.Bank](ctx, r.db, bankId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *InitialRepositoryImpl) CustomerExists(ctx context.Context, customerId uint) (bool, error) {
|
|
||||||
return repository.Exists[entity.Customer](ctx, r.db, customerId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *InitialRepositoryImpl) SupplierExists(ctx context.Context, supplierId uint) (bool, error) {
|
|
||||||
return repository.Exists[entity.Supplier](ctx, r.db, supplierId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *InitialRepositoryImpl) NextPaymentSequence(ctx context.Context) (int64, error) {
|
|
||||||
var next int64
|
|
||||||
if err := r.db.WithContext(ctx).
|
|
||||||
Raw("SELECT nextval('payments_code_seq')").
|
|
||||||
Scan(&next).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return next, nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package initials
|
|
||||||
|
|
||||||
import (
|
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/controllers"
|
|
||||||
initial "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/services"
|
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitialRoutes(v1 fiber.Router, u user.UserService, s initial.InitialService) {
|
|
||||||
ctrl := controller.NewInitialController(s)
|
|
||||||
|
|
||||||
route := v1.Group("/initial-balances")
|
|
||||||
// route.Use(m.Auth(u))
|
|
||||||
|
|
||||||
route.Post("/", ctrl.CreateOne)
|
|
||||||
route.Get("/:id", ctrl.GetOne)
|
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
|
||||||
}
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/repositories"
|
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/validations"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InitialService interface {
|
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error)
|
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Payment, error)
|
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type initialService struct {
|
|
||||||
Log *logrus.Logger
|
|
||||||
Validate *validator.Validate
|
|
||||||
Repository repository.InitialRepository
|
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
|
||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInitialService(
|
|
||||||
repo repository.InitialRepository,
|
|
||||||
approvalSvc commonSvc.ApprovalService,
|
|
||||||
validate *validator.Validate,
|
|
||||||
) InitialService {
|
|
||||||
return &initialService{
|
|
||||||
Log: utils.Log,
|
|
||||||
Validate: validate,
|
|
||||||
Repository: repo,
|
|
||||||
ApprovalSvc: approvalSvc,
|
|
||||||
approvalWorkflow: utils.ApprovalWorkflowInitial,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s initialService) withRelations(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Preload("CreatedUser").
|
|
||||||
Preload("BankWarehouse").
|
|
||||||
Preload("Customer").
|
|
||||||
Preload("Supplier")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s initialService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
|
|
||||||
initial, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed get initial by id: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !isInitialTransaction(initial.TransactionType) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
|
|
||||||
}
|
|
||||||
if s.ApprovalSvc != nil {
|
|
||||||
approval, err := s.ApprovalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Warnf("Unable to load latest approval for initial %d: %+v", id, err)
|
|
||||||
} else {
|
|
||||||
initial.LatestApproval = approval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return initial, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
party, err := normalizePartyType(req.PartyType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.ensurePartyExists(c.Context(), party, req.PartyId); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
balanceType, err := normalizeInitialBalanceType(req.InitialBalanceType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
code, err := s.generateInitialCode(c.Context())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
reference := req.ReferenceNumber
|
|
||||||
createBody := &entity.Payment{
|
|
||||||
PaymentCode: code,
|
|
||||||
ReferenceNumber: &reference,
|
|
||||||
TransactionType: string(utils.TransactionTypeSaldoAwal),
|
|
||||||
PartyType: party,
|
|
||||||
PartyId: req.PartyId,
|
|
||||||
PaymentDate: time.Now(),
|
|
||||||
PaymentMethod: string(utils.PaymentMethodSaldo),
|
|
||||||
BankId: req.BankId,
|
|
||||||
Direction: directionForInitialType(balanceType),
|
|
||||||
Nominal: signedNominal(balanceType, req.Nominal),
|
|
||||||
Notes: req.Note,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
|
||||||
initialRepoTx := repository.NewInitialRepository(dbTransaction)
|
|
||||||
if err := initialRepoTx.CreateOne(c.Context(), createBody, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.ApprovalSvc != nil {
|
|
||||||
action := entity.ApprovalActionCreated
|
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
|
||||||
_, err := approvalSvcTx.CreateApproval(
|
|
||||||
c.Context(),
|
|
||||||
s.approvalWorkflow,
|
|
||||||
createBody.Id,
|
|
||||||
utils.InitialStepPengajuan,
|
|
||||||
&action,
|
|
||||||
actorID,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to create initial: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetOne(c, createBody.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBody := make(map[string]any)
|
|
||||||
|
|
||||||
if req.ReferenceNumber != nil {
|
|
||||||
updateBody["reference_number"] = *req.ReferenceNumber
|
|
||||||
}
|
|
||||||
if req.Note != nil {
|
|
||||||
updateBody["notes"] = *req.Note
|
|
||||||
}
|
|
||||||
if req.BankId != nil {
|
|
||||||
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
updateBody["bank_id"] = *req.BankId
|
|
||||||
}
|
|
||||||
|
|
||||||
requiresExisting := req.PartyType != nil || req.PartyId != nil || req.InitialBalanceType != nil || req.Nominal != nil
|
|
||||||
requiresVerification := requiresExisting || req.ReferenceNumber != nil || req.Note != nil || req.BankId != nil
|
|
||||||
var existing *entity.Payment
|
|
||||||
if requiresVerification {
|
|
||||||
current, err := s.Repository.GetByID(c.Context(), id, nil)
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed get initial by id: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !isInitialTransaction(current.TransactionType) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
|
|
||||||
}
|
|
||||||
existing = current
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.PartyType != nil || req.PartyId != nil {
|
|
||||||
partyType := existing.PartyType
|
|
||||||
partyId := existing.PartyId
|
|
||||||
|
|
||||||
if req.PartyType != nil {
|
|
||||||
normalized, err := normalizePartyType(*req.PartyType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
partyType = normalized
|
|
||||||
updateBody["party_type"] = partyType
|
|
||||||
}
|
|
||||||
if req.PartyId != nil {
|
|
||||||
partyId = *req.PartyId
|
|
||||||
updateBody["party_id"] = partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.ensurePartyExists(c.Context(), partyType, partyId); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.InitialBalanceType != nil || req.Nominal != nil {
|
|
||||||
balanceType := balanceTypeFromPayment(existing)
|
|
||||||
if req.InitialBalanceType != nil {
|
|
||||||
normalized, err := normalizeInitialBalanceType(*req.InitialBalanceType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
balanceType = normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
nominal := math.Abs(existing.Nominal)
|
|
||||||
if req.Nominal != nil {
|
|
||||||
nominal = *req.Nominal
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBody["direction"] = directionForInitialType(balanceType)
|
|
||||||
updateBody["nominal"] = signedNominal(balanceType, nominal)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(updateBody) == 0 {
|
|
||||||
return s.GetOne(c, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to update initial: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetOne(c, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isInitialTransaction(transactionType string) bool {
|
|
||||||
return strings.EqualFold(transactionType, string(utils.TransactionTypeSaldoAwal))
|
|
||||||
}
|
|
||||||
|
|
||||||
func balanceTypeFromPayment(payment *entity.Payment) string {
|
|
||||||
if strings.EqualFold(payment.Direction, "OUT") || payment.Nominal < 0 {
|
|
||||||
return "NEGATIVE"
|
|
||||||
}
|
|
||||||
return "POSITIVE"
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePartyType(partyType string) (string, error) {
|
|
||||||
party := strings.ToUpper(strings.TrimSpace(partyType))
|
|
||||||
if !utils.IsValidPaymentParty(party) {
|
|
||||||
return "", utils.BadRequest("`party_type` must be `customer` or `supplier`")
|
|
||||||
}
|
|
||||||
return party, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeInitialBalanceType(balanceType string) (string, error) {
|
|
||||||
normalized := strings.ToUpper(strings.TrimSpace(balanceType))
|
|
||||||
switch normalized {
|
|
||||||
case "NEGATIVE", "POSITIVE":
|
|
||||||
return normalized, nil
|
|
||||||
default:
|
|
||||||
return "", utils.BadRequest("`initial_balance_type` must be `NEGATIVE` or `POSITIVE`")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func directionForInitialType(balanceType string) string {
|
|
||||||
if strings.EqualFold(balanceType, "NEGATIVE") {
|
|
||||||
return "OUT"
|
|
||||||
}
|
|
||||||
return "IN"
|
|
||||||
}
|
|
||||||
|
|
||||||
func signedNominal(balanceType string, nominal float64) float64 {
|
|
||||||
normalized := math.Abs(nominal)
|
|
||||||
if strings.EqualFold(balanceType, "NEGATIVE") {
|
|
||||||
return -normalized
|
|
||||||
}
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s initialService) generateInitialCode(ctx context.Context) (string, error) {
|
|
||||||
sequence, err := s.Repository.NextPaymentSequence(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("INIT-%05d", sequence), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s initialService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
|
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Preload("ActionUser")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s initialService) ensurePartyExists(ctx context.Context, partyType string, partyId uint) error {
|
|
||||||
switch utils.PaymentParty(partyType) {
|
|
||||||
case utils.PaymentPartyCustomer:
|
|
||||||
return commonSvc.EnsureRelations(ctx,
|
|
||||||
commonSvc.RelationCheck{Name: "Customer", ID: &partyId, Exists: s.Repository.CustomerExists},
|
|
||||||
)
|
|
||||||
case utils.PaymentPartySupplier:
|
|
||||||
return commonSvc.EnsureRelations(ctx,
|
|
||||||
commonSvc.RelationCheck{Name: "Supplier", ID: &partyId, Exists: s.Repository.SupplierExists},
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return utils.BadRequest("`party_type` must be `customer` or `supplier`")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s initialService) ensureBankExists(ctx context.Context, bankId *uint) error {
|
|
||||||
return commonSvc.EnsureRelations(ctx,
|
|
||||||
commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package validation
|
|
||||||
|
|
||||||
type Create struct {
|
|
||||||
PartyType string `json:"party_type" validate:"required_strict,max=50"`
|
|
||||||
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
|
|
||||||
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
|
|
||||||
ReferenceNumber string `json:"reference_number" validate:"required_strict,max=100"`
|
|
||||||
InitialBalanceType string `json:"initial_balance_type" validate:"required_strict,oneof=NEGATIVE POSITIVE"`
|
|
||||||
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
|
|
||||||
Note string `json:"note" validate:"required_strict,max=500"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Update struct {
|
|
||||||
PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"`
|
|
||||||
PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"`
|
|
||||||
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty" validate:"omitempty,max=100"`
|
|
||||||
InitialBalanceType *string `json:"initial_balance_type,omitempty" validate:"omitempty,oneof=NEGATIVE POSITIVE"`
|
|
||||||
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
|
|
||||||
Note *string `json:"note,omitempty" validate:"omitempty,max=500"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/dto"
|
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/services"
|
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/validations"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InjectionController struct {
|
|
||||||
InjectionService service.InjectionService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInjectionController(injectionService service.InjectionService) *InjectionController {
|
|
||||||
return &InjectionController{
|
|
||||||
InjectionService: injectionService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *InjectionController) GetOne(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.InjectionService.GetOne(c, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get injection successfully",
|
|
||||||
Data: dto.ToInjectionListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *InjectionController) CreateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Create)
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.InjectionService.CreateOne(c, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusCreated,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Balance injection created successfully",
|
|
||||||
Data: dto.ToInjectionListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *InjectionController) UpdateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Update)
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.InjectionService.UpdateOne(c, req, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Update injection successfully",
|
|
||||||
Data: dto.ToInjectionListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
|
||||||
bankDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
|
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// === DTO Structs ===
|
|
||||||
|
|
||||||
type InjectionRelationDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
TransactionType string `json:"transaction_type"`
|
|
||||||
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
|
|
||||||
AdjustmentDate string `json:"adjustment_date"`
|
|
||||||
Direction string `json:"direction"`
|
|
||||||
Nominal float64 `json:"nominal"`
|
|
||||||
Notes string `json:"notes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InjectionListDTO struct {
|
|
||||||
InjectionRelationDTO
|
|
||||||
CreatedBy uint `json:"created_by"`
|
|
||||||
CreatedByUser userDTO.UserRelationDTO `json:"created_by_user,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InjectionDetailDTO struct {
|
|
||||||
InjectionListDTO
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions ===
|
|
||||||
|
|
||||||
func ToInjectionRelationDTO(e entity.Payment) InjectionRelationDTO {
|
|
||||||
return InjectionRelationDTO{
|
|
||||||
Id: e.Id,
|
|
||||||
TransactionType: transactionTypeLabel(e.TransactionType),
|
|
||||||
Bank: bankFromInjection(e),
|
|
||||||
AdjustmentDate: utils.FormatDate(e.PaymentDate),
|
|
||||||
Direction: e.Direction,
|
|
||||||
Nominal: e.Nominal,
|
|
||||||
Notes: e.Notes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToInjectionListDTO(e entity.Payment) InjectionListDTO {
|
|
||||||
approval := approvalDTO.ApprovalRelationDTO{}
|
|
||||||
if e.LatestApproval != nil {
|
|
||||||
approval = approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
|
||||||
}
|
|
||||||
|
|
||||||
return InjectionListDTO{
|
|
||||||
InjectionRelationDTO: ToInjectionRelationDTO(e),
|
|
||||||
CreatedBy: e.CreatedBy,
|
|
||||||
CreatedByUser: userFromInjection(e),
|
|
||||||
CreatedAt: e.CreatedAt,
|
|
||||||
UpdatedAt: e.UpdatedAt,
|
|
||||||
Approval: approval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToInjectionListDTOs(e []entity.Payment) []InjectionListDTO {
|
|
||||||
result := make([]InjectionListDTO, len(e))
|
|
||||||
for i, r := range e {
|
|
||||||
result[i] = ToInjectionListDTO(r)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToInjectionDetailDTO(e entity.Payment) InjectionDetailDTO {
|
|
||||||
return InjectionDetailDTO{
|
|
||||||
InjectionListDTO: ToInjectionListDTO(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func bankFromInjection(e entity.Payment) bankDTO.BankRelationDTO {
|
|
||||||
if e.BankWarehouse.Id == 0 {
|
|
||||||
return bankDTO.BankRelationDTO{}
|
|
||||||
}
|
|
||||||
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func userFromInjection(e entity.Payment) userDTO.UserRelationDTO {
|
|
||||||
if e.CreatedUser.Id == 0 {
|
|
||||||
return userDTO.UserRelationDTO{}
|
|
||||||
}
|
|
||||||
return userDTO.ToUserRelationDTO(e.CreatedUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transactionTypeLabel(transactionType string) string {
|
|
||||||
if strings.EqualFold(transactionType, string(utils.TransactionTypeInjection)) {
|
|
||||||
return "Injection"
|
|
||||||
}
|
|
||||||
return transactionType
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package injections
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
rInjection "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/repositories"
|
|
||||||
sInjection "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/services"
|
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InjectionModule struct{}
|
|
||||||
|
|
||||||
func (InjectionModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
|
||||||
injectionRepo := rInjection.NewInjectionRepository(db)
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowInjection, utils.InjectionApprovalSteps); err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to register injection approval workflow: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
injectionService := sInjection.NewInjectionService(injectionRepo, approvalService, validate)
|
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
|
||||||
|
|
||||||
InjectionRoutes(router, userService, injectionService)
|
|
||||||
}
|
|
||||||
@@ -1,41 +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 InjectionRepository interface {
|
|
||||||
repository.BaseRepository[entity.Payment]
|
|
||||||
BankExists(ctx context.Context, bankId uint) (bool, error)
|
|
||||||
NextPaymentSequence(ctx context.Context) (int64, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type InjectionRepositoryImpl struct {
|
|
||||||
*repository.BaseRepositoryImpl[entity.Payment]
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInjectionRepository(db *gorm.DB) InjectionRepository {
|
|
||||||
return &InjectionRepositoryImpl{
|
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Payment](db),
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *InjectionRepositoryImpl) BankExists(ctx context.Context, bankId uint) (bool, error) {
|
|
||||||
return repository.Exists[entity.Bank](ctx, r.db, bankId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *InjectionRepositoryImpl) NextPaymentSequence(ctx context.Context) (int64, error) {
|
|
||||||
var next int64
|
|
||||||
if err := r.db.WithContext(ctx).
|
|
||||||
Raw("SELECT nextval('payments_code_seq')").
|
|
||||||
Scan(&next).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return next, nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package injections
|
|
||||||
|
|
||||||
import (
|
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/controllers"
|
|
||||||
injection "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/services"
|
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InjectionRoutes(v1 fiber.Router, u user.UserService, s injection.InjectionService) {
|
|
||||||
ctrl := controller.NewInjectionController(s)
|
|
||||||
|
|
||||||
route := v1.Group("/injections")
|
|
||||||
// route.Use(m.Auth(u))
|
|
||||||
|
|
||||||
route.Post("/", ctrl.CreateOne)
|
|
||||||
route.Get("/:id", ctrl.GetOne)
|
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/repositories"
|
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/validations"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InjectionService interface {
|
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error)
|
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Payment, error)
|
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type injectionService struct {
|
|
||||||
Log *logrus.Logger
|
|
||||||
Validate *validator.Validate
|
|
||||||
Repository repository.InjectionRepository
|
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
|
||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInjectionService(
|
|
||||||
repo repository.InjectionRepository,
|
|
||||||
approvalSvc commonSvc.ApprovalService,
|
|
||||||
validate *validator.Validate,
|
|
||||||
) InjectionService {
|
|
||||||
return &injectionService{
|
|
||||||
Log: utils.Log,
|
|
||||||
Validate: validate,
|
|
||||||
Repository: repo,
|
|
||||||
ApprovalSvc: approvalSvc,
|
|
||||||
approvalWorkflow: utils.ApprovalWorkflowInjection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s injectionService) withRelations(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Preload("CreatedUser").
|
|
||||||
Preload("BankWarehouse")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s injectionService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
|
|
||||||
injection, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed get injection by id: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !isInjectionTransaction(injection.TransactionType) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
|
|
||||||
}
|
|
||||||
if s.ApprovalSvc != nil {
|
|
||||||
approval, err := s.ApprovalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Warnf("Unable to load latest approval for injection %d: %+v", id, err)
|
|
||||||
} else {
|
|
||||||
injection.LatestApproval = approval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return injection, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustmentDate, err := utils.ParseDateString(req.AdjustmentDate)
|
|
||||||
if err != nil {
|
|
||||||
return nil, utils.BadRequest(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
code, err := s.generateInjectionCode(c.Context())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
createBody := &entity.Payment{
|
|
||||||
PaymentCode: code,
|
|
||||||
TransactionType: string(utils.TransactionTypeInjection),
|
|
||||||
PartyType: string(utils.PaymentPartyCustomer),
|
|
||||||
PartyId: 0,
|
|
||||||
PaymentDate: adjustmentDate,
|
|
||||||
PaymentMethod: string(utils.PaymentMethodSaldo),
|
|
||||||
BankId: req.BankId,
|
|
||||||
Direction: "IN",
|
|
||||||
Nominal: req.Nominal,
|
|
||||||
Notes: req.Notes,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
|
||||||
injectionRepoTx := repository.NewInjectionRepository(dbTransaction)
|
|
||||||
if err := injectionRepoTx.CreateOne(c.Context(), createBody, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.ApprovalSvc != nil {
|
|
||||||
action := entity.ApprovalActionCreated
|
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
|
||||||
_, err := approvalSvcTx.CreateApproval(
|
|
||||||
c.Context(),
|
|
||||||
s.approvalWorkflow,
|
|
||||||
createBody.Id,
|
|
||||||
utils.InjectionStepPengajuan,
|
|
||||||
&action,
|
|
||||||
actorID,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to create injection: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetOne(c, createBody.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s injectionService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBody := make(map[string]any)
|
|
||||||
|
|
||||||
requiresVerification := req.BankId != nil || req.AdjustmentDate != nil || req.Nominal != nil || req.Notes != nil
|
|
||||||
if requiresVerification {
|
|
||||||
current, err := s.Repository.GetByID(c.Context(), id, nil)
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed get injection by id: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !isInjectionTransaction(current.TransactionType) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.BankId != nil {
|
|
||||||
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
updateBody["bank_id"] = *req.BankId
|
|
||||||
}
|
|
||||||
if req.AdjustmentDate != nil {
|
|
||||||
parsedDate, err := utils.ParseDateString(*req.AdjustmentDate)
|
|
||||||
if err != nil {
|
|
||||||
return nil, utils.BadRequest(err.Error())
|
|
||||||
}
|
|
||||||
updateBody["payment_date"] = parsedDate
|
|
||||||
}
|
|
||||||
if req.Nominal != nil {
|
|
||||||
updateBody["nominal"] = *req.Nominal
|
|
||||||
}
|
|
||||||
if req.Notes != nil {
|
|
||||||
updateBody["notes"] = *req.Notes
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(updateBody) == 0 {
|
|
||||||
return s.GetOne(c, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to update injection: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetOne(c, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isInjectionTransaction(transactionType string) bool {
|
|
||||||
return strings.EqualFold(transactionType, string(utils.TransactionTypeInjection))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s injectionService) generateInjectionCode(ctx context.Context) (string, error) {
|
|
||||||
sequence, err := s.Repository.NextPaymentSequence(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("INJ-%05d", sequence), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s injectionService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
|
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Preload("ActionUser")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s injectionService) ensureBankExists(ctx context.Context, bankId *uint) error {
|
|
||||||
return commonSvc.EnsureRelations(ctx,
|
|
||||||
commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package validation
|
|
||||||
|
|
||||||
type Create struct {
|
|
||||||
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
|
|
||||||
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
|
|
||||||
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
|
|
||||||
Notes string `json:"notes" validate:"required_strict,max=500"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Update struct {
|
|
||||||
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
|
|
||||||
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
|
|
||||||
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
|
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package finance
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FinanceModule struct{}
|
|
||||||
|
|
||||||
func (FinanceModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
|
||||||
RegisterRoutes(router, db, validate)
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/dto"
|
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/services"
|
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/validations"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PaymentController struct {
|
|
||||||
PaymentService service.PaymentService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPaymentController(paymentService service.PaymentService) *PaymentController {
|
|
||||||
return &PaymentController{
|
|
||||||
PaymentService: paymentService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *PaymentController) GetOne(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.PaymentService.GetOne(c, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get payment successfully",
|
|
||||||
Data: dto.ToPaymentListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *PaymentController) CreateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Create)
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.PaymentService.CreateOne(c, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusCreated,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Create payment successfully",
|
|
||||||
Data: dto.ToPaymentListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *PaymentController) UpdateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Update)
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.PaymentService.UpdateOne(c, req, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Update payment successfully",
|
|
||||||
Data: dto.ToPaymentListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
|
||||||
bankDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
|
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// === DTO Structs ===
|
|
||||||
|
|
||||||
type PaymentRelationDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
PaymentCode string `json:"payment_code"`
|
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
|
||||||
TransactionType string `json:"transaction_type"`
|
|
||||||
Party Party `json:"party"`
|
|
||||||
PaymentDate time.Time `json:"payment_date"`
|
|
||||||
PaymentMethod string `json:"payment_method"`
|
|
||||||
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
|
|
||||||
ExpenseAmount float64 `json:"expense_amount"`
|
|
||||||
IncomeAmount float64 `json:"income_amount"`
|
|
||||||
Nominal float64 `json:"nominal"`
|
|
||||||
Notes string `json:"notes"`
|
|
||||||
CreatedUser userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PaymentListDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
PaymentCode string `json:"payment_code"`
|
|
||||||
ReferenceNumber *string `json:"reference_number"`
|
|
||||||
TransactionType string `json:"transaction_type"`
|
|
||||||
Party Party `json:"party"`
|
|
||||||
PaymentDate time.Time `json:"payment_date"`
|
|
||||||
PaymentMethod string `json:"payment_method"`
|
|
||||||
Bank bankDTO.BankRelationDTO `json:"bank"`
|
|
||||||
ExpenseAmount float64 `json:"expense_amount"`
|
|
||||||
IncomeAmount float64 `json:"income_amount"`
|
|
||||||
Nominal float64 `json:"nominal"`
|
|
||||||
Notes string `json:"notes"`
|
|
||||||
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PaymentDetailDTO struct {
|
|
||||||
PaymentListDTO
|
|
||||||
}
|
|
||||||
|
|
||||||
type Party struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
AccountNumber string `json:"account_number"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions ===
|
|
||||||
|
|
||||||
func ToPaymentRelationDTO(e entity.Payment) PaymentRelationDTO {
|
|
||||||
expenseAmount, incomeAmount := paymentAmounts(e.Direction, e.Nominal)
|
|
||||||
|
|
||||||
return PaymentRelationDTO{
|
|
||||||
Id: e.Id,
|
|
||||||
PaymentCode: paymentCodeFromPayment(e),
|
|
||||||
ReferenceNumber: e.ReferenceNumber,
|
|
||||||
TransactionType: transactionTypeFromPayment(e),
|
|
||||||
Party: partyFromPayment(e),
|
|
||||||
PaymentDate: e.PaymentDate,
|
|
||||||
PaymentMethod: e.PaymentMethod,
|
|
||||||
Bank: bankFromPayment(e),
|
|
||||||
ExpenseAmount: expenseAmount,
|
|
||||||
IncomeAmount: incomeAmount,
|
|
||||||
Nominal: e.Nominal,
|
|
||||||
Notes: e.Notes,
|
|
||||||
CreatedUser: userFromPayment(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPaymentListDTO(e entity.Payment) PaymentListDTO {
|
|
||||||
expenseAmount, incomeAmount := paymentAmounts(e.Direction, e.Nominal)
|
|
||||||
approval := approvalDTO.ApprovalRelationDTO{}
|
|
||||||
if e.LatestApproval != nil {
|
|
||||||
approval = approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
|
||||||
}
|
|
||||||
|
|
||||||
return PaymentListDTO{
|
|
||||||
Id: e.Id,
|
|
||||||
PaymentCode: paymentCodeFromPayment(e),
|
|
||||||
ReferenceNumber: e.ReferenceNumber,
|
|
||||||
TransactionType: transactionTypeFromPayment(e),
|
|
||||||
Party: partyFromPayment(e),
|
|
||||||
PaymentDate: e.PaymentDate,
|
|
||||||
PaymentMethod: e.PaymentMethod,
|
|
||||||
Bank: bankFromPayment(e),
|
|
||||||
ExpenseAmount: expenseAmount,
|
|
||||||
IncomeAmount: incomeAmount,
|
|
||||||
Nominal: e.Nominal,
|
|
||||||
Notes: e.Notes,
|
|
||||||
CreatedUser: userFromPayment(e),
|
|
||||||
CreatedAt: e.CreatedAt,
|
|
||||||
UpdatedAt: e.UpdatedAt,
|
|
||||||
Approval: approval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPaymentListDTOs(e []entity.Payment) []PaymentListDTO {
|
|
||||||
result := make([]PaymentListDTO, len(e))
|
|
||||||
for i, r := range e {
|
|
||||||
result[i] = ToPaymentListDTO(r)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToPaymentDetailDTO(e entity.Payment) PaymentDetailDTO {
|
|
||||||
return PaymentDetailDTO{
|
|
||||||
PaymentListDTO: ToPaymentListDTO(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func partyFromPayment(e entity.Payment) Party {
|
|
||||||
party := Party{
|
|
||||||
Id: e.PartyId,
|
|
||||||
Type: e.PartyType,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch utils.PaymentParty(e.PartyType) {
|
|
||||||
case utils.PaymentPartyCustomer:
|
|
||||||
if e.Customer != nil && e.Customer.Id != 0 {
|
|
||||||
party.Name = e.Customer.Name
|
|
||||||
party.AccountNumber = e.Customer.AccountNumber
|
|
||||||
}
|
|
||||||
case utils.PaymentPartySupplier:
|
|
||||||
if e.Supplier != nil && e.Supplier.Id != 0 {
|
|
||||||
party.Name = e.Supplier.Name
|
|
||||||
if e.Supplier.AccountNumber != nil {
|
|
||||||
party.AccountNumber = *e.Supplier.AccountNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return party
|
|
||||||
}
|
|
||||||
|
|
||||||
func bankFromPayment(e entity.Payment) bankDTO.BankRelationDTO {
|
|
||||||
if e.BankWarehouse.Id == 0 {
|
|
||||||
return bankDTO.BankRelationDTO{}
|
|
||||||
}
|
|
||||||
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func userFromPayment(e entity.Payment) userDTO.UserRelationDTO {
|
|
||||||
if e.CreatedUser.Id == 0 {
|
|
||||||
return userDTO.UserRelationDTO{}
|
|
||||||
}
|
|
||||||
return userDTO.ToUserRelationDTO(e.CreatedUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
func paymentCodeFromPayment(e entity.Payment) string {
|
|
||||||
if e.PaymentCode != "" {
|
|
||||||
return e.PaymentCode
|
|
||||||
}
|
|
||||||
if e.ReferenceNumber != nil {
|
|
||||||
return *e.ReferenceNumber
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func transactionTypeFromPayment(e entity.Payment) string {
|
|
||||||
if e.TransactionType != "" {
|
|
||||||
return e.TransactionType
|
|
||||||
}
|
|
||||||
return e.Direction
|
|
||||||
}
|
|
||||||
|
|
||||||
func paymentAmounts(direction string, nominal float64) (float64, float64) {
|
|
||||||
switch strings.ToUpper(direction) {
|
|
||||||
case "IN":
|
|
||||||
return 0, nominal
|
|
||||||
case "OUT":
|
|
||||||
return nominal, 0
|
|
||||||
default:
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package payments
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
rPayment "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/repositories"
|
|
||||||
sPayment "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/services"
|
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PaymentModule struct{}
|
|
||||||
|
|
||||||
func (PaymentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
|
||||||
paymentRepo := rPayment.NewPaymentRepository(db)
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPayment, utils.PaymentApprovalSteps); err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to register payment approval workflow: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
paymentService := sPayment.NewPaymentService(paymentRepo, approvalService, validate)
|
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
|
||||||
|
|
||||||
PaymentRoutes(router, userService, paymentService)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user