Feat(BE-36,37,38,39): master area, customer, kandang, location, warehouse

This commit is contained in:
Hafizh A. Y
2025-10-02 10:51:15 +07:00
parent dbc1f79a36
commit e8905be856
79 changed files with 3745 additions and 169 deletions
+4 -1
View File
@@ -27,7 +27,7 @@ WAIT_DB := docker run --rm --network $(NETWORK) postgres:alpine \
.DEFAULT_GOAL := start .DEFAULT_GOAL := start
# --- Daftar phony targets --- # --- Daftar phony targets ---
.PHONY: start build lint gen \ .PHONY: start build test lint gen \
db-up wait-db \ db-up wait-db \
migration-% migrate-up migrate-down migrate-fresh \ migration-% migrate-up migrate-down migrate-fresh \
seed \ seed \
@@ -40,6 +40,9 @@ start:
build: build:
@go build -o tmp/app ./cmd/api @go build -o tmp/app ./cmd/api
test:
@go test ./test/...
lint: lint:
@golangci-lint run @golangci-lint run
+11 -1
View File
@@ -24,8 +24,11 @@ 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/glebarez/sqlite v1.11.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
@@ -38,15 +41,17 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.2 // indirect github.com/philhofer/fwd v1.1.2 // indirect
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
@@ -71,4 +76,9 @@ require (
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/sqlite v1.5.5 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
) )
+23
View File
@@ -23,12 +23,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -65,6 +71,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -81,6 +88,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
@@ -92,6 +101,9 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -167,6 +179,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -198,6 +211,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/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=
+34
View File
@@ -0,0 +1,34 @@
package repository
import (
"context"
"gorm.io/gorm"
)
// Exists reports whether a record with the given ID exists for type T.
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(new(T)).
Where("id = ?", id).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
var count int64
q := db.WithContext(ctx).
Model(new(T)).
Where("name = ?", name).
Where("deleted_at IS NULL")
if excludeID != nil {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
+41
View File
@@ -0,0 +1,41 @@
package service
import (
"context"
"fmt"
"strings"
"github.com/gofiber/fiber/v2"
)
// RelationCheck describes a foreign-key style dependency that must exist before processing.
type RelationCheck struct {
Name string
ID *uint
Exists func(context.Context, uint) (bool, error)
}
// EnsureRelations validates that each RelationCheck is satisfied, returning consistent Fiber errors.
func EnsureRelations(ctx context.Context, checks ...RelationCheck) error {
for _, check := range checks {
if check.ID == nil {
continue
}
exists, err := check.Exists(ctx, *check.ID)
if err != nil {
return fiber.NewError(
fiber.StatusInternalServerError,
fmt.Sprintf("Failed to check %s", strings.ToLower(check.Name)),
)
}
if !exists {
return fiber.NewError(
fiber.StatusNotFound,
fmt.Sprintf("%s with id %d not found", check.Name, *check.ID),
)
}
}
return nil
}
@@ -70,6 +70,5 @@ func Validator() *validator.Validate {
if err := validate.RegisterValidation("omitempty_strict", OmitemptyStrict); err != nil { if err := validate.RegisterValidation("omitempty_strict", OmitemptyStrict); err != nil {
return nil return nil
} }
return validate return validate
} }
@@ -1,20 +1,32 @@
DROP TABLE IF EXISTS fcr_standards; DROP TABLE IF EXISTS fcr_standards;
DROP INDEX IF EXISTS suppliers_name_unique;
DROP INDEX IF EXISTS products_sku_unique; DROP INDEX IF EXISTS products_sku_unique;
DROP INDEX IF EXISTS products_name_unique;
DROP TABLE IF EXISTS products; DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS flags; DROP TABLE IF EXISTS flags;
DROP INDEX IF EXISTS customers_name_unique;
DROP INDEX IF EXISTS customers_email_unique; DROP INDEX IF EXISTS customers_email_unique;
DROP TABLE IF EXISTS customers; DROP TABLE IF EXISTS customers;
DROP INDEX IF EXISTS warehouses_name_unique;
DROP INDEX IF EXISTS product_categories_code_unique; DROP INDEX IF EXISTS product_categories_code_unique;
DROP INDEX IF EXISTS product_categories_name_unique;
DROP TABLE IF EXISTS product_categories; DROP TABLE IF EXISTS product_categories;
DROP INDEX IF EXISTS nonstocks_name_unique;
DROP TABLE IF EXISTS nonstocks; DROP TABLE IF EXISTS nonstocks;
DROP INDEX IF EXISTS banks_name_unique;
DROP TABLE IF EXISTS banks; DROP TABLE IF EXISTS banks;
DROP INDEX IF EXISTS kandangs_name_unique;
DROP TABLE IF EXISTS warehouses; DROP TABLE IF EXISTS warehouses;
DROP TABLE IF EXISTS kandangs; DROP TABLE IF EXISTS kandangs;
DROP INDEX IF EXISTS locations_name_unique;
DROP TABLE IF EXISTS locations; DROP TABLE IF EXISTS locations;
DROP INDEX IF EXISTS areas_name_unique;
DROP TABLE IF EXISTS areas; DROP TABLE IF EXISTS areas;
DROP INDEX IF EXISTS uoms_name_unique;
DROP TABLE IF EXISTS uoms; DROP TABLE IF EXISTS uoms;
DROP TABLE IF EXISTS suppliers; DROP TABLE IF EXISTS suppliers;
DROP INDEX IF EXISTS fcrs_name_unique;
DROP TABLE IF EXISTS fcrs; DROP TABLE IF EXISTS fcrs;
DROP TABLE IF EXISTS projects; DROP TABLE IF EXISTS projects;
DROP INDEX IF EXISTS users_id_user_unique; DROP INDEX IF EXISTS users_id_user_unique;
@@ -31,8 +31,9 @@ CREATE TABLE product_categories (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code) WHERE deleted_at IS NULL;
-- UOM -- UOM
@@ -42,8 +43,9 @@ CREATE TABLE uoms (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name) WHERE deleted_at IS NULL;
-- PRODUCTS -- PRODUCTS
CREATE TABLE products ( CREATE TABLE products (
@@ -51,8 +53,8 @@ CREATE TABLE products (
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
brand VARCHAR NOT NULL, brand VARCHAR NOT NULL,
sku VARCHAR(100), sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms(id), uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_category_id BIGINT NOT NULL REFERENCES product_categories(id), product_category_id BIGINT NOT NULL REFERENCES product_categories(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_price NUMERIC(15,3) NOT NULL, product_price NUMERIC(15,3) NOT NULL,
selling_price NUMERIC(15,3), selling_price NUMERIC(15,3),
tax NUMERIC(15,3), tax NUMERIC(15,3),
@@ -60,8 +62,9 @@ CREATE TABLE products (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
-- BANKS -- BANKS
@@ -74,8 +77,9 @@ CREATE TABLE banks (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX banks_name_unique ON banks (name) WHERE deleted_at IS NULL;
-- AREAS -- AREAS
CREATE TABLE areas ( CREATE TABLE areas (
@@ -84,51 +88,56 @@ CREATE TABLE areas (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX areas_name_unique ON areas (name) WHERE deleted_at IS NULL;
-- LOCATIONS -- LOCATIONS
CREATE TABLE locations ( CREATE TABLE locations (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas(id), area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX locations_name_unique ON locations (name) WHERE deleted_at IS NULL;
-- KANDANG -- KANDANG
CREATE TABLE kandangs ( CREATE TABLE kandangs (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(191) NOT NULL, name VARCHAR NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations(id), location_id BIGINT NOT NULL REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE,
pic_id BIGINT NOT NULL REFERENCES users(id), pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name) WHERE deleted_at IS NULL;
-- WAREHOUSES -- WAREHOUSES
CREATE TABLE warehouses ( CREATE TABLE warehouses (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations(id), area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
kandang_id BIGINT REFERENCES kandangs(id), location_id BIGINT REFERENCES locations(id) ON DELETE SET NULL ON UPDATE CASCADE,
kandang_id BIGINT REFERENCES kandangs(id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name) WHERE deleted_at IS NULL;
-- CUSTOMERS -- CUSTOMERS
CREATE TABLE customers ( CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
pic_id BIGINT REFERENCES users(id), pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
phone VARCHAR(20) NOT NULL, phone VARCHAR(20) NOT NULL,
@@ -138,19 +147,21 @@ CREATE TABLE customers (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX customers_name_unique ON customers (name) WHERE deleted_at IS NULL;
-- NONSTOCK -- NONSTOCK
CREATE TABLE nonstocks ( CREATE TABLE nonstocks (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
uom_id BIGINT NOT NULL REFERENCES uoms(id), uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name) WHERE deleted_at IS NULL;
-- FCR -- FCR
CREATE TABLE fcrs ( CREATE TABLE fcrs (
@@ -159,8 +170,9 @@ CREATE TABLE fcrs (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name) WHERE deleted_at IS NULL;
CREATE TABLE fcr_standards ( CREATE TABLE fcr_standards (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
@@ -191,8 +203,9 @@ CREATE TABLE suppliers (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL;
-- PROJECTS -- PROJECTS
CREATE TABLE projects ( CREATE TABLE projects (
@@ -200,5 +213,5 @@ CREATE TABLE projects (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id) created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
); );
+2 -2
View File
@@ -3,7 +3,7 @@ package seed
import ( import (
"fmt" "fmt"
mUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -11,7 +11,7 @@ import (
func Run(db *gorm.DB) error { func Run(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error { return db.Transaction(func(tx *gorm.DB) error {
// ===== Users (user) ===== // ===== Users (user) =====
user := mUser.User{ user := entity.User{
Email: "admin@mbugroup.id", Email: "admin@mbugroup.id",
IdUser: 1, IdUser: 1,
Name: "Super Admin", Name: "Super Admin",
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Area struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not 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"`
Locations []Location `gorm:"foreignKey:AreaId;references:Id"`
}
+26
View File
@@ -0,0 +1,26 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Customer struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
PicId uint `gorm:"not null"`
Type string `gorm:"not null"`
Address string `gorm:"not null"`
Phone string `gorm:"not null"`
Email string `gorm:"not null"`
AccountNumber string `gorm:"not null"`
Balance float64 `gorm:"default:0"`
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"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
}
+22
View File
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Kandang struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not 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"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
}
+21
View File
@@ -0,0 +1,21 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Location struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Address string `gorm:"not null"`
AreaId uint `gorm:"not 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"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Uom struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not 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"`
}
+17
View File
@@ -0,0 +1,17 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type User struct {
Id uint `gorm:"primaryKey"`
IdUser int64 `gorm:"uniqueIndex"`
Email string `gorm:"uniqueIndex"`
Name string `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
+25
View File
@@ -0,0 +1,25 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Warehouse struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Type string `gorm:"not null"`
AreaId uint `gorm:"not null"`
LocationId *uint
KandangId *uint
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"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
Location *Location `gorm:"foreignKey:LocationId;references:Id"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
}
+64
View File
@@ -0,0 +1,64 @@
package trim
import (
"bytes"
"encoding/json"
"strings"
"github.com/gofiber/fiber/v2"
)
// JSONBody trims whitespace from string fields in JSON request bodies.
func JSONBody() fiber.Handler {
return func(c *fiber.Ctx) error {
contentType := c.Get(fiber.HeaderContentType)
if !strings.Contains(contentType, fiber.MIMEApplicationJSON) {
return c.Next()
}
body := c.Body()
if len(body) == 0 {
return c.Next()
}
var payload any
if err := json.Unmarshal(body, &payload); err != nil {
return c.Next()
}
trimStrings(payload)
buf := bytes.Buffer{}
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(payload); err != nil {
return err
}
trimmedBody := bytes.TrimSpace(buf.Bytes())
c.Request().SetBody(trimmedBody)
return c.Next()
}
}
func trimStrings(value any) {
switch v := value.(type) {
case map[string]any:
for key, val := range v {
if str, ok := val.(string); ok {
v[key] = strings.TrimSpace(str)
continue
}
trimStrings(val)
}
case []any:
for i, elem := range v {
if str, ok := elem.(string); ok {
v[i] = strings.TrimSpace(str)
continue
}
trimStrings(elem)
}
}
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type AreaController struct {
AreaService service.AreaService
}
func NewAreaController(areaService service.AreaService) *AreaController {
return &AreaController{
AreaService: areaService,
}
}
func (u *AreaController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.AreaService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.AreaListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all areas successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToAreaListDTOs(result),
})
}
func (u *AreaController) 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.AreaService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get area successfully",
Data: dto.ToAreaListDTO(*result),
})
}
func (u *AreaController) 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.AreaService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create area successfully",
Data: dto.ToAreaListDTO(*result),
})
}
func (u *AreaController) 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.AreaService.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 area successfully",
Data: dto.ToAreaListDTO(*result),
})
}
func (u *AreaController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.AreaService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete area successfully",
})
}
@@ -0,0 +1,58 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type AreaBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaListDTO struct {
AreaBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type AreaDetailDTO struct {
AreaListDTO
}
// === Mapper Functions ===
func ToAreaBaseDTO(e entity.Area) AreaBaseDTO {
return AreaBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToAreaListDTO(e entity.Area) AreaListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return AreaListDTO{
AreaBaseDTO: ToAreaBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToAreaListDTOs(e []entity.Area) []AreaListDTO {
result := make([]AreaListDTO, len(e))
for i, r := range e {
result[i] = ToAreaListDTO(r)
}
return result
}
+26
View File
@@ -0,0 +1,26 @@
package areas
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rArea "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/repositories"
sArea "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type AreaModule struct{}
func (AreaModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
areaRepo := rArea.NewAreaRepository(db)
userRepo := rUser.NewUserRepository(db)
areaService := sArea.NewAreaService(areaRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
AreaRoutes(router, userService, areaService)
}
@@ -0,0 +1,31 @@
package repository
import (
"context"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type AreaRepository interface {
repository.BaseRepository[entity.Area]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type AreaRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Area]
db *gorm.DB
}
func NewAreaRepository(db *gorm.DB) AreaRepository {
return &AreaRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Area](db),
db: db,
}
}
func (r *AreaRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Area](ctx, r.db, name, excludeID)
}
+28
View File
@@ -0,0 +1,28 @@
package areas
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/controllers"
area "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func AreaRoutes(v1 fiber.Router, u user.UserService, s area.AreaService) {
ctrl := controller.NewAreaController(s)
route := v1.Group("/areas")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,145 @@
package service
import (
"errors"
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type AreaService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Area, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Area, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Area, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Area, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type areaService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.AreaRepository
}
func NewAreaService(repo repository.AreaRepository, validate *validator.Validate) AreaService {
return &areaService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s areaService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Area, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
areas, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get areas: %+v", err)
return nil, 0, err
}
return areas, total, nil
}
func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) {
area, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Area not found")
}
if err != nil {
s.Log.Errorf("Failed get area by id: %+v", err)
return nil, err
}
return area, nil
}
func (s *areaService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Area, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check area name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check area name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Area with name %s already exists", req.Name))
}
//TODO: created by dummy
createBody := &entity.Area{
Name: req.Name,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create area: %+v", err)
return nil, err
}
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
}
func (s areaService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Area, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check area name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check area name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Area with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
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, "Area not found")
}
s.Log.Errorf("Failed to update area: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s areaService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Area not found")
}
s.Log.Errorf("Failed to delete area: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,15 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type CustomerController struct {
CustomerService service.CustomerService
}
func NewCustomerController(customerService service.CustomerService) *CustomerController {
return &CustomerController{
CustomerService: customerService,
}
}
func (u *CustomerController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.CustomerService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.CustomerListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all customers successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToCustomerListDTOs(result),
})
}
func (u *CustomerController) 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.CustomerService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get customer successfully",
Data: dto.ToCustomerListDTO(*result),
})
}
func (u *CustomerController) 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.CustomerService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create customer successfully",
Data: dto.ToCustomerListDTO(*result),
})
}
func (u *CustomerController) 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.CustomerService.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 customer successfully",
Data: dto.ToCustomerListDTO(*result),
})
}
func (u *CustomerController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.CustomerService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete customer successfully",
})
}
@@ -0,0 +1,79 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type CustomerBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
PicId uint `json:"pic_id"`
Type string `json:"type"`
Address string `json:"address"`
Phone string `json:"phone"`
Email string `json:"email"`
AccountNumber string `json:"account_number"`
Pic *userDTO.UserBaseDTO `json:"pic"`
}
type CustomerListDTO struct {
CustomerBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CustomerDetailDTO struct {
CustomerListDTO
}
// === Mapper Functions ===
func ToCustomerBaseDTO(e entity.Customer) CustomerBaseDTO {
var pic *userDTO.UserBaseDTO
if e.Pic.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.Pic)
pic = &mapped
}
return CustomerBaseDTO{
Id: e.Id,
Name: e.Name,
PicId: e.PicId,
Type: e.Type,
Address: e.Address,
Phone: e.Phone,
Email: e.Email,
AccountNumber: e.AccountNumber,
Pic: pic,
}
}
func ToCustomerListDTO(e entity.Customer) CustomerListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return CustomerListDTO{
CustomerBaseDTO: ToCustomerBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToCustomerListDTOs(e []entity.Customer) []CustomerListDTO {
result := make([]CustomerListDTO, len(e))
for i, r := range e {
result[i] = ToCustomerListDTO(r)
}
return result
}
@@ -0,0 +1,26 @@
package customers
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
sCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type CustomerModule struct{}
func (CustomerModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
customerRepo := rCustomer.NewCustomerRepository(db)
userRepo := rUser.NewUserRepository(db)
customerService := sCustomer.NewCustomerService(customerRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
CustomerRoutes(router, userService, customerService)
}
@@ -0,0 +1,35 @@
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 CustomerRepository interface {
repository.BaseRepository[entity.Customer]
PicExists(ctx context.Context, areaId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type CustomerRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Customer]
db *gorm.DB
}
func NewCustomerRepository(db *gorm.DB) CustomerRepository {
return &CustomerRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Customer](db),
db: db,
}
}
func (r *CustomerRepositoryImpl) PicExists(ctx context.Context, picId uint) (bool, error) {
return repository.Exists[entity.User](ctx, r.db, picId)
}
func (r *CustomerRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Customer](ctx, r.db, name, excludeID)
}
@@ -0,0 +1,28 @@
package customers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/controllers"
customer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func CustomerRoutes(v1 fiber.Router, u user.UserService, s customer.CustomerService) {
ctrl := controller.NewCustomerController(s)
route := v1.Group("/customers")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,179 @@
package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type CustomerService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Customer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Customer, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Customer, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Customer, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type customerService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.CustomerRepository
}
func NewCustomerService(repo repository.CustomerRepository, validate *validator.Validate) CustomerService {
return &customerService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s customerService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").Preload("Pic")
}
func (s customerService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Customer, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
customers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get customers: %+v", err)
return nil, 0, err
}
return customers, total, nil
}
func (s customerService) GetOne(c *fiber.Ctx, id uint) (*entity.Customer, error) {
customer, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Customer not found")
}
if err != nil {
s.Log.Errorf("Failed get customer by id: %+v", err)
return nil, err
}
return customer, nil
}
func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Customer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check customer name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check customer name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Customer with name %s already exists", req.Name))
}
typ := strings.ToUpper(req.Type)
if !utils.IsValidCustomerSupplierType(typ) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid customer type")
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Pic", ID: &req.PicId, Exists: s.Repository.PicExists},
); err != nil {
return nil, err
}
//TODO: created by dummy
createBody := &entity.Customer{
Name: req.Name,
PicId: req.PicId,
Type: typ,
Address: req.Address,
Phone: req.Phone,
Email: req.Email,
AccountNumber: req.AccountNumber,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create customer: %+v", err)
return nil, err
}
return createBody, nil
}
func (s customerService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Customer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check customer name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check customer name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Customer with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.PicId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists}); err != nil {
return nil, err
}
updateBody["pic_id"] = *req.PicId
}
if req.Type != nil {
typ := strings.ToUpper(*req.Type)
if !utils.IsValidCustomerSupplierType(typ) {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid customer type")
}
updateBody["type"] = typ
}
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, "Customer not found")
}
s.Log.Errorf("Failed to update customer: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s customerService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Customer not found")
}
s.Log.Errorf("Failed to delete customer: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,27 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
Type string `json:"type" validate:"required_strict"`
Address string `json:"address" validate:"required_strict"`
Phone string `json:"phone" validate:"required_strict,max=20"`
Email string `json:"email" validate:"required_strict,email"`
AccountNumber string `json:"account_number" validate:"required_strict"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
Type *string `json:"type,omitempty" validate:"omitempty"`
Address *string `json:"address,omitempty" validate:"omitempty"`
Phone *string `json:"phone,omitempty" validate:"omitempty"`
Email *string `json:"email,omitempty" validate:"omitempty"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type KandangController struct {
KandangService service.KandangService
}
func NewKandangController(kandangService service.KandangService) *KandangController {
return &KandangController{
KandangService: kandangService,
}
}
func (u *KandangController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.KandangService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.KandangListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all kandangs successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToKandangListDTOs(result),
})
}
func (u *KandangController) 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.KandangService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get kandang successfully",
Data: dto.ToKandangListDTO(*result),
})
}
func (u *KandangController) 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.KandangService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create kandang successfully",
Data: dto.ToKandangListDTO(*result),
})
}
func (u *KandangController) 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.KandangService.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 kandang successfully",
Data: dto.ToKandangListDTO(*result),
})
}
func (u *KandangController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.KandangService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete kandang successfully",
})
}
@@ -0,0 +1,75 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type KandangBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Pic *userDTO.UserBaseDTO `json:"pic"`
}
type KandangListDTO struct {
KandangBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type KandangDetailDTO struct {
KandangListDTO
}
// === Mapper Functions ===
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
var location *locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
location = &mapped
}
var pic *userDTO.UserBaseDTO
if e.Pic.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.Pic)
pic = &mapped
}
return KandangBaseDTO{
Id: e.Id,
Name: e.Name,
Location: location,
Pic: pic,
}
}
func ToKandangListDTO(e entity.Kandang) KandangListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return KandangListDTO{
KandangBaseDTO: ToKandangBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToKandangListDTOs(e []entity.Kandang) []KandangListDTO {
result := make([]KandangListDTO, len(e))
for i, r := range e {
result[i] = ToKandangListDTO(r)
}
return result
}
@@ -0,0 +1,26 @@
package kandangs
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
sKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type KandangModule struct{}
func (KandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
kandangRepo := rKandang.NewKandangRepository(db)
userRepo := rUser.NewUserRepository(db)
kandangService := sKandang.NewKandangService(kandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
KandangRoutes(router, userService, kandangService)
}
@@ -0,0 +1,40 @@
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 KandangRepository interface {
repository.BaseRepository[entity.Kandang]
LocationExists(ctx context.Context, areaId uint) (bool, error)
PicExists(ctx context.Context, areaId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type KandangRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Kandang]
db *gorm.DB
}
func NewKandangRepository(db *gorm.DB) KandangRepository {
return &KandangRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Kandang](db),
db: db,
}
}
func (r *KandangRepositoryImpl) LocationExists(ctx context.Context, locationId uint) (bool, error) {
return repository.Exists[entity.Location](ctx, r.db, locationId)
}
func (r *KandangRepositoryImpl) PicExists(ctx context.Context, picId uint) (bool, error) {
return repository.Exists[entity.User](ctx, r.db, picId)
}
func (r *KandangRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Kandang](ctx, r.db, name, excludeID)
}
+28
View File
@@ -0,0 +1,28 @@
package kandangs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService) {
ctrl := controller.NewKandangController(s)
route := v1.Group("/kandangs")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,169 @@
package service
import (
"errors"
"fmt"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type KandangService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Kandang, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Kandang, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Kandang, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type kandangService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.KandangRepository
}
func NewKandangService(repo repository.KandangRepository, validate *validator.Validate) KandangService {
return &kandangService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").Preload("Location").Preload("Pic")
}
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get kandangs: %+v", err)
return nil, 0, err
}
return kandangs, total, nil
}
func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) {
kandang, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
if err != nil {
s.Log.Errorf("Failed get kandang by id: %+v", err)
return nil, err
}
return kandang, nil
}
func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Kandang, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check kandang name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check kandang name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang with name %s already exists", req.Name))
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
common.RelationCheck{Name: "Pic", ID: &req.PicId, Exists: s.Repository.PicExists},
); err != nil {
return nil, err
}
//TODO: created by dummy
createBody := &entity.Kandang{
Name: req.Name,
LocationId: req.LocationId,
PicId: req.PicId,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create kandang: %+v", err)
return nil, err
}
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
}
func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Kandang, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check kandang name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check kandang name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.LocationId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists}); err != nil {
return nil, err
}
updateBody["location_id"] = *req.LocationId
}
if req.PicId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists}); err != nil {
return nil, err
}
updateBody["pic_id"] = *req.PicId
}
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, "Kandang not found")
}
s.Log.Errorf("Failed to update kandang: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s kandangService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
s.Log.Errorf("Failed to delete kandang: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,19 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type LocationController struct {
LocationService service.LocationService
}
func NewLocationController(locationService service.LocationService) *LocationController {
return &LocationController{
LocationService: locationService,
}
}
func (u *LocationController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.LocationService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.LocationListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all locations successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToLocationListDTOs(result),
})
}
func (u *LocationController) 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.LocationService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get location successfully",
Data: dto.ToLocationListDTO(*result),
})
}
func (u *LocationController) 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.LocationService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create location successfully",
Data: dto.ToLocationListDTO(*result),
})
}
func (u *LocationController) 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.LocationService.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 location successfully",
Data: dto.ToLocationListDTO(*result),
})
}
func (u *LocationController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.LocationService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete location successfully",
})
}
@@ -0,0 +1,69 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type LocationBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
Area *areaDTO.AreaBaseDTO `json:"area"`
}
type LocationListDTO struct {
LocationBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type LocationDetailDTO struct {
LocationListDTO
}
// === Mapper Functions ===
func ToLocationBaseDTO(e entity.Location) LocationBaseDTO {
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
return LocationBaseDTO{
Id: e.Id,
Name: e.Name,
Address: e.Address,
Area: area,
}
}
func ToLocationListDTO(e entity.Location) LocationListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return LocationListDTO{
LocationBaseDTO: ToLocationBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToLocationListDTOs(e []entity.Location) []LocationListDTO {
result := make([]LocationListDTO, len(e))
for i, r := range e {
result[i] = ToLocationListDTO(r)
}
return result
}
@@ -0,0 +1,26 @@
package locations
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rLocation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/repositories"
sLocation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type LocationModule struct{}
func (LocationModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
locationRepo := rLocation.NewLocationRepository(db)
userRepo := rUser.NewUserRepository(db)
locationService := sLocation.NewLocationService(locationRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
LocationRoutes(router, userService, locationService)
}
@@ -0,0 +1,35 @@
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 LocationRepository interface {
repository.BaseRepository[entity.Location]
AreaExists(ctx context.Context, areaId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type LocationRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Location]
db *gorm.DB
}
func NewLocationRepository(db *gorm.DB) LocationRepository {
return &LocationRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Location](db),
db: db,
}
}
func (r *LocationRepositoryImpl) AreaExists(ctx context.Context, areaId uint) (bool, error) {
return repository.Exists[entity.Area](ctx, r.db, areaId)
}
func (r *LocationRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Location](ctx, r.db, name, excludeID)
}
@@ -0,0 +1,28 @@
package locations
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/controllers"
location "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func LocationRoutes(v1 fiber.Router, u user.UserService, s location.LocationService) {
ctrl := controller.NewLocationController(s)
route := v1.Group("/locations")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,171 @@
package service
import (
"errors"
"fmt"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type LocationService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Location, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Location, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Location, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Location, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type locationService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.LocationRepository
}
func NewLocationService(repo repository.LocationRepository, validate *validator.Validate) LocationService {
return &locationService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s locationService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Area").Preload("CreatedUser")
}
func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Location, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
locations, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get locations: %+v", err)
return nil, 0, err
}
return locations, total, nil
}
func (s locationService) GetOne(c *fiber.Ctx, id uint) (*entity.Location, error) {
location, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Location not found")
}
if err != nil {
s.Log.Errorf("Failed get location by id: %+v", err)
return nil, err
}
return location, nil
}
func (s *locationService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Location, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check location name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check location name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Location with name %s already exists", req.Name))
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
); err != nil {
return nil, err
}
//TODO: created by dummy
createBody := &entity.Location{
Name: req.Name,
Address: req.Address,
AreaId: req.AreaId,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create location: %+v", err)
return nil, err
}
created, err := s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
if err != nil {
s.Log.Errorf("Failed to reload created location: %+v", err)
return nil, err
}
return created, nil
}
func (s locationService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Location, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check location name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check location name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Location with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.Address != nil {
updateBody["address"] = *req.Address
}
if req.AreaId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Area", ID: req.AreaId, Exists: s.Repository.AreaExists}); err != nil {
return nil, err
}
updateBody["area_id"] = *req.AreaId
}
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, "Location not found")
}
s.Log.Errorf("Failed to update location: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s locationService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Location not found")
}
s.Log.Errorf("Failed to delete location: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,19 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Address string `json:"address" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Address *string `json:"address,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
+11 -1
View File
@@ -7,15 +7,25 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
areas "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas"
customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers"
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms" uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
// MODULE IMPORTS // MODULE IMPORTS
) )
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
group := router.Group("/master") group := router.Group("/master-data")
allModules := []modules.Module{ allModules := []modules.Module{
uoms.UomModule{}, uoms.UomModule{},
areas.AreaModule{},
locations.LocationModule{},
kandangs.KandangModule{},
warehouses.WarehouseModule{},
customers.CustomerModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }
+20 -11
View File
@@ -3,7 +3,8 @@ package dto
import ( import (
"time" "time"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/models" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
// === DTO Structs === // === DTO Structs ===
@@ -15,6 +16,7 @@ type UomBaseDTO struct {
type UomListDTO struct { type UomListDTO struct {
UomBaseDTO UomBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
@@ -25,24 +27,31 @@ type UomDetailDTO struct {
// === Mapper Functions === // === Mapper Functions ===
func ToUomBaseDTO(m model.Uom) UomBaseDTO { func ToUomBaseDTO(e entity.Uom) UomBaseDTO {
return UomBaseDTO{ return UomBaseDTO{
Id: m.Id, Id: e.Id,
Name: m.Name, Name: e.Name,
} }
} }
func ToUomListDTO(m model.Uom) UomListDTO { func ToUomListDTO(e entity.Uom) UomListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return UomListDTO{ return UomListDTO{
UomBaseDTO: ToUomBaseDTO(m), UomBaseDTO: ToUomBaseDTO(e),
CreatedAt: m.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: m.UpdatedAt, UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
} }
} }
func ToUomListDTOs(m []model.Uom) []UomListDTO { func ToUomListDTOs(e []entity.Uom) []UomListDTO {
result := make([]UomListDTO, len(m)) result := make([]UomListDTO, len(e))
for i, r := range m { for i, r := range e {
result[i] = ToUomListDTO(r) result[i] = ToUomListDTO(r)
} }
return result return result
@@ -1,19 +0,0 @@
package model
import (
"time"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models"
"gorm.io/gorm"
)
type Uom struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
CreatedBy int64 `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser model.User `gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -1,21 +1,30 @@
package repository package repository
import ( import (
model "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/models" "context"
"gitlab.com/mbugroup/lti-api.git/internal/repository"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
) )
type UomRepository interface { type UomRepository interface {
repository.BaseRepository[model.Uom] repository.BaseRepository[entity.Uom]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
} }
type UomRepositoryImpl struct { type UomRepositoryImpl struct {
*repository.BaseRepositoryImpl[model.Uom] *repository.BaseRepositoryImpl[entity.Uom]
db *gorm.DB
} }
func NewUomRepository(db *gorm.DB) UomRepository { func NewUomRepository(db *gorm.DB) UomRepository {
return &UomRepositoryImpl{ return &UomRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[model.Uom](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.Uom](db),
db: db,
} }
} }
func (r *UomRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Uom](ctx, r.db, name, excludeID)
}
@@ -2,8 +2,9 @@ package service
import ( import (
"errors" "errors"
"fmt"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/models" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -15,10 +16,10 @@ import (
) )
type UomService interface { type UomService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]model.Uom, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Uom, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*model.Uom, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.Uom, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*model.Uom, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Uom, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*model.Uom, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Uom, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
} }
@@ -35,7 +36,12 @@ func NewUomService(repo repository.UomRepository, validate *validator.Validate)
Repository: repo, Repository: repo,
} }
} }
func (s uomService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.Uom, int64, error) {
func (s uomService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s uomService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Uom, int64, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -43,6 +49,7 @@ func (s uomService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.Uom,
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
uoms, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { uoms, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" { if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%") return db.Where("name LIKE ?", "%"+params.Search+"%")
} }
@@ -56,8 +63,8 @@ func (s uomService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.Uom,
return uoms, total, nil return uoms, total, nil
} }
func (s uomService) GetOne(c *fiber.Ctx, id uint) (*model.Uom, error) { func (s uomService) GetOne(c *fiber.Ctx, id uint) (*entity.Uom, error) {
uom, err := s.Repository.GetByID(c.Context(), id, nil) uom, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uom not found") return nil, fiber.NewError(fiber.StatusNotFound, "Uom not found")
} }
@@ -68,12 +75,20 @@ func (s uomService) GetOne(c *fiber.Ctx, id uint) (*model.Uom, error) {
return uom, nil return uom, nil
} }
func (s *uomService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.Uom, error) { func (s *uomService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Uom, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
createBody := &model.Uom{ if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check uom name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check uom name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Uom with name %s already exists", req.Name))
}
//TODO: created by dummy
createBody := &entity.Uom{
Name: req.Name, Name: req.Name,
CreatedBy: 1, CreatedBy: 1,
} }
@@ -83,10 +98,10 @@ func (s *uomService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.Uom
return nil, err return nil, err
} }
return createBody, nil return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
} }
func (s uomService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*model.Uom, error) { func (s uomService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Uom, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
@@ -94,9 +109,19 @@ func (s uomService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*m
updateBody := make(map[string]any) updateBody := make(map[string]any)
if req.Name != nil { if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check uom name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check uom name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Uom with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name updateBody["name"] = *req.Name
} }
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uom not found") return nil, fiber.NewError(fiber.StatusNotFound, "Uom not found")
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type WarehouseController struct {
WarehouseService service.WarehouseService
}
func NewWarehouseController(warehouseService service.WarehouseService) *WarehouseController {
return &WarehouseController{
WarehouseService: warehouseService,
}
}
func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.WarehouseService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.WarehouseListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all warehouses successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToWarehouseListDTOs(result),
})
}
func (u *WarehouseController) 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.WarehouseService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get warehouse successfully",
Data: dto.ToWarehouseListDTO(*result),
})
}
func (u *WarehouseController) 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.WarehouseService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create warehouse successfully",
Data: dto.ToWarehouseListDTO(*result),
})
}
func (u *WarehouseController) 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.WarehouseService.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 warehouse successfully",
Data: dto.ToWarehouseListDTO(*result),
})
}
func (u *WarehouseController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.WarehouseService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete warehouse successfully",
})
}
@@ -0,0 +1,87 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type WarehouseBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Area *areaDTO.AreaBaseDTO `json:"area"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang"`
}
type WarehouseListDTO struct {
WarehouseBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WarehouseDetailDTO struct {
WarehouseListDTO
}
// === Mapper Functions ===
func ToWarehouseBaseDTO(e entity.Warehouse) WarehouseBaseDTO {
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
var location *locationDTO.LocationBaseDTO
if e.Location != nil && e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(*e.Location)
location = &mapped
}
var kandang *kandangDTO.KandangBaseDTO
if e.Kandang != nil && e.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangBaseDTO(*e.Kandang)
kandang = &mapped
}
return WarehouseBaseDTO{
Id: e.Id,
Name: e.Name,
Type: e.Type,
Area: area,
Location: location,
Kandang: kandang,
}
}
func ToWarehouseListDTO(e entity.Warehouse) WarehouseListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return WarehouseListDTO{
WarehouseBaseDTO: ToWarehouseBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToWarehouseListDTOs(e []entity.Warehouse) []WarehouseListDTO {
result := make([]WarehouseListDTO, len(e))
for i, r := range e {
result[i] = ToWarehouseListDTO(r)
}
return result
}
@@ -0,0 +1,26 @@
package warehouses
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
sWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type WarehouseModule struct{}
func (WarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
warehouseService := sWarehouse.NewWarehouseService(warehouseRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
WarehouseRoutes(router, userService, warehouseService)
}
@@ -0,0 +1,45 @@
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 WarehouseRepository interface {
repository.BaseRepository[entity.Warehouse]
AreaExists(ctx context.Context, areaId uint) (bool, error)
LocationExists(ctx context.Context, locationId uint) (bool, error)
KandangExists(ctx context.Context, kandangId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type WarehouseRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Warehouse]
db *gorm.DB
}
func NewWarehouseRepository(db *gorm.DB) WarehouseRepository {
return &WarehouseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Warehouse](db),
db: db,
}
}
func (r *WarehouseRepositoryImpl) AreaExists(ctx context.Context, areaId uint) (bool, error) {
return repository.Exists[entity.Area](ctx, r.db, areaId)
}
func (r *WarehouseRepositoryImpl) LocationExists(ctx context.Context, locationId uint) (bool, error) {
return repository.Exists[entity.Location](ctx, r.db, locationId)
}
func (r *WarehouseRepositoryImpl) KandangExists(ctx context.Context, kandangId uint) (bool, error) {
return repository.Exists[entity.Kandang](ctx, r.db, kandangId)
}
func (r *WarehouseRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Warehouse](ctx, r.db, name, excludeID)
}
@@ -0,0 +1,28 @@
package warehouses
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/controllers"
warehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func WarehouseRoutes(v1 fiber.Router, u user.UserService, s warehouse.WarehouseService) {
ctrl := controller.NewWarehouseController(s)
route := v1.Group("/warehouses")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,268 @@
package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type WarehouseService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Warehouse, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Warehouse, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Warehouse, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Warehouse, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type warehouseService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.WarehouseRepository
}
func NewWarehouseService(repo repository.WarehouseRepository, validate *validator.Validate) WarehouseService {
return &warehouseService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s warehouseService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").Preload("Area").Preload("Location").Preload("Kandang")
}
func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Warehouse, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get warehouses: %+v", err)
return nil, 0, err
}
return warehouses, total, nil
}
func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) {
warehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
if err != nil {
s.Log.Errorf("Failed get warehouse by id: %+v", err)
return nil, err
}
return warehouse, nil
}
func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Warehouse, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check warehouse name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Warehouse with name %s already exists", req.Name))
}
typ := strings.ToUpper(req.Type)
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, req.LocationId, req.KandangId); err != nil {
return nil, err
}
//? Check relation area, location, and kandang
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists},
common.RelationCheck{Name: "Kandang", ID: req.KandangId, Exists: s.Repository.KandangExists},
); err != nil {
return nil, err
}
//TODO: created by dummy
createBody := &entity.Warehouse{
Name: req.Name,
Type: typ,
AreaId: req.AreaId,
CreatedBy: 1,
}
if req.LocationId != nil {
createBody.LocationId = req.LocationId
}
if req.KandangId != nil {
createBody.KandangId = req.KandangId
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create warehouse: %+v", err)
return nil, err
}
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
}
func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Warehouse, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
existing, err := s.GetOne(c, id)
if err != nil {
s.Log.Errorf("Failed to get warehouse for update: %+v", err)
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check warehouse name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Warehouse with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.Type != nil {
normalizedType := strings.ToUpper(*req.Type)
updateBody["type"] = normalizedType
req.Type = &normalizedType
}
if req.AreaId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Area", ID: req.AreaId, Exists: s.Repository.AreaExists}); err != nil {
return nil, err
}
updateBody["area_id"] = *req.AreaId
}
if req.LocationId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists}); err != nil {
return nil, err
}
updateBody["location_id"] = req.LocationId
}
if req.KandangId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Kandang", ID: req.KandangId, Exists: s.Repository.KandangExists}); err != nil {
return nil, err
}
updateBody["kandang_id"] = req.KandangId
}
finalType := strings.ToUpper(existing.Type)
if req.Type != nil {
finalType = *req.Type
}
finalAreaId := existing.AreaId
if req.AreaId != nil {
finalAreaId = *req.AreaId
}
finalLocationId := existing.LocationId
if req.LocationId != nil {
finalLocationId = req.LocationId
}
finalKandangId := existing.KandangId
if req.KandangId != nil {
finalKandangId = req.KandangId
}
if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, finalLocationId, finalKandangId); err != nil {
return nil, err
}
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, "Warehouse not found")
}
s.Log.Errorf("Failed to update warehouse: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
s.Log.Errorf("Failed to delete warehouse: %+v", err)
return err
}
return nil
}
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID *uint, kandangID *uint) error {
switch utils.WarehouseType(typ) {
case utils.WarehouseTypeArea:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is AREA")
}
if locationID != nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
}
if kandangID != nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
}
return nil
case utils.WarehouseTypeLokasi:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is LOCATION")
}
if locationID == nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is LOCATION")
}
if *locationID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is LOCATION")
}
if kandangID != nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
}
return nil
case utils.WarehouseTypeKandang:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is KANDANG")
}
if locationID == nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is KANDANG")
}
if *locationID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is KANDANG")
}
if kandangID == nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id is required when type is KANDANG")
}
if *kandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must be greater than 0 when type is KANDANG")
}
return nil
default:
return fiber.NewError(fiber.StatusBadRequest, "Invalid warehouse type")
}
}
@@ -0,0 +1,23 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Type string `json:"type" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Type *string `json:"type,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
+6 -4
View File
@@ -3,7 +3,7 @@ package dto
import ( import (
"time" "time"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
) )
// === DTO Structs === // === DTO Structs ===
@@ -27,14 +27,16 @@ type UserDetailDTO struct {
// === Mapper Functions === // === Mapper Functions ===
func ToUserBaseDTO(m model.User) UserBaseDTO { func ToUserBaseDTO(m entity.User) UserBaseDTO {
return UserBaseDTO{ return UserBaseDTO{
Id: m.Id, Id: m.Id,
IdUser: m.IdUser,
Email: m.Email,
Name: m.Name, Name: m.Name,
} }
} }
func ToUserListDTO(m model.User) UserListDTO { func ToUserListDTO(m entity.User) UserListDTO {
return UserListDTO{ return UserListDTO{
UserBaseDTO: ToUserBaseDTO(m), UserBaseDTO: ToUserBaseDTO(m),
CreatedAt: m.CreatedAt, CreatedAt: m.CreatedAt,
@@ -42,7 +44,7 @@ func ToUserListDTO(m model.User) UserListDTO {
} }
} }
func ToUserListDTOs(m []model.User) []UserListDTO { func ToUserListDTOs(m []entity.User) []UserListDTO {
result := make([]UserListDTO, len(m)) result := make([]UserListDTO, len(m))
for i, r := range m { for i, r := range m {
result[i] = ToUserListDTO(r) result[i] = ToUserListDTO(r)
@@ -1,17 +0,0 @@
package model
import (
"time"
"gorm.io/gorm"
)
type User struct {
Id uint `gorm:"primaryKey"`
IdUser int64 `gorm:"uniqueIndex"`
Email string `gorm:"uniqueIndex"`
Name string `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
@@ -1,21 +1,21 @@
package repository package repository
import ( import (
model "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gitlab.com/mbugroup/lti-api.git/internal/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
) )
type UserRepository interface { type UserRepository interface {
repository.BaseRepository[model.User] repository.BaseRepository[entity.User]
} }
type UserRepositoryImpl struct { type UserRepositoryImpl struct {
*repository.BaseRepositoryImpl[model.User] *repository.BaseRepositoryImpl[entity.User]
} }
func NewUserRepository(db *gorm.DB) UserRepository { func NewUserRepository(db *gorm.DB) UserRepository {
return &UserRepositoryImpl{ return &UserRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[model.User](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.User](db),
} }
} }
+14 -10
View File
@@ -3,7 +3,7 @@ package service
import ( import (
"errors" "errors"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/users/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/users/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -15,10 +15,10 @@ import (
) )
type UserService interface { type UserService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]model.User, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.User, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*model.User, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.User, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*model.User, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.User, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*model.User, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.User, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
} }
@@ -35,7 +35,7 @@ func NewUserService(repo repository.UserRepository, validate *validator.Validate
Repository: repo, Repository: repo,
} }
} }
func (s userService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.User, int64, error) { func (s userService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.User, int64, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -56,7 +56,7 @@ func (s userService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.Use
return users, total, nil return users, total, nil
} }
func (s userService) GetOne(c *fiber.Ctx, id uint) (*model.User, error) { func (s userService) GetOne(c *fiber.Ctx, id uint) (*entity.User, error) {
user, err := s.Repository.GetByID(c.Context(), id, nil) user, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "User not found") return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
@@ -68,12 +68,12 @@ func (s userService) GetOne(c *fiber.Ctx, id uint) (*model.User, error) {
return user, nil return user, nil
} }
func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.User, error) { func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.User, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
createBody := &model.User{ createBody := &entity.User{
Name: req.Name, Name: req.Name,
} }
@@ -85,7 +85,7 @@ func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.Us
return createBody, nil return createBody, nil
} }
func (s userService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*model.User, error) { func (s userService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.User, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
@@ -96,6 +96,10 @@ func (s userService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*
updateBody["name"] = *req.Name updateBody["name"] = *req.Name
} }
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "User not found") return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
+3 -1
View File
@@ -1,8 +1,9 @@
package route package route
import ( import (
"gitlab.com/mbugroup/lti-api.git/internal/common/validation"
"gitlab.com/mbugroup/lti-api.git/internal/modules" "gitlab.com/mbugroup/lti-api.git/internal/modules"
"gitlab.com/mbugroup/lti-api.git/internal/validation" trim "gitlab.com/mbugroup/lti-api.git/internal/middleware/trim"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
@@ -15,6 +16,7 @@ import (
func Routes(app *fiber.App, db *gorm.DB) { func Routes(app *fiber.App, db *gorm.DB) {
validate := validation.Validator() validate := validation.Validator()
api := app.Group("/api") api := app.Group("/api")
api.Use(trim.JSONBody())
// root modules di sini // root modules di sini
allModules := []modules.Module{ allModules := []modules.Module{
+39
View File
@@ -9,6 +9,29 @@ const (
FlagIsActive FlagType = "IS_ACTIVE" FlagIsActive FlagType = "IS_ACTIVE"
) )
// -------------------------------------------------------------------
// WarehouseType
// -------------------------------------------------------------------
type WarehouseType string
const (
WarehouseTypeArea WarehouseType = "AREA"
WarehouseTypeLokasi WarehouseType = "LOKASI"
WarehouseTypeKandang WarehouseType = "KANDANG"
)
// -------------------------------------------------------------------
// WarehouseType
// -------------------------------------------------------------------
type CustomerSupplierType string
const (
CustomerSupplierTypeBisnis CustomerSupplierType = "BISNIS"
CustomerSupplierTypeIndividual CustomerSupplierType = "INDIVIDUAL"
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Validators // Validators
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -21,6 +44,22 @@ func IsValidFlagType(v string) bool {
return false return false
} }
func IsValidWarehouseType(v string) bool {
switch WarehouseType(v) {
case WarehouseTypeArea, WarehouseTypeLokasi, WarehouseTypeKandang:
return true
}
return false
}
func IsValidCustomerSupplierType(v string) bool {
switch CustomerSupplierType(v) {
case CustomerSupplierTypeBisnis, CustomerSupplierTypeIndividual:
return true
}
return false
}
// example use // example use
/** /**
+1 -1
View File
@@ -3,8 +3,8 @@ package utils
import ( import (
"errors" "errors"
"gitlab.com/mbugroup/lti-api.git/internal/common/validation"
"gitlab.com/mbugroup/lti-api.git/internal/response" "gitlab.com/mbugroup/lti-api.git/internal/response"
"gitlab.com/mbugroup/lti-api.git/internal/validation"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
+13
View File
@@ -0,0 +1,13 @@
package utils
import "strings"
// NormalizeTrim returns the input string without leading/trailing whitespace.
func NormalizeTrim(value string) string {
return strings.TrimSpace(value)
}
// NormalizeUpper returns the trimmed, upper-case version of value.
func NormalizeUpper(value string) string {
return strings.ToUpper(NormalizeTrim(value))
}
+27
View File
@@ -0,0 +1,27 @@
package test
import (
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestAreaIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
t.Run("create area trims name", func(t *testing.T) {
areaID := createArea(t, app, " Area Trim ")
if got := fetchAreaName(t, db, areaID); got != "Area Trim" {
t.Fatalf("expected trimmed name, got %q", got)
}
})
t.Run("duplicate area returns conflict", func(t *testing.T) {
createArea(t, app, "Duplicate Area")
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/areas", map[string]any{"name": "Duplicate Area"})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 conflict, got %d: %s", resp.StatusCode, string(body))
}
})
}
@@ -0,0 +1,189 @@
package test
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
func TestCustomerIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
t.Run("creating customer without existing pic fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{
"name": "Invalid Customer",
"pic_id": 9999,
"type": utils.CustomerSupplierTypeBisnis,
"address": "Somewhere",
"phone": "0800000000",
"email": "invalid@example.com",
"account_number": "ACC-INVALID",
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when pic is missing, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("creating customer with invalid type fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{
"name": "Invalid Type",
"pic_id": 1,
"type": "UNKNOWN",
"address": "Somewhere",
"phone": "081234567891",
"email": "invalid-type@example.com",
"account_number": "ACC-INVALID-TYPE",
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when type is invalid, got %d: %s", resp.StatusCode, string(body))
}
})
const customerName = "Customer Alpha"
var customerID uint
t.Run("creating customer succeeds", func(t *testing.T) {
customerID = createCustomer(t, app, customerName, 1)
customer := fetchCustomer(t, db, customerID)
if customer.Name != customerName {
t.Fatalf("expected name %q, got %q", customerName, customer.Name)
}
if customer.PicId != 1 {
t.Fatalf("expected pic id 1, got %d", customer.PicId)
}
if customer.Type != string(utils.CustomerSupplierTypeBisnis) {
t.Fatalf("expected type %q, got %q", utils.CustomerSupplierTypeBisnis, customer.Type)
}
})
t.Run("creating customer with duplicate name fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{
"name": customerName,
"pic_id": 1,
"type": utils.CustomerSupplierTypeBisnis,
"address": "Duplicate address",
"phone": "0811111111",
"email": "duplicate@example.com",
"account_number": "ACC-DUP",
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when creating duplicate customer, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("getting existing customer succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching customer, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Id uint `json:"id"`
Name string `json:"name"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse customer response: %v", err)
}
if payload.Data.Id != customerID {
t.Fatalf("expected id %d, got %d", customerID, payload.Data.Id)
}
if payload.Data.Name != customerName {
t.Fatalf("expected name %q, got %q", customerName, payload.Data.Name)
}
})
const updatedName = "Customer Gamma"
t.Run("updating customer name succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{
"name": updatedName,
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating customer, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Name string `json:"name"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse update response: %v", err)
}
if payload.Data.Name != updatedName {
t.Fatalf("expected updated name %q, got %q", updatedName, payload.Data.Name)
}
customer := fetchCustomer(t, db, customerID)
if customer.Name != updatedName {
t.Fatalf("expected persisted name %q, got %q", updatedName, customer.Name)
}
})
t.Run("updating customer type succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{
"type": utils.CustomerSupplierTypeIndividual,
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating customer type, got %d: %s", resp.StatusCode, string(body))
}
customer := fetchCustomer(t, db, customerID)
if customer.Type != string(utils.CustomerSupplierTypeIndividual) {
t.Fatalf("expected persisted type %q, got %q", utils.CustomerSupplierTypeIndividual, customer.Type)
}
})
t.Run("updating customer with invalid type fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{
"type": "random-type",
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating with invalid type, got %d: %s", resp.StatusCode, string(body))
}
customer := fetchCustomer(t, db, customerID)
if customer.Type != string(utils.CustomerSupplierTypeIndividual) {
t.Fatalf("expected type to remain %q after invalid update, got %q", utils.CustomerSupplierTypeIndividual, customer.Type)
}
})
t.Run("updating non existent customer returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, "/api/master-data/customers/9999", map[string]any{
"name": "Does Not Matter",
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when updating missing customer, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("deleting customer succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting customer, got %d: %s", resp.StatusCode, string(body))
}
var customer entities.Customer
if err := db.First(&customer, customerID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("expected customer to be deleted, got error %v", err)
}
})
t.Run("deleting non existent customer returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when deleting missing customer, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("getting deleted customer returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when fetching deleted customer, got %d: %s", resp.StatusCode, string(body))
}
})
}
@@ -0,0 +1,36 @@
package test
import (
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestKandangIntegration(t *testing.T) {
app, _ := setupIntegrationApp(t)
areaID := createArea(t, app, "Area Kandang")
locationID := createLocation(t, app, "Location For Kandang", "Address", areaID)
t.Run("create kandang success", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{
"name": "Kandang OK",
"location_id": locationID,
"pic_id": 1,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("create kandang with unknown location fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{
"name": "Kandang Fail",
"location_id": 999,
"pic_id": 1,
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404, got %d: %s", resp.StatusCode, string(body))
}
})
}
@@ -0,0 +1,35 @@
package test
import (
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestLocationIntegration(t *testing.T) {
app, _ := setupIntegrationApp(t)
t.Run("creating location without existing area fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/locations", map[string]any{
"name": "Loc A",
"address": "Address",
"area_id": 999, // non-existent
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("creating location succeeds", func(t *testing.T) {
areaID := createArea(t, app, "Area For Location")
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/locations", map[string]any{
"name": "Location OK",
"address": "Addr",
"area_id": areaID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
})
}
+188
View File
@@ -0,0 +1,188 @@
package test
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"github.com/glebarez/sqlite"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/route"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
func setupIntegrationApp(t *testing.T) (*fiber.App, *gorm.DB) {
t.Helper()
dir := t.TempDir()
dsn := filepath.Join(dir, "integration.db")
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatalf("failed to open sqlite database: %v", err)
}
if err := db.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
t.Fatalf("failed to enable foreign keys: %v", err)
}
if err := db.AutoMigrate(
&entities.User{},
&entities.Area{},
&entities.Location{},
&entities.Kandang{},
&entities.Warehouse{},
&entities.Uom{},
&entities.Customer{},
); err != nil {
t.Fatalf("auto migrate failed: %v", err)
}
seedUser := entities.User{
Id: 1,
IdUser: 1001,
Email: "tester@example.com",
Name: "Tester",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := db.Create(&seedUser).Error; err != nil {
t.Fatalf("failed to seed user: %v", err)
}
app := fiber.New()
route.Routes(app, db)
return app, db
}
func doJSONRequest(t *testing.T, app *fiber.App, method, path string, payload any) (*http.Response, []byte) {
t.Helper()
var body io.Reader
if payload != nil {
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(payload); err != nil {
t.Fatalf("failed to encode payload: %v", err)
}
body = buf
}
req := httptest.NewRequest(method, path, body)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("failed to read response body: %v", err)
}
return resp, data
}
func parseID(t *testing.T, body []byte) uint {
t.Helper()
var resp struct {
Data struct {
Id uint `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal(body, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
return resp.Data.Id
}
func createArea(t *testing.T, app *fiber.App, name string) uint {
t.Helper()
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/areas", map[string]any{"name": name})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating area, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func createLocation(t *testing.T, app *fiber.App, name, address string, areaID uint) uint {
t.Helper()
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/locations", map[string]any{
"name": name,
"address": address,
"area_id": areaID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating location, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func createKandang(t *testing.T, app *fiber.App, name string, locationID, picID uint) uint {
t.Helper()
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{
"name": name,
"location_id": locationID,
"pic_id": picID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating kandang, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func createCustomer(t *testing.T, app *fiber.App, name string, picID uint) uint {
t.Helper()
identifier := strings.ToLower(strings.ReplaceAll(name, " ", "_"))
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{
"name": name,
"pic_id": picID,
"type": utils.CustomerSupplierTypeBisnis,
"address": "Customer address",
"phone": "081234567890",
"email": fmt.Sprintf("%s@example.com", identifier),
"account_number": fmt.Sprintf("ACC-%s", identifier),
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating customer, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func fetchCustomer(t *testing.T, db *gorm.DB, id uint) entities.Customer {
t.Helper()
var customer entities.Customer
if err := db.Preload("Pic").Preload("CreatedUser").First(&customer, id).Error; err != nil {
t.Fatalf("failed to fetch customer: %v", err)
}
return customer
}
func fetchAreaName(t *testing.T, db *gorm.DB, id uint) string {
t.Helper()
var area entities.Area
if err := db.First(&area, id).Error; err != nil {
t.Fatalf("failed to fetch area: %v", err)
}
return area.Name
}
func fetchWarehouse(t *testing.T, db *gorm.DB, id uint) entities.Warehouse {
t.Helper()
var wh entities.Warehouse
if err := db.First(&wh, id).Error; err != nil {
t.Fatalf("failed to fetch warehouse: %v", err)
}
return wh
}
@@ -0,0 +1,96 @@
package test
import (
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestWarehouseIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
areaID := createArea(t, app, "Warehouse Area")
locationID := createLocation(t, app, "Location WH", "Addr", areaID)
kandangID := createKandang(t, app, "Kandang WH", locationID, 1)
t.Run("type AREA only needs area_id", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Area",
"type": "AREA",
"area_id": areaID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("type AREA rejects location_id", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Area Invalid",
"type": "AREA",
"area_id": areaID,
"location_id": locationID,
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("type LOKASI requires location_id", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Lokasi Fail",
"type": "LOKASI",
"area_id": areaID,
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("type LOKASI success", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Lokasi",
"type": "LOKASI",
"area_id": areaID,
"location_id": locationID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
whID := parseID(t, body)
wh := fetchWarehouse(t, db, whID)
if wh.LocationId == nil || *wh.LocationId != locationID {
t.Fatalf("expected location_id %d, got %v", locationID, wh.LocationId)
}
})
t.Run("type KANDANG requires all ids", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Kandang Fail",
"type": "KANDANG",
"area_id": areaID,
"location_id": locationID,
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("type KANDANG success", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Kandang",
"type": "KANDANG",
"area_id": areaID,
"location_id": locationID,
"kandang_id": kandangID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
whID := parseID(t, body)
wh := fetchWarehouse(t, db, whID)
if wh.KandangId == nil || *wh.KandangId != kandangID {
t.Fatalf("expected kandang_id %d, got %v", kandangID, wh.KandangId)
}
})
}
+5 -4
View File
@@ -44,10 +44,11 @@ func main() {
TplName string TplName string
}{ }{
{ {
TplPath: "tools/templates/model.tmpl", TplPath: "tools/templates/entity.tmpl",
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "models"), // Centralize entities at internal/entities
OutSuffix: ".model.go", OutDir: filepath.Join("internal", "entities"),
TplName: "model", OutSuffix: ".go",
TplName: "entity",
}, },
{ {
TplPath: "tools/templates/validation.tmpl", TplPath: "tools/templates/validation.tmpl",
+20 -11
View File
@@ -3,7 +3,8 @@
import ( import (
"time" "time"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/models" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
// === DTO Structs === // === DTO Structs ===
@@ -15,6 +16,7 @@ type {{Pascal .Entity}}BaseDTO struct {
type {{Pascal .Entity}}ListDTO struct { type {{Pascal .Entity}}ListDTO struct {
{{Pascal .Entity}}BaseDTO {{Pascal .Entity}}BaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
@@ -25,24 +27,31 @@ type {{Pascal .Entity}}DetailDTO struct {
// === Mapper Functions === // === Mapper Functions ===
func To{{Pascal .Entity}}BaseDTO(m model.{{Pascal .Entity}}) {{Pascal .Entity}}BaseDTO { func To{{Pascal .Entity}}BaseDTO(e entity.{{Pascal .Entity}}) {{Pascal .Entity}}BaseDTO {
return {{Pascal .Entity}}BaseDTO{ return {{Pascal .Entity}}BaseDTO{
Id: m.Id, Id: e.Id,
Name: m.Name, Name: e.Name,
} }
} }
func To{{Pascal .Entity}}ListDTO(m model.{{Pascal .Entity}}) {{Pascal .Entity}}ListDTO { func To{{Pascal .Entity}}ListDTO(e entity.{{Pascal .Entity}}) {{Pascal .Entity}}ListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return {{Pascal .Entity}}ListDTO{ return {{Pascal .Entity}}ListDTO{
{{Pascal .Entity}}BaseDTO: To{{Pascal .Entity}}BaseDTO(m), {{Pascal .Entity}}BaseDTO: To{{Pascal .Entity}}BaseDTO(e),
CreatedAt: m.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: m.UpdatedAt, UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
} }
} }
func To{{Pascal .Entity}}ListDTOs(m []model.{{Pascal .Entity}}) []{{Pascal .Entity}}ListDTO { func To{{Pascal .Entity}}ListDTOs(e []entity.{{Pascal .Entity}}) []{{Pascal .Entity}}ListDTO {
result := make([]{{Pascal .Entity}}ListDTO, len(m)) result := make([]{{Pascal .Entity}}ListDTO, len(e))
for i, r := range m { for i, r := range e {
result[i] = To{{Pascal .Entity}}ListDTO(r) result[i] = To{{Pascal .Entity}}ListDTO(r)
} }
return result return result
+19
View File
@@ -0,0 +1,19 @@
{{define "entity"}}package entities
import (
"time"
"gorm.io/gorm"
)
type {{Pascal .Entity}} struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not 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"`
}
{{end}}
-17
View File
@@ -1,17 +0,0 @@
{{define "model"}}package model
import (
"time"
)
type {{Pascal .Entity}} struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
CreatedBy int64 `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser model.User `gorm:"foreignKey:CreatedBy;references:Id"`
}
{{end}}
+5 -5
View File
@@ -1,22 +1,22 @@
{{define "repository"}}package repository {{define "repository"}}package repository
import ( import (
model "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/models" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm" "gorm.io/gorm"
) )
type {{Pascal .Entity}}Repository interface { type {{Pascal .Entity}}Repository interface {
repository.BaseRepository[model.{{Pascal .Entity}}] repository.BaseRepository[entity.{{Pascal .Entity}}]
} }
type {{Pascal .Entity}}RepositoryImpl struct { type {{Pascal .Entity}}RepositoryImpl struct {
*repository.BaseRepositoryImpl[model.{{Pascal .Entity}}] *repository.BaseRepositoryImpl[entity.{{Pascal .Entity}}]
} }
func New{{Pascal .Entity}}Repository(db *gorm.DB) {{Pascal .Entity}}Repository { func New{{Pascal .Entity}}Repository(db *gorm.DB) {{Pascal .Entity}}Repository {
return &{{Pascal .Entity}}RepositoryImpl{ return &{{Pascal .Entity}}RepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[model.{{Pascal .Entity}}](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.{{Pascal .Entity}}](db),
} }
} }
{{end}} {{end}}
+21 -11
View File
@@ -3,7 +3,7 @@
import ( import (
"errors" "errors"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/models" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -15,10 +15,10 @@ import (
) )
type {{Pascal .Entity}}Service interface { type {{Pascal .Entity}}Service interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]model.{{Pascal .Entity}}, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.{{Pascal .Entity}}, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*model.{{Pascal .Entity}}, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.{{Pascal .Entity}}, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*model.{{Pascal .Entity}}, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.{{Pascal .Entity}}, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*model.{{Pascal .Entity}}, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.{{Pascal .Entity}}, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
} }
@@ -35,7 +35,12 @@ func New{{Pascal .Entity}}Service(repo repository.{{Pascal .Entity}}Repository,
Repository: repo, Repository: repo,
} }
} }
func (s {{Camel .Entity}}Service) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.{{Pascal .Entity}}, int64, error) {
func (s {{Camel .Entity}}Service) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s {{Camel .Entity}}Service) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.{{Pascal .Entity}}, int64, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -43,6 +48,7 @@ func (s {{Camel .Entity}}Service) GetAll(c *fiber.Ctx, params *validation.Query)
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
{{Camel .Entity}}s, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { {{Camel .Entity}}s, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" { if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%") return db.Where("name LIKE ?", "%"+params.Search+"%")
} }
@@ -56,8 +62,8 @@ func (s {{Camel .Entity}}Service) GetAll(c *fiber.Ctx, params *validation.Query)
return {{Camel .Entity}}s, total, nil return {{Camel .Entity}}s, total, nil
} }
func (s {{Camel .Entity}}Service) GetOne(c *fiber.Ctx, id uint) (*model.{{Pascal .Entity}}, error) { func (s {{Camel .Entity}}Service) GetOne(c *fiber.Ctx, id uint) (*entity.{{Pascal .Entity}}, error) {
{{Camel .Entity}}, err := s.Repository.GetByID(c.Context(), id, nil) {{Camel .Entity}}, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "{{Pascal .Entity}} not found") return nil, fiber.NewError(fiber.StatusNotFound, "{{Pascal .Entity}} not found")
} }
@@ -68,12 +74,12 @@ func (s {{Camel .Entity}}Service) GetOne(c *fiber.Ctx, id uint) (*model.{{Pascal
return {{Camel .Entity}}, nil return {{Camel .Entity}}, nil
} }
func (s *{{Camel .Entity}}Service) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.{{Pascal .Entity}}, error) { func (s *{{Camel .Entity}}Service) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.{{Pascal .Entity}}, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
createBody := &model.{{Pascal .Entity}}{ createBody := &entity.{{Pascal .Entity}}{
Name: req.Name, Name: req.Name,
} }
@@ -85,7 +91,7 @@ func (s *{{Camel .Entity}}Service) CreateOne(c *fiber.Ctx, req *validation.Creat
return createBody, nil return createBody, nil
} }
func (s {{Camel .Entity}}Service) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*model.{{Pascal .Entity}}, error) { func (s {{Camel .Entity}}Service) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.{{Pascal .Entity}}, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
@@ -96,6 +102,10 @@ func (s {{Camel .Entity}}Service) UpdateOne(c *fiber.Ctx, req *validation.Update
updateBody["name"] = *req.Name updateBody["name"] = *req.Name
} }
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "{{Pascal .Entity}} not found") return nil, fiber.NewError(fiber.StatusNotFound, "{{Pascal .Entity}} not found")