Merge branch 'feat/BE/US-33/master-data-management' into 'development'

[FEAT/BE][US#33/TASK#36,37,38,39] Finish Master Data Management Api

See merge request mbugroup/lti-api!6
This commit is contained in:
Adnan Zahir
2025-10-03 21:20:04 +07:00
148 changed files with 10646 additions and 213 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
-2
View File
@@ -60,11 +60,9 @@ func setupFiberApp() *fiber.App {
app := fiber.New(config.FiberConfig()) app := fiber.New(config.FiberConfig())
// Middleware setup // Middleware setup
app.Use("/api/auth", middleware.LimiterConfig())
app.Use(middleware.LoggerConfig()) app.Use(middleware.LoggerConfig())
app.Use(helmet.New()) app.Use(helmet.New())
app.Use(compress.New()) app.Use(compress.New())
app.Use(cors.New())
app.Use(middleware.RecoverConfig()) app.Use(middleware.RecoverConfig())
origins := "*" origins := "*"
+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,36 @@
DROP TABLE IF EXISTS fcr_standards; DROP TABLE IF EXISTS fcr_standards;
DROP INDEX IF EXISTS suppliers_name_unique;
DROP TABLE IF EXISTS product_suppliers;
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 INDEX IF EXISTS flags_flagable_lookup;
DROP INDEX IF EXISTS flags_unique_flagable;
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 nonstock_suppliers;
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;
@@ -19,20 +19,23 @@ CREATE TABLE flags (
flagable_id BIGINT NOT NULL, flagable_id BIGINT NOT NULL,
flagable_type VARCHAR(50) NOT NULL, flagable_type VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
deleted_at TIMESTAMPTZ
); );
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
-- PRODUCT CATEGORIES -- PRODUCT CATEGORIES
CREATE TABLE product_categories ( CREATE TABLE product_categories (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
code VARCHAR(3) NOT NULL, code VARCHAR(10) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT 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,27 +45,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
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
brand VARCHAR NOT NULL,
sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms(id),
product_category_id BIGINT NOT NULL REFERENCES product_categories(id),
product_price NUMERIC(15,3) NOT NULL,
selling_price NUMERIC(15,3),
tax NUMERIC(15,3),
expiry_period INT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id)
);
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
-- BANKS -- BANKS
CREATE TABLE banks ( CREATE TABLE banks (
@@ -74,8 +59,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 +70,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 +129,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 +152,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,
@@ -180,6 +174,7 @@ CREATE TABLE suppliers (
alias VARCHAR(5) NOT NULL, alias VARCHAR(5) NOT NULL,
pic VARCHAR NOT NULL, pic VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
category VARCHAR(20) NOT NULL,
hatchery VARCHAR, hatchery VARCHAR,
phone VARCHAR(20) NOT NULL, phone VARCHAR(20) NOT NULL,
email VARCHAR NOT NULL, email VARCHAR NOT NULL,
@@ -191,7 +186,42 @@ 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;
CREATE TABLE nonstock_suppliers (
nonstock_id BIGINT NOT NULL REFERENCES nonstocks(id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (nonstock_id, supplier_id)
);
-- PRODUCTS
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
brand VARCHAR NOT NULL,
sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_category_id BIGINT NOT NULL REFERENCES product_categories(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_price NUMERIC(15,3) NOT NULL,
selling_price NUMERIC(15,3),
tax NUMERIC(15,3),
expiry_period INT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
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 TABLE product_suppliers (
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (product_id, supplier_id)
); );
-- PROJECTS -- PROJECTS
@@ -200,5 +230,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
); );
+759 -8
View File
@@ -1,26 +1,777 @@
package seed package seed
import ( import (
"errors"
"fmt" "fmt"
"strings"
"time"
mUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm" "gorm.io/gorm"
) )
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, err := seedUsers(tx)
user := mUser.User{ if err != nil {
Email: "admin@mbugroup.id", return err
IdUser: 1,
Name: "Super Admin",
} }
if err := tx.Where("email = ?", user.Email).FirstOrCreate(&user).Error; err != nil { adminID := users["admin"]
uoms, err := seedUoms(tx, adminID)
if err != nil {
return err return err
} }
fmt.Println("✅ Seeder successfully") areas, err := seedAreas(tx, adminID)
if err != nil {
return err
}
locations, err := seedLocations(tx, adminID, areas)
if err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users)
if err != nil {
return err
}
if err := seedWarehouses(tx, adminID, areas, locations, kandangs); err != nil {
return err
}
productCategories, err := seedProductCategories(tx, adminID)
if err != nil {
return err
}
suppliers, err := seedSuppliers(tx, adminID)
if err != nil {
return err
}
if err := seedCustomers(tx, adminID, users); err != nil {
return err
}
if err := seedFcr(tx, adminID); err != nil {
return err
}
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
return err
}
if err := seedNonstocks(tx, adminID, uoms, suppliers); err != nil {
return err
}
if err := seedBanks(tx, adminID); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed")
return nil return nil
}) })
} }
func seedUsers(tx *gorm.DB) (map[string]uint, error) {
seeds := []struct {
Key string
Data entity.User
}{
{
Key: "admin",
Data: entity.User{Email: "admin@mbugroup.id", IdUser: 1, Name: "Super Admin"},
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
var user entity.User
err := tx.Where("email = ?", seed.Data.Email).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
user = seed.Data
if err := tx.Create(&user).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Key] = user.Id
}
return result, nil
}
func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Kilogram", "Gram", "Liter", "Unit", "Ekor"}
result := make(map[string]uint, len(names))
for _, name := range names {
var uom entity.Uom
err := tx.Where("name = ?", name).First(&uom).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
uom = entity.Uom{Name: name, CreatedBy: createdBy}
if err := tx.Create(&uom).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[name] = uom.Id
}
return result, nil
}
func seedAreas(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Priangan", "Banten"}
result := make(map[string]uint, len(names))
for _, name := range names {
var area entity.Area
err := tx.Where("name = ?", name).First(&area).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
area = entity.Area{Name: name, CreatedBy: createdBy}
if err := tx.Create(&area).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[name] = area.Id
}
return result, nil
}
func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Address string
Area string
}{
{"Singaparna", "Tasik", "Priangan"},
{"Cikaum", "Cikaum", "Banten"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
areaID, ok := areas[seed.Area]
if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area)
}
var loc entity.Location
err := tx.Where("name = ?", seed.Name).First(&loc).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
loc = entity.Location{
Name: seed.Name,
Address: seed.Address,
AreaId: areaID,
CreatedBy: createdBy,
}
if err := tx.Create(&loc).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = loc.Id
}
return result, nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Location string
PicKey string
}{
{"Singaparna 1", "Singaparna", "admin"},
{"Singaparna 2", "Singaparna", "admin"},
{"Cikaum 1", "Cikaum", "admin"},
{"Cikaum 2", "Cikaum", "admin"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
locID, ok := locations[seed.Location]
if !ok {
return nil, fmt.Errorf("location %s not seeded", seed.Location)
}
picID, ok := users[seed.PicKey]
if !ok {
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
}
var kandang entity.Kandang
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
kandang = entity.Kandang{
Name: seed.Name,
LocationId: locID,
PicId: picID,
CreatedBy: createdBy,
}
if err := tx.Create(&kandang).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = kandang.Id
}
return result, nil
}
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct {
Name string
Type string
Area string
Location *string
Kandang *string
}{
{Name: "Gudang Priangan", Type: string(utils.WarehouseTypeArea), Area: "Priangan"},
{Name: "Gudang Singaparna", Type: string(utils.WarehouseTypeLokasi), Area: "Priangan", Location: strPtr("Singaparna")},
{Name: "Gudang Singaparna 1", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 1")},
{Name: "Gudang Singaparna 2", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 2")},
{Name: "Gudang Banten", Type: string(utils.WarehouseTypeArea), Area: "Banten"},
{Name: "Gudang Cikaum", Type: string(utils.WarehouseTypeLokasi), Area: "Banten", Location: strPtr("Cikaum")},
{Name: "Gudang Cikaum 1", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 1")},
{Name: "Gudang Cikaum 2", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 2")},
}
for _, seed := range seeds {
areaID, ok := areas[seed.Area]
if !ok {
return fmt.Errorf("area %s not seeded", seed.Area)
}
var warehouse entity.Warehouse
err := tx.Where("name = ?", seed.Name).First(&warehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
warehouse = entity.Warehouse{
Name: seed.Name,
Type: seed.Type,
AreaId: areaID,
CreatedBy: createdBy,
}
} else if err != nil {
return err
}
if seed.Location != nil {
locID, ok := locations[*seed.Location]
if !ok {
return fmt.Errorf("location %s not seeded", *seed.Location)
}
warehouse.LocationId = uintPtr(locID)
}
if seed.Kandang != nil {
kandangID, ok := kandangs[*seed.Kandang]
if !ok {
return fmt.Errorf("kandang %s not seeded", *seed.Kandang)
}
warehouse.KandangId = uintPtr(kandangID)
}
if warehouse.Id == 0 {
if err := tx.Create(&warehouse).Error; err != nil {
return err
}
} else {
if err := tx.Model(&entity.Warehouse{}).Where("id = ?", warehouse.Id).Updates(map[string]any{
"type": warehouse.Type,
"area_id": warehouse.AreaId,
"location_id": warehouse.LocationId,
"kandang_id": warehouse.KandangId,
}).Error; err != nil {
return err
}
}
}
return nil
}
func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct {
Name string
Code string
}{
{"Bahan Baku", "RAW"},
{"Day Old Chick", "DOC"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
var category entity.ProductCategory
err := tx.Where("name = ?", seed.Name).First(&category).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
category = entity.ProductCategory{Name: seed.Name, Code: seed.Code, CreatedBy: createdBy}
if err := tx.Create(&category).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.ProductCategory{}).Where("id = ?", category.Id).Updates(map[string]any{
"code": seed.Code,
}).Error; err != nil {
return nil, err
}
}
result[seed.Name] = category.Id
}
return result, nil
}
func seedSuppliers(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct {
Name string
Alias string
Category string
Email string
Phone string
Address string
}{
{"PT CHAROEN POKPHAND INDONESIA Tbk", "CPI", string(utils.SupplierCategorySapronak), "cpi@gmail.com", "081200000001", "Jl. Pakan 1, Bekasi"},
{"BOP Vendor", "BOP", string(utils.SupplierCategoryBOP), "bop@gmail.com", "081200000002", "Jl. Veteriner 3, Bogor"},
{"Ekspedisi", "EKS", string(utils.SupplierCategoryBOP), "bop@gmail.com", "081200000002", "Jl. Veteriner 3, Bogor"},
}
result := make(map[string]uint, len(seeds))
for idx, seed := range seeds {
var supplier entity.Supplier
err := tx.Where("name = ?", seed.Name).First(&supplier).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
supplier = entity.Supplier{
Name: seed.Name,
Alias: seed.Alias,
Pic: "John Doe",
Type: string(utils.CustomerSupplierTypeBisnis),
Category: seed.Category,
Phone: seed.Phone,
Email: seed.Email,
Address: seed.Address,
DueDate: 30,
CreatedBy: createdBy,
AccountNumber: strPtr(fmt.Sprintf("%03d", idx+1)),
}
if err := tx.Create(&supplier).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = supplier.Id
}
return result, nil
}
func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
seeds := []struct {
Name string
PicKey string
Address string
Phone string
Email string
}{
{"Abdul Azis", "admin", "Jl. Raya Utama 1, Bekasi", "082100000001", "abdul.azis@gmail.com"},
}
for idx, seed := range seeds {
picID, ok := users[seed.PicKey]
if !ok {
return fmt.Errorf("user %s not seeded", seed.PicKey)
}
var customer entity.Customer
err := tx.Where("name = ?", seed.Name).First(&customer).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
customer = entity.Customer{
Name: seed.Name,
PicId: picID,
Type: string(utils.CustomerSupplierTypeBisnis),
Address: seed.Address,
Phone: seed.Phone,
Email: seed.Email,
AccountNumber: *strPtr(fmt.Sprintf("%03d", idx+1)),
CreatedBy: createdBy,
}
if err := tx.Create(&customer).Error; err != nil {
return err
}
} else if err != nil {
return err
}
}
return nil
}
func seedFcr(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
Name string
Standards []struct {
Weight float64
FcrNumber float64
Mortality float64
}
}{
{
Name: "FCR Layer",
Standards: []struct {
Weight float64
FcrNumber float64
Mortality float64
}{
{Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0},
{Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5},
},
},
}
for _, seed := range seeds {
var fcr entity.Fcr
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
if err := tx.Create(&fcr).Error; err != nil {
return err
}
} else if err != nil {
return err
}
for _, std := range seed.Standards {
var standard entity.FcrStandard
err := tx.Where("fcr_id = ? AND weight = ?", fcr.Id, std.Weight).First(&standard).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
standard = entity.FcrStandard{
FcrID: fcr.Id,
Weight: std.Weight,
FcrNumber: std.FcrNumber,
Mortality: std.Mortality,
}
if err := tx.Create(&standard).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
"fcr_number": std.FcrNumber,
"mortality": std.Mortality,
}).Error; err != nil {
return err
}
}
}
}
return nil
}
func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
seeds := []struct {
Name string
Brand string
Sku string
Uom string
Category string
Price float64
Selling *float64
Tax *float64
Expiry *int
Suppliers []string
Flags []utils.FlagType
}{
{
Name: "DOC Broiler",
Brand: "MBU Broiler",
Sku: "BRO0001",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 7500,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagDOC},
},
{
Name: "281 SPECIAL STARTER",
Brand: "281 STARTER",
Sku: "281",
Uom: "Kilogram",
Category: "Bahan Baku",
Price: 7850,
Expiry: intPtr(60),
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
},
}
for _, seed := range seeds {
uomID, ok := uoms[seed.Uom]
if !ok {
return fmt.Errorf("uom %s not seeded", seed.Uom)
}
categoryID, ok := categories[seed.Category]
if !ok {
return fmt.Errorf("product category %s not seeded", seed.Category)
}
var product entity.Product
err := tx.Where("name = ?", seed.Name).First(&product).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
selling := seed.Selling
tax := seed.Tax
product = entity.Product{
Name: seed.Name,
Brand: seed.Brand,
Sku: &seed.Sku,
UomId: uomID,
ProductCategoryId: categoryID,
ProductPrice: seed.Price,
SellingPrice: selling,
Tax: tax,
ExpiryPeriod: seed.Expiry,
CreatedBy: createdBy,
}
if err := tx.Create(&product).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
updates := map[string]any{
"brand": seed.Brand,
"uom_id": uomID,
"product_category_id": categoryID,
"product_price": seed.Price,
"selling_price": seed.Selling,
"tax": seed.Tax,
"expiry_period": seed.Expiry,
}
if seed.Sku != "" {
updates["sku"] = seed.Sku
}
if err := tx.Model(&entity.Product{}).Where("id = ?", product.Id).Updates(updates).Error; err != nil {
return err
}
}
for _, supplierName := range seed.Suppliers {
supplierID, ok := suppliers[supplierName]
if !ok {
return fmt.Errorf("supplier %s not seeded", supplierName)
}
var existing entity.ProductSupplier
err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.ProductSupplier{ProductID: product.Id, SupplierID: supplierID}
if err := tx.Create(&link).Error; err != nil {
return err
}
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
}
if err := seedFlags(tx, product.Id, entity.FlagableTypeProduct, seed.Flags); err != nil {
return err
}
}
return nil
}
func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error {
seeds := []struct {
Name string
Uom string
Suppliers []string
Flags []utils.FlagType
}{
{
Name: "Expedisi DOC",
Uom: "Ekor",
Suppliers: []string{"Ekspedisi"},
Flags: []utils.FlagType{utils.FlagEkspedisi},
},
{
Name: "Solar",
Uom: "Liter",
Suppliers: []string{"BOP Vendor"},
Flags: []utils.FlagType{},
},
}
for _, seed := range seeds {
uomID, ok := uoms[seed.Uom]
if !ok {
return fmt.Errorf("uom %s not seeded", seed.Uom)
}
var nonstock entity.Nonstock
err := tx.Where("name = ?", seed.Name).First(&nonstock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
nonstock = entity.Nonstock{
Name: seed.Name,
UomId: uomID,
CreatedBy: createdBy,
}
if err := tx.Create(&nonstock).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{
"uom_id": uomID,
}).Error; err != nil {
return err
}
}
for _, supplierName := range seed.Suppliers {
supplierID, ok := suppliers[supplierName]
if !ok {
return fmt.Errorf("supplier %s not seeded", supplierName)
}
var existing entity.NonstockSupplier
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.NonstockSupplier{NonstockID: nonstock.Id, SupplierID: supplierID}
if err := tx.Create(&link).Error; err != nil {
return err
}
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
}
if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil {
return err
}
}
return nil
}
func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error {
if len(flags) == 0 {
return nil
}
for _, flag := range flags {
name := strings.ToUpper(string(flag))
var existing entity.Flag
err := tx.Where("name = ? AND flagable_id = ? AND flagable_type = ?", name, flagableID, flagableType).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
record := entity.Flag{
Name: name,
FlagableID: flagableID,
FlagableType: flagableType,
}
if err := tx.Create(&record).Error; err != nil {
return err
}
} else if err != nil {
return err
}
}
return nil
}
func seedBanks(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
Name string
Alias string
Owner *string
AccountNumber string
}{
{
Name: "Bank Central Asia",
Alias: "BCA",
AccountNumber: "1234567890",
Owner: ptr("PT MBU Group"),
},
{
Name: "Bank Rakyat Indonesia",
Alias: "BRI",
AccountNumber: "9876543210",
Owner: ptr("PT MBU Group"),
},
{
Name: "Bank Mandiri",
Alias: "MAND",
AccountNumber: "1122334455",
Owner: ptr("PT MBU Group"),
},
}
for _, seed := range seeds {
var bank entity.Bank
err := tx.Where("name = ?", seed.Name).First(&bank).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
bank = entity.Bank{
Name: seed.Name,
Alias: seed.Alias,
Owner: seed.Owner,
AccountNumber: seed.AccountNumber,
CreatedBy: createdBy,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := tx.Create(&bank).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
// update data jika sudah ada
if err := tx.Model(&entity.Bank{}).Where("id = ?", bank.Id).Updates(map[string]any{
"alias": seed.Alias,
"owner": seed.Owner,
"account_number": seed.AccountNumber,
"updated_at": time.Now(),
}).Error; err != nil {
return err
}
}
}
return nil
}
func ptr[T any](v T) *T {
return &v
}
func strPtr(s string) *string {
return &s
}
func intPtr(v int) *int {
return &v
}
func uintPtr(v uint) *uint {
return &v
}
+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;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Locations []Location `gorm:"foreignKey:AreaId;references:Id"`
}
+21
View File
@@ -0,0 +1,21 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Bank struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
Alias string `gorm:"not null;size:5"`
Owner *string `gorm:""`
AccountNumber string `gorm:"not null;size:50"`
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"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Constant struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+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;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
PicId uint `gorm:"not null"`
Type string `gorm:"not null;size:50"`
Address string `gorm:"not null"`
Phone string `gorm:"not null;size:20"`
Email string `gorm:"not null"`
AccountNumber string `gorm:"not null;size:50"`
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"`
}
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Fcr struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Standards []FcrStandard `gorm:"foreignKey:FcrID;references:Id"`
}
+20
View File
@@ -0,0 +1,20 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type FcrStandard struct {
Id uint `gorm:"primaryKey"`
FcrID uint `gorm:"not null;index"`
Weight float64 `gorm:"type:numeric(15,3);not null"`
FcrNumber float64 `gorm:"type:numeric(15,3);not null"`
Mortality float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Fcr Fcr `gorm:"foreignKey:FcrID;references:Id"`
}
+17
View File
@@ -0,0 +1,17 @@
package entities
import "time"
const (
FlagableTypeProduct = "products"
FlagableTypeNonstock = "nonstocks"
)
type Flag struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:flags_unique_flagable"`
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"`
FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
+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;uniqueIndex:kandangs_name_unique,where:deleted_at IS 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;uniqueIndex:locations_name_unique,where:deleted_at IS 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"`
}
+22
View File
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Nonstock struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
UomId 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"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
Suppliers []Supplier `gorm:"many2many:nonstock_suppliers;joinForeignKey:NonstockID;joinReferences:SupplierID"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"`
}
+9
View File
@@ -0,0 +1,9 @@
package entities
import "time"
type NonstockSupplier struct {
NonstockID uint `gorm:"primaryKey"`
SupplierID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProductCategory struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"`
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"`
}
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Product struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
Brand string `gorm:"not null"`
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
UomId uint `gorm:"not null"`
ProductCategoryId uint `gorm:"not null"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Tax *float64 `gorm:"type:numeric(15,3)"`
ExpiryPeriod *int `gorm:""`
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"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
Suppliers []Supplier `gorm:"many2many:product_suppliers;joinForeignKey:ProductID;joinReferences:SupplierID"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
}
+9
View File
@@ -0,0 +1,9 @@
package entities
import "time"
type ProductSupplier struct {
ProductID uint `gorm:"primaryKey"`
SupplierID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Supplier struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
Alias string `gorm:"not null;size:5"`
Pic string `gorm:"not null"`
Type string `gorm:"not null;size:50"`
Category string `gorm:"not null;size:20"`
Hatchery *string `gorm:"size:255"`
Phone string `gorm:"not null;size:20"`
Email string `gorm:"not null"`
Address string `gorm:"not null"`
Npwp *string `gorm:"size:50"`
AccountNumber *string `gorm:"size:50"`
Balance float64 `gorm:"type:numeric(15,3);default:0"`
DueDate int `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"`
}
+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;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+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,25 @@
package controller
import (
service "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/services"
"github.com/gofiber/fiber/v2"
)
type ConstantController struct {
ConstantService service.ConstantService
}
func NewConstantController(constantService service.ConstantService) *ConstantController {
return &ConstantController{
ConstantService: constantService,
}
}
func (ctrl *ConstantController) GetAll(c *fiber.Ctx) error {
data, err := ctrl.ConstantService.GetAll(c)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.Status(fiber.StatusOK).JSON(data)
}
+20
View File
@@ -0,0 +1,20 @@
package constants
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rConstant "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/repositories"
sConstant "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/services"
)
type ConstantModule struct{}
func (ConstantModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
constantRepo := rConstant.NewConstantRepository(db)
constantService := sConstant.NewConstantService(constantRepo, validate)
ConstantRoutes(router, constantService)
}
@@ -0,0 +1,46 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
type ConstantRepository interface {
GetConstants() map[string]interface{}
}
type ConstantRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Constant]
}
func NewConstantRepository(db *gorm.DB) ConstantRepository {
return &ConstantRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Constant](db),
}
}
func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
flagList := make([]string, 0)
for f := range utils.AllFlagTypes() {
flagList = append(flagList, string(f))
}
return map[string]interface{}{
"flags": flagList,
"warehouse_types": []string{
"AREA",
"LOKASI",
"KANDANG",
},
"supplier_categories": []string{
"BOP",
"SAPRONAK",
},
"customer_supplier_types": []string{
"BISNIS",
"INDIVIDUAL",
},
}
}
+17
View File
@@ -0,0 +1,17 @@
package constants
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/controllers"
constant "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/services"
"github.com/gofiber/fiber/v2"
)
func ConstantRoutes(v1 fiber.Router, s constant.ConstantService) {
ctrl := controller.NewConstantController(s)
route := v1.Group("/constants")
route.Get("/", ctrl.GetAll)
}
@@ -0,0 +1,26 @@
package service
import (
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/repositories"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type ConstantService interface {
GetAll(ctx *fiber.Ctx) (map[string]interface{}, error)
}
type constantService struct {
Repository repository.ConstantRepository
}
func NewConstantService(repo repository.ConstantRepository, validate *validator.Validate) ConstantService {
return &constantService{
Repository: repo,
}
}
func (s constantService) GetAll(c *fiber.Ctx) (map[string]interface{}, error) {
return s.Repository.GetConstants(), nil
}
@@ -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,64 @@
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
}
func ToAreaDetailDTO(e entity.Area) AreaDetailDTO {
return AreaDetailDTO{
AreaListDTO: ToAreaListDTO(e),
}
}
+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.GetOne(c, createBody.Id)
}
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"`
}
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/banks/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type BankController struct {
BankService service.BankService
}
func NewBankController(bankService service.BankService) *BankController {
return &BankController{
BankService: bankService,
}
}
func (u *BankController) 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.BankService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.BankListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all banks successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToBankListDTOs(result),
})
}
func (u *BankController) 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.BankService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get bank successfully",
Data: dto.ToBankListDTO(*result),
})
}
func (u *BankController) 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.BankService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create bank successfully",
Data: dto.ToBankListDTO(*result),
})
}
func (u *BankController) 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.BankService.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 bank successfully",
Data: dto.ToBankListDTO(*result),
})
}
func (u *BankController) 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.BankService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete bank successfully",
})
}
@@ -0,0 +1,70 @@
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 BankBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Owner *string `json:"owner"`
AccountNumber string `json:"account_number"`
}
type BankListDTO struct {
BankBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type BankDetailDTO struct {
BankListDTO
}
// === Mapper Functions ===
func ToBankBaseDTO(e entity.Bank) BankBaseDTO {
return BankBaseDTO{
Id: e.Id,
Name: e.Name,
Alias: e.Alias,
Owner: e.Owner,
AccountNumber: e.AccountNumber,
}
}
func ToBankListDTO(e entity.Bank) BankListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return BankListDTO{
BankBaseDTO: ToBankBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToBankListDTOs(e []entity.Bank) []BankListDTO {
result := make([]BankListDTO, len(e))
for i, r := range e {
result[i] = ToBankListDTO(r)
}
return result
}
func ToBankDetailDTO(e entity.Bank) BankDetailDTO {
return BankDetailDTO{
BankListDTO: ToBankListDTO(e),
}
}
+26
View File
@@ -0,0 +1,26 @@
package banks
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rBank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/repositories"
sBank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type BankModule struct{}
func (BankModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
bankRepo := rBank.NewBankRepository(db)
userRepo := rUser.NewUserRepository(db)
bankService := sBank.NewBankService(bankRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
BankRoutes(router, userService, bankService)
}
@@ -0,0 +1,30 @@
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 BankRepository interface {
repository.BaseRepository[entity.Bank]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type BankRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Bank]
db *gorm.DB
}
func NewBankRepository(db *gorm.DB) BankRepository {
return &BankRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Bank](db),
db: db,
}
}
func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID)
}
+28
View File
@@ -0,0 +1,28 @@
package banks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/controllers"
bank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func BankRoutes(v1 fiber.Router, u user.UserService, s bank.BankService) {
ctrl := controller.NewBankController(s)
route := v1.Group("/banks")
// 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,156 @@
package service
import (
"errors"
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/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 BankService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Bank, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Bank, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Bank, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Bank, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type bankService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.BankRepository
}
func NewBankService(repo repository.BankRepository, validate *validator.Validate) BankService {
return &bankService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s bankService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s bankService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Bank, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
banks, 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 banks: %+v", err)
return nil, 0, err
}
return banks, total, nil
}
func (s bankService) GetOne(c *fiber.Ctx, id uint) (*entity.Bank, error) {
bank, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Bank not found")
}
if err != nil {
s.Log.Errorf("Failed get bank by id: %+v", err)
return nil, err
}
return bank, nil
}
func (s *bankService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Bank, 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 bank name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name))
}
createBody := &entity.Bank{
Name: req.Name,
Alias: req.Alias,
Owner: req.Owner,
AccountNumber: req.AccountNumber,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create bank: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s bankService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Bank, 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 bank name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.Alias != nil {
updateBody["alias"] = *req.Alias
}
if req.Owner != nil {
updateBody["owner"] = *req.Owner
}
if req.AccountNumber != nil {
updateBody["account_number"] = *req.AccountNumber
}
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, "Bank not found")
}
s.Log.Errorf("Failed to update bank: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s bankService) 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, "Bank not found")
}
s.Log.Errorf("Failed to delete bank: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Alias string `json:"alias" validate:"required_strict"`
Owner *string `json:"owner,omitempty" validate:"omitempty"`
AccountNumber string `json:"account_number" validate:"required_strict,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Alias *string `json:"alias,omitempty" validate:"omitempty"`
Owner *string `json:"owner,omitempty" validate:"omitempty"`
AccountNumber *string `json:"account_number,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,86 @@
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"`
Balance float64 `json:"balance"`
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
}
func ToCustomerDetailDTO(e entity.Customer) CustomerDetailDTO {
return CustomerDetailDTO{
CustomerListDTO: ToCustomerListDTO(e),
}
}
@@ -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,180 @@
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 s.GetOne(c, createBody.Id)
}
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 err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists}); err != nil {
return nil, err
}
if req.PicId != nil {
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"`
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/fcrs/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type FcrController struct {
FcrService service.FcrService
}
func NewFcrController(fcrService service.FcrService) *FcrController {
return &FcrController{
FcrService: fcrService,
}
}
func (u *FcrController) 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.FcrService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.FcrListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all fcrs successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToFcrListDTOs(result),
})
}
func (u *FcrController) 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.FcrService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get fcr successfully",
Data: dto.ToFcrDetailDTO(*result),
})
}
func (u *FcrController) 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.FcrService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create fcr successfully",
Data: dto.ToFcrDetailDTO(*result),
})
}
func (u *FcrController) 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.FcrService.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 fcr successfully",
Data: dto.ToFcrDetailDTO(*result),
})
}
func (u *FcrController) 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.FcrService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete fcr successfully",
})
}
@@ -0,0 +1,86 @@
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 FcrBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type FcrStandardDTO struct {
Id uint `json:"id"`
Weight float64 `json:"weight"`
FcrNumber float64 `json:"fcr_number"`
Mortality float64 `json:"mortality"`
}
type FcrListDTO struct {
FcrBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type FcrDetailDTO struct {
FcrListDTO
Standards []FcrStandardDTO `json:"fcr_standards"`
}
// === Mapper Functions ===
func ToFcrBaseDTO(e entity.Fcr) FcrBaseDTO {
return FcrBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToFcrListDTO(e entity.Fcr) FcrListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return FcrListDTO{
FcrBaseDTO: ToFcrBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToFcrListDTOs(e []entity.Fcr) []FcrListDTO {
result := make([]FcrListDTO, len(e))
for i, r := range e {
result[i] = ToFcrListDTO(r)
}
return result
}
func ToFcrDetailDTO(e entity.Fcr) FcrDetailDTO {
return FcrDetailDTO{
FcrListDTO: ToFcrListDTO(e),
Standards: ToFcrStandardDTOs(e.Standards),
}
}
func ToFcrStandardDTOs(standards []entity.FcrStandard) []FcrStandardDTO {
result := make([]FcrStandardDTO, len(standards))
for i, s := range standards {
result[i] = FcrStandardDTO{
Id: s.Id,
Weight: s.Weight,
FcrNumber: s.FcrNumber,
Mortality: s.Mortality,
}
}
return result
}
+25
View File
@@ -0,0 +1,25 @@
package fcrs
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rFcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/repositories"
sFcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type FcrModule struct{}
func (FcrModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
fcrRepo := rFcr.NewFcrRepository(db)
userRepo := rUser.NewUserRepository(db)
fcrService := sFcr.NewFcrService(fcrRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
FcrRoutes(router, userService, fcrService)
}
@@ -0,0 +1,90 @@
package repository
import (
"context"
"errors"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type FcrRepository interface {
repository.BaseRepository[entity.Fcr]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SyncStandardsDiff(ctx context.Context, tx *gorm.DB, fcrID uint, standards []entity.FcrStandard) error
}
type FcrRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Fcr]
}
func NewFcrRepository(db *gorm.DB) FcrRepository {
return &FcrRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Fcr](db),
}
}
func (r *FcrRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Fcr](ctx, r.DB(), name, excludeID)
}
func (r *FcrRepositoryImpl) SyncStandardsDiff(ctx context.Context, tx *gorm.DB, fcrID uint, standards []entity.FcrStandard) error {
db := tx
if db == nil {
db = r.DB()
}
var existing []entity.FcrStandard
if err := db.WithContext(ctx).
Where("fcr_id = ?", fcrID).
Find(&existing).
Error; err != nil {
return err
}
existingMap := make(map[float64]entity.FcrStandard)
for _, st := range existing {
existingMap[st.Weight] = st
}
newMap := make(map[float64]entity.FcrStandard)
for _, st := range standards {
st.FcrID = fcrID
newMap[st.Weight] = st
}
baseRepo := repository.NewBaseRepository[entity.FcrStandard](db)
for weight, newStd := range newMap {
if current, ok := existingMap[weight]; ok {
if current.FcrNumber != newStd.FcrNumber || current.Mortality != newStd.Mortality {
update := map[string]any{
"fcr_number": newStd.FcrNumber,
"mortality": newStd.Mortality,
}
if err := baseRepo.PatchOne(ctx, current.Id, update, nil); err != nil {
return err
}
}
} else {
entry := newStd
if err := baseRepo.CreateOne(ctx, &entry, nil); err != nil {
return err
}
}
}
for weight, current := range existingMap {
if _, keep := newMap[weight]; !keep {
if err := baseRepo.DeleteOne(ctx, current.Id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
}
}
return nil
}
+28
View File
@@ -0,0 +1,28 @@
package fcrs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/controllers"
fcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func FcrRoutes(v1 fiber.Router, u user.UserService, s fcr.FcrService) {
ctrl := controller.NewFcrController(s)
route := v1.Group("/fcrs")
// 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,219 @@
package service
import (
"errors"
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/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 FcrService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Fcr, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Fcr, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Fcr, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Fcr, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type fcrService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.FcrRepository
}
func NewFcrService(repo repository.FcrRepository, validate *validator.Validate) FcrService {
return &fcrService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s fcrService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Standards", func(db *gorm.DB) *gorm.DB {
return db.Order("weight ASC")
})
}
func (s fcrService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Fcr, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
fcrs, 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 fcrs: %+v", err)
return nil, 0, err
}
return fcrs, total, nil
}
func (s fcrService) GetOne(c *fiber.Ctx, id uint) (*entity.Fcr, error) {
fcr, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Fcr not found")
}
if err != nil {
s.Log.Errorf("Failed get fcr by id: %+v", err)
return nil, err
}
return fcr, nil
}
func (s *fcrService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Fcr, 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 fcr name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check fcr name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Fcr with name %s already exists", req.Name))
}
createBody := &entity.Fcr{
Name: req.Name,
CreatedBy: 1,
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
if len(req.FcrStandards) == 0 {
return nil
}
standards := make([]entity.FcrStandard, len(req.FcrStandards))
for i, std := range req.FcrStandards {
standards[i] = entity.FcrStandard{
FcrID: createBody.Id,
Weight: std.Weight,
FcrNumber: std.FcrNumber,
Mortality: std.Mortality,
}
}
if err := s.Repository.SyncStandardsDiff(c.Context(), tx, createBody.Id, standards); err != nil {
return err
}
return nil
})
if err != nil {
s.Log.Errorf("Failed to create fcr: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s fcrService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Fcr, 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 fcr name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check fcr name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Fcr with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if len(updateBody) == 0 && req.FcrStandards == nil {
return s.GetOne(c, id)
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if len(updateBody) > 0 {
if err := repoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return err
}
} else {
if _, err := repoTx.GetByID(c.Context(), id, nil); err != nil {
return err
}
}
if req.FcrStandards != nil {
standards := make([]entity.FcrStandard, len(req.FcrStandards))
for i, std := range req.FcrStandards {
standards[i] = entity.FcrStandard{
FcrID: id,
Weight: std.Weight,
FcrNumber: std.FcrNumber,
Mortality: std.Mortality,
}
}
if err := s.Repository.SyncStandardsDiff(c.Context(), tx, id, standards); err != nil {
return err
}
}
return nil
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Fcr not found")
}
s.Log.Errorf("Failed to update fcr: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s fcrService) DeleteOne(c *fiber.Ctx, id uint) error {
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := s.Repository.SyncStandardsDiff(c.Context(), tx, id, nil); err != nil {
return err
}
return repoTx.DeleteOne(c.Context(), id)
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Fcr not found")
}
s.Log.Errorf("Failed to delete fcr: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,23 @@
package validation
type FcrStandard struct {
Weight float64 `json:"weight" validate:"required,gte=0"`
FcrNumber float64 `json:"fcr_number" validate:"required,gte=0"`
Mortality float64 `json:"mortality" validate:"required,gte=0"`
}
type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"`
FcrStandards []FcrStandard `json:"fcr_standards" validate:"required,min=1,dive"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty_strict,min=3,max=50"`
FcrStandards []FcrStandard `json:"fcr_standards,omitempty" validate:"omitempty,min=1,dive"`
}
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,81 @@
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
}
func ToKandangDetailDTO(e entity.Kandang) KandangDetailDTO {
return KandangDetailDTO{
KandangListDTO: ToKandangListDTO(e),
}
}
@@ -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,170 @@
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.GetOne(c, createBody.Id)
}
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 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
}
if req.LocationId != nil {
updateBody["location_id"] = *req.LocationId
}
if req.PicId != nil {
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"`
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,75 @@
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
}
func ToLocationDetailDTO(e entity.Location) LocationDetailDTO {
return LocationDetailDTO{
LocationListDTO: ToLocationListDTO(e),
}
}
@@ -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,166 @@
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
}
return s.GetOne(c, createBody.Id)
}
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 err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Area", ID: req.AreaId, Exists: s.Repository.AreaExists}); err != nil {
return nil, err
}
if req.AreaId != nil {
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"`
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"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type NonstockController struct {
NonstockService service.NonstockService
}
func NewNonstockController(nonstockService service.NonstockService) *NonstockController {
return &NonstockController{
NonstockService: nonstockService,
}
}
func (u *NonstockController) 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.NonstockService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.NonstockListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all nonstocks successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToNonstockListDTOs(result),
})
}
func (u *NonstockController) 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.NonstockService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get nonstock successfully",
Data: dto.ToNonstockDetailDTO(*result),
})
}
func (u *NonstockController) 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.NonstockService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create nonstock successfully",
Data: dto.ToNonstockDetailDTO(*result),
})
}
func (u *NonstockController) 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.NonstockService.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 nonstock successfully",
Data: dto.ToNonstockDetailDTO(*result),
})
}
func (u *NonstockController) 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.NonstockService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete nonstock successfully",
})
}
@@ -0,0 +1,97 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type NonstockBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
UomID uint `json:"uom_id"`
}
type NonstockListDTO struct {
NonstockBaseDTO
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
Flags []string `json:"flags"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type NonstockDetailDTO struct {
NonstockListDTO
Flags []string `json:"flags"`
}
// === Mapper Functions ===
func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO {
return NonstockBaseDTO{
Id: e.Id,
Name: e.Name,
UomID: e.UomId,
}
}
func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
suppliers := make([]supplierDTO.SupplierBaseDTO, len(e.Suppliers))
for i, s := range e.Suppliers {
suppliers[i] = supplierDTO.ToSupplierBaseDTO(s)
}
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return NonstockListDTO{
NonstockBaseDTO: ToNonstockBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Uom: uomRef,
Suppliers: suppliers,
Flags: flags,
}
}
func ToNonstockListDTOs(e []entity.Nonstock) []NonstockListDTO {
result := make([]NonstockListDTO, len(e))
for i, r := range e {
result[i] = ToNonstockListDTO(r)
}
return result
}
func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return NonstockDetailDTO{
NonstockListDTO: ToNonstockListDTO(e),
Flags: flags,
}
}
@@ -0,0 +1,26 @@
package nonstocks
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
sNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type NonstockModule struct{}
func (NonstockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
nonstockRepo := rNonstock.NewNonstockRepository(db)
userRepo := rUser.NewUserRepository(db)
nonstockService := sNonstock.NewNonstockService(nonstockRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
NonstockRoutes(router, userService, nonstockService)
}
@@ -0,0 +1,172 @@
package repository
import (
"context"
"errors"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type NonstockRepository interface {
repository.BaseRepository[entity.Nonstock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error
UomExists(ctx context.Context, uomID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error
DeleteFlags(ctx context.Context, tx *gorm.DB, nonstockID uint) error
GetFlags(ctx context.Context, nonstockID uint) ([]entity.Flag, error)
}
type NonstockRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Nonstock]
}
func NewNonstockRepository(db *gorm.DB) NonstockRepository {
return &NonstockRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Nonstock](db),
}
}
func (r *NonstockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Nonstock](ctx, r.DB(), name, excludeID)
}
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error {
db := tx
if db == nil {
db = r.DB()
}
if supplierIDs == nil {
return db.WithContext(ctx).
Where("nonstock_id = ?", nonstockID).
Delete(&entity.NonstockSupplier{}).
Error
}
var existing []entity.NonstockSupplier
if err := db.WithContext(ctx).
Where("nonstock_id = ?", nonstockID).
Find(&existing).
Error; err != nil {
return err
}
existingMap := make(map[uint]struct{}, len(existing))
for _, rel := range existing {
existingMap[rel.SupplierID] = struct{}{}
}
incomingMap := make(map[uint]struct{}, len(supplierIDs))
for _, id := range supplierIDs {
incomingMap[id] = struct{}{}
if _, exists := existingMap[id]; exists {
continue
}
record := entity.NonstockSupplier{NonstockID: nonstockID, SupplierID: id}
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err
}
}
for _, rel := range existing {
if _, keep := incomingMap[rel.SupplierID]; !keep {
if err := db.WithContext(ctx).
Where("nonstock_id = ? AND supplier_id = ?", nonstockID, rel.SupplierID).
Delete(&entity.NonstockSupplier{}).
Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
}
}
return nil
}
func (r *NonstockRepositoryImpl) UomExists(ctx context.Context, uomID uint) (bool, error) {
var count int64
if err := r.DB().WithContext(ctx).
Model(&entity.Uom{}).
Where("id = ?", uomID).
Count(&count).
Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *NonstockRepositoryImpl) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) {
if len(supplierIDs) == 0 {
return nil, nil
}
var suppliers []entity.Supplier
if err := r.DB().WithContext(ctx).
Select("id", "category").
Where("id IN ?", supplierIDs).
Find(&suppliers).
Error; err != nil {
return nil, err
}
return suppliers, nil
}
func (r *NonstockRepositoryImpl) SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error {
db := tx
if db == nil {
db = r.DB()
}
// Hapus flags lama terlebih dahulu
if err := db.WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", nonstockID, entity.FlagableTypeNonstock).
Delete(&entity.Flag{}).
Error; err != nil {
return err
}
// Insert flags baru jika ada
if len(flags) > 0 {
newFlags := make([]entity.Flag, len(flags))
for i, f := range flags {
newFlags[i] = entity.Flag{
Name: f,
FlagableID: nonstockID,
FlagableType: entity.FlagableTypeNonstock,
}
}
if err := db.WithContext(ctx).Create(&newFlags).Error; err != nil {
return err
}
}
return nil
}
func (r *NonstockRepositoryImpl) DeleteFlags(ctx context.Context, tx *gorm.DB, nonstockID uint) error {
db := tx
if db == nil {
db = r.DB()
}
return db.WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", nonstockID, entity.FlagableTypeNonstock).
Delete(&entity.Flag{}).
Error
}
func (r *NonstockRepositoryImpl) GetFlags(ctx context.Context, nonstockID uint) ([]entity.Flag, error) {
var flags []entity.Flag
if err := r.DB().WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", nonstockID, entity.FlagableTypeNonstock).
Find(&flags).
Error; err != nil {
return nil, err
}
return flags, nil
}
@@ -0,0 +1,28 @@
package nonstocks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/controllers"
nonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func NonstockRoutes(v1 fiber.Router, u user.UserService, s nonstock.NonstockService) {
ctrl := controller.NewNonstockController(s)
route := v1.Group("/nonstocks")
// 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,308 @@
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/nonstocks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/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 NonstockService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Nonstock, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Nonstock, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Nonstock, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Nonstock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type nonstockService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.NonstockRepository
}
func NewNonstockService(repo repository.NonstockRepository, validate *validator.Validate) NonstockService {
return &nonstockService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s nonstockService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Uom").
Preload("Flags").
Preload("Suppliers", func(db *gorm.DB) *gorm.DB {
return db.Order("suppliers.name ASC")
})
}
func (s nonstockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Nonstock, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
nonstocks, 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 nonstocks: %+v", err)
return nil, 0, err
}
return nonstocks, total, nil
}
func (s nonstockService) GetOne(c *fiber.Ctx, id uint) (*entity.Nonstock, error) {
nonstock, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Nonstock not found")
}
if err != nil {
s.Log.Errorf("Failed get nonstock by id: %+v", err)
return nil, err
}
return nonstock, nil
}
func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Nonstock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
ctx := c.Context()
name := strings.TrimSpace(req.Name)
if exists, err := s.Repository.NameExists(ctx, name, nil); err != nil {
s.Log.Errorf("Failed to check nonstock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check nonstock name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Nonstock with name %s already exists", name))
}
if err := common.EnsureRelations(ctx, common.RelationCheck{Name: "Uom", ID: &req.UomID, Exists: s.Repository.UomExists}); err != nil {
return nil, err
}
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
if len(supplierIDs) > 0 {
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
}
if len(supplierList) != len(supplierIDs) {
actualIDs := make([]uint, len(supplierList))
for i, supplier := range supplierList {
actualIDs[i] = supplier.Id
}
missing := utils.MissingUintIDs(supplierIDs, actualIDs)
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
}
for _, sup := range supplierList {
if strings.ToUpper(sup.Category) != string(utils.SupplierCategoryBOP) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category BOP", sup.Id))
}
}
}
nonstockFlags, flagErr := normalizeNonstockFlags(req.Flags)
if flagErr != nil {
return nil, flagErr
}
createBody := &entity.Nonstock{
Name: req.Name,
UomId: req.UomID,
CreatedBy: 1,
}
err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(ctx, createBody, nil); err != nil {
return err
}
if err := s.Repository.SyncFlags(ctx, tx, createBody.Id, nonstockFlags); err != nil {
return err
}
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
})
if err != nil {
s.Log.Errorf("Failed to create nonstock: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Nonstock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
ctx := c.Context()
if err := common.EnsureRelations(ctx, common.RelationCheck{Name: "Uom", ID: req.UomID, Exists: s.Repository.UomExists}); err != nil {
return nil, err
}
if req.Name != nil {
if exists, err := s.Repository.NameExists(ctx, *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check nonstock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check nonstock name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Nonstock with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.UomID != nil {
updateBody["uom_id"] = *req.UomID
}
var supplierIDs []uint
var supplierUpdate bool
if req.SupplierIDs != nil {
supplierUpdate = true
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
if len(supplierIDs) > 0 {
var supplierList []entity.Supplier
var supplierErr error
supplierList, supplierErr = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
}
if len(supplierList) != len(supplierIDs) {
actualIDs := make([]uint, len(supplierList))
for i, supplier := range supplierList {
actualIDs[i] = supplier.Id
}
missing := utils.MissingUintIDs(supplierIDs, actualIDs)
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
}
for _, sup := range supplierList {
if strings.ToUpper(sup.Category) != string(utils.SupplierCategoryBOP) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category BOP", sup.Id))
}
}
}
}
var (
flagUpdate bool
flagValues []string
)
if req.Flags != nil {
flagUpdate = true
var flagErr error
flagValues, flagErr = normalizeNonstockFlags(*req.Flags)
if flagErr != nil {
return nil, flagErr
}
}
if len(updateBody) == 0 && !supplierUpdate && !flagUpdate {
return s.GetOne(c, id)
}
err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if len(updateBody) > 0 {
if err := repoTx.PatchOne(ctx, id, updateBody, nil); err != nil {
return err
}
} else {
if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
return err
}
}
if supplierUpdate {
var ids []uint
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
return err
}
}
if flagUpdate {
if err := s.Repository.SyncFlags(ctx, tx, id, flagValues); err != nil {
return err
}
}
return nil
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Nonstock not found")
}
s.Log.Errorf("Failed to update nonstock: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s nonstockService) DeleteOne(c *fiber.Ctx, id uint) error {
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := s.Repository.SyncSuppliersDiff(c.Context(), tx, id, nil); err != nil {
return err
}
if err := s.Repository.DeleteFlags(c.Context(), tx, id); err != nil {
return err
}
return repoTx.DeleteOne(c.Context(), id)
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Nonstock not found")
}
s.Log.Errorf("Failed to delete nonstock: %+v", err)
return err
}
return nil
}
func normalizeNonstockFlags(raw []string) ([]string, error) {
normalized, invalid := utils.NormalizeFlagsForGroup(raw, utils.FlagGroupNonstock)
if len(invalid) > 0 {
invalidStr := strings.Join(utils.FlagTypesToStrings(invalid), ", ")
allowedStr := strings.Join(utils.FlagTypesToStrings(utils.AllowedFlagTypes(utils.FlagGroupNonstock)), ", ")
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid nonstock flags: %s. Allowed flags: %s", invalidStr, allowedStr))
}
return utils.FlagTypesToStrings(normalized), nil
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,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/product-categories/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ProductCategoryController struct {
ProductCategoryService service.ProductCategoryService
}
func NewProductCategoryController(productCategoryService service.ProductCategoryService) *ProductCategoryController {
return &ProductCategoryController{
ProductCategoryService: productCategoryService,
}
}
func (u *ProductCategoryController) 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.ProductCategoryService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductCategoryListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all product categories successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProductCategoryListDTOs(result),
})
}
func (u *ProductCategoryController) 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.ProductCategoryService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get product category successfully",
Data: dto.ToProductCategoryDetailDTO(*result),
})
}
func (u *ProductCategoryController) 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.ProductCategoryService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create product category successfully",
Data: dto.ToProductCategoryDetailDTO(*result),
})
}
func (u *ProductCategoryController) 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.ProductCategoryService.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 product category successfully",
Data: dto.ToProductCategoryDetailDTO(*result),
})
}
func (u *ProductCategoryController) 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.ProductCategoryService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete product category successfully",
})
}
@@ -0,0 +1,66 @@
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 ProductCategoryBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
}
type ProductCategoryListDTO struct {
ProductCategoryBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ProductCategoryDetailDTO struct {
ProductCategoryListDTO
}
// === Mapper Functions ===
func ToProductCategoryBaseDTO(e entity.ProductCategory) ProductCategoryBaseDTO {
return ProductCategoryBaseDTO{
Id: e.Id,
Name: e.Name,
Code: e.Code,
}
}
func ToProductCategoryListDTO(e entity.ProductCategory) ProductCategoryListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return ProductCategoryListDTO{
ProductCategoryBaseDTO: ToProductCategoryBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToProductCategoryListDTOs(e []entity.ProductCategory) []ProductCategoryListDTO {
result := make([]ProductCategoryListDTO, len(e))
for i, r := range e {
result[i] = ToProductCategoryListDTO(r)
}
return result
}
func ToProductCategoryDetailDTO(e entity.ProductCategory) ProductCategoryDetailDTO {
return ProductCategoryDetailDTO{
ProductCategoryListDTO: ToProductCategoryListDTO(e),
}
}
@@ -0,0 +1,25 @@
package productcategories
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rProductCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/repositories"
sProductCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProductCategoryModule struct{}
func (ProductCategoryModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
productCategoryRepo := rProductCategory.NewProductCategoryRepository(db)
userRepo := rUser.NewUserRepository(db)
productCategoryService := sProductCategory.NewProductCategoryService(productCategoryRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProductCategoryRoutes(router, userService, productCategoryService)
}
@@ -0,0 +1,44 @@
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 ProductCategoryRepository interface {
repository.BaseRepository[entity.ProductCategory]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
CodeExists(ctx context.Context, code string, excludeID *uint) (bool, error)
}
type ProductCategoryRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProductCategory]
}
func NewProductCategoryRepository(db *gorm.DB) ProductCategoryRepository {
return &ProductCategoryRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductCategory](db),
}
}
func (r *ProductCategoryRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.ProductCategory](ctx, r.DB(), name, excludeID)
}
func (r *ProductCategoryRepositoryImpl) CodeExists(ctx context.Context, code string, excludeID *uint) (bool, error) {
var count int64
q := r.DB().WithContext(ctx).
Model(new(entity.ProductCategory)).
Where("code = ?", code).
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
}
@@ -0,0 +1,28 @@
package productcategories
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/controllers"
productCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProductCategoryRoutes(v1 fiber.Router, u user.UserService, s productCategory.ProductCategoryService) {
ctrl := controller.NewProductCategoryController(s)
route := v1.Group("/product-categories")
// 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,175 @@
package service
import (
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/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 ProductCategoryService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductCategory, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductCategory, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductCategory, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductCategory, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type productCategoryService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProductCategoryRepository
}
func NewProductCategoryService(repo repository.ProductCategoryRepository, validate *validator.Validate) ProductCategoryService {
return &productCategoryService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s productCategoryService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s productCategoryService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductCategory, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
productCategories, 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 product categories: %+v", err)
return nil, 0, err
}
return productCategories, total, nil
}
func (s productCategoryService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductCategory, error) {
productCategory, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product category not found")
}
if err != nil {
s.Log.Errorf("Failed get product category by id: %+v", err)
return nil, err
}
return productCategory, nil
}
func (s *productCategoryService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProductCategory, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
ctx := c.Context()
name := strings.TrimSpace(req.Name)
code := strings.ToUpper(strings.TrimSpace(req.Code))
if exists, err := s.Repository.NameExists(ctx, name, nil); err != nil {
s.Log.Errorf("Failed to check product category name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product category name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product category with name %s already exists", name))
}
if exists, err := s.Repository.CodeExists(ctx, code, nil); err != nil {
s.Log.Errorf("Failed to check product category code: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product category code")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product category with code %s already exists", code))
}
createBody := &entity.ProductCategory{
Name: name,
Code: code,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(ctx, createBody, nil); err != nil {
s.Log.Errorf("Failed to create product category: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s productCategoryService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductCategory, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name cannot be empty")
}
if exists, err := s.Repository.NameExists(c.Context(), name, &id); err != nil {
s.Log.Errorf("Failed to check product category name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product category name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product category with name %s already exists", name))
}
updateBody["name"] = name
}
if req.Code != nil {
code := strings.ToUpper(strings.TrimSpace(*req.Code))
if code == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Code cannot be empty")
}
if exists, err := s.Repository.CodeExists(c.Context(), code, &id); err != nil {
s.Log.Errorf("Failed to check product category code: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product category code")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product category with code %s already exists", code))
}
updateBody["code"] = code
}
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, "Product category not found")
}
s.Log.Errorf("Failed to update product category: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s productCategoryService) 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, "Product category not found")
}
s.Log.Errorf("Failed to delete product category: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,17 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Code string `json:"code" validate:"required_strict,max=10"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Code *string `json:"code,omitempty" validate:"omitempty,max=10"`
}
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/products/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ProductController struct {
ProductService service.ProductService
}
func NewProductController(productService service.ProductService) *ProductController {
return &ProductController{
ProductService: productService,
}
}
func (u *ProductController) 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.ProductService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all products successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProductListDTOs(result),
})
}
func (u *ProductController) 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.ProductService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get product successfully",
Data: dto.ToProductDetailDTO(*result),
})
}
func (u *ProductController) 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.ProductService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create product successfully",
Data: dto.ToProductDetailDTO(*result),
})
}
func (u *ProductController) 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.ProductService.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 product successfully",
Data: dto.ToProductDetailDTO(*result),
})
}
func (u *ProductController) 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.ProductService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete product successfully",
})
}
@@ -0,0 +1,116 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type ProductBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductListDTO struct {
ProductBaseDTO
Brand string `json:"brand"`
Sku *string `json:"sku,omitempty"`
ProductPrice float64 `json:"product_price"`
SellingPrice *float64 `json:"selling_price,omitempty"`
Tax *float64 `json:"tax,omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty"`
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
Flags []string `json:"flags"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ProductDetailDTO struct {
ProductListDTO
Flags []string `json:"flags"`
}
// === Mapper Functions ===
func ToProductBaseDTO(e entity.Product) ProductBaseDTO {
return ProductBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToProductListDTO(e entity.Product) ProductListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
var categoryRef *productCategoryDTO.ProductCategoryBaseDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
categoryRef = &mapped
}
suppliers := make([]supplierDTO.SupplierBaseDTO, len(e.Suppliers))
for i, s := range e.Suppliers {
suppliers[i] = supplierDTO.ToSupplierBaseDTO(s)
}
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return ProductListDTO{
Brand: e.Brand,
Sku: e.Sku,
ProductPrice: e.ProductPrice,
SellingPrice: e.SellingPrice,
Tax: e.Tax,
ExpiryPeriod: e.ExpiryPeriod,
ProductBaseDTO: ToProductBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Uom: uomRef,
ProductCategory: categoryRef,
Suppliers: suppliers,
Flags: flags,
}
}
func ToProductListDTOs(e []entity.Product) []ProductListDTO {
result := make([]ProductListDTO, len(e))
for i, r := range e {
result[i] = ToProductListDTO(r)
}
return result
}
func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return ProductDetailDTO{
ProductListDTO: ToProductListDTO(e),
Flags: flags,
}
}
@@ -0,0 +1,26 @@
package products
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
sProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProductModule struct{}
func (ProductModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
productRepo := rProduct.NewProductRepository(db)
userRepo := rUser.NewUserRepository(db)
productService := sProduct.NewProductService(productRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProductRoutes(router, userService, productService)
}
@@ -0,0 +1,196 @@
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 ProductRepository interface {
repository.BaseRepository[entity.Product]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error)
UomExists(ctx context.Context, uomID uint) (bool, error)
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
GetFlags(ctx context.Context, productID uint) ([]entity.Flag, error)
}
type ProductRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Product]
}
func NewProductRepository(db *gorm.DB) ProductRepository {
return &ProductRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Product](db),
}
}
func (r *ProductRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Product](ctx, r.DB(), name, excludeID)
}
func (r *ProductRepositoryImpl) SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error) {
var count int64
q := r.DB().WithContext(ctx).
Model(new(entity.Product)).
Where("sku = ?", sku).
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
}
func (r *ProductRepositoryImpl) UomExists(ctx context.Context, uomID uint) (bool, error) {
var count int64
if err := r.DB().WithContext(ctx).
Model(&entity.Uom{}).
Where("id = ?", uomID).
Count(&count).
Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductRepositoryImpl) CategoryExists(ctx context.Context, categoryID uint) (bool, error) {
var count int64
if err := r.DB().WithContext(ctx).
Model(&entity.ProductCategory{}).
Where("id = ?", categoryID).
Count(&count).
Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductRepositoryImpl) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) {
if len(supplierIDs) == 0 {
return nil, nil
}
var suppliers []entity.Supplier
if err := r.DB().WithContext(ctx).
Select("id", "category").
Where("id IN ?", supplierIDs).
Find(&suppliers).
Error; err != nil {
return nil, err
}
return suppliers, nil
}
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error {
db := tx
if db == nil {
db = r.DB()
}
if supplierIDs == nil {
return db.WithContext(ctx).
Where("product_id = ?", productID).
Delete(&entity.ProductSupplier{}).
Error
}
var existing []entity.ProductSupplier
if err := db.WithContext(ctx).
Where("product_id = ?", productID).
Find(&existing).
Error; err != nil {
return err
}
existingMap := make(map[uint]struct{}, len(existing))
for _, rel := range existing {
existingMap[rel.SupplierID] = struct{}{}
}
incomingMap := make(map[uint]struct{}, len(supplierIDs))
for _, id := range supplierIDs {
incomingMap[id] = struct{}{}
if _, exists := existingMap[id]; exists {
continue
}
record := entity.ProductSupplier{ProductID: productID, SupplierID: id}
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err
}
}
for _, rel := range existing {
if _, keep := incomingMap[rel.SupplierID]; !keep {
if err := db.WithContext(ctx).
Where("product_id = ? AND supplier_id = ?", productID, rel.SupplierID).
Delete(&entity.ProductSupplier{}).
Error; err != nil {
return err
}
}
}
return nil
}
func (r *ProductRepositoryImpl) SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error {
db := tx
if db == nil {
db = r.DB()
}
// Hapus flags lama terlebih dahulu
if err := db.WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", productID, entity.FlagableTypeProduct).
Delete(&entity.Flag{}).
Error; err != nil {
return err
}
// Insert flags baru jika ada
if len(flags) > 0 {
newFlags := make([]entity.Flag, len(flags))
for i, f := range flags {
newFlags[i] = entity.Flag{
Name: f,
FlagableID: productID,
FlagableType: entity.FlagableTypeProduct,
}
}
if err := db.WithContext(ctx).Create(&newFlags).Error; err != nil {
return err
}
}
return nil
}
func (r *ProductRepositoryImpl) DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error {
db := tx
if db == nil {
db = r.DB()
}
return db.WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", productID, entity.FlagableTypeProduct).
Delete(&entity.Flag{}).
Error
}
func (r *ProductRepositoryImpl) GetFlags(ctx context.Context, productID uint) ([]entity.Flag, error) {
var flags []entity.Flag
if err := r.DB().WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", productID, entity.FlagableTypeProduct).
Find(&flags).
Error; err != nil {
return nil, err
}
return flags, nil
}
+28
View File
@@ -0,0 +1,28 @@
package products
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/controllers"
product "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProductRoutes(v1 fiber.Router, u user.UserService, s product.ProductService) {
ctrl := controller.NewProductController(s)
route := v1.Group("/products")
// 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,384 @@
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/products/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/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 ProductService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Product, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Product, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Product, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type productService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProductRepository
}
func normalizeProductFlags(raw []string) ([]string, error) {
normalized, invalid := utils.NormalizeFlagsForGroup(raw, utils.FlagGroupProduct)
if len(invalid) > 0 {
invalidStr := strings.Join(utils.FlagTypesToStrings(invalid), ", ")
allowedStr := strings.Join(utils.FlagTypesToStrings(utils.AllowedFlagTypes(utils.FlagGroupProduct)), ", ")
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid product flags: %s. Allowed flags: %s", invalidStr, allowedStr))
}
return utils.FlagTypesToStrings(normalized), nil
}
func NewProductService(repo repository.ProductRepository, validate *validator.Validate) ProductService {
return &productService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s productService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Uom").
Preload("ProductCategory").
Preload("Flags").
Preload("Suppliers", func(db *gorm.DB) *gorm.DB {
return db.Order("suppliers.name ASC")
})
}
func (s productService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
products, 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 products: %+v", err)
return nil, 0, err
}
return products, total, nil
}
func (s productService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) {
product, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
if err != nil {
s.Log.Errorf("Failed get product by id: %+v", err)
return nil, err
}
return product, nil
}
func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Product, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
ctx := c.Context()
name := strings.TrimSpace(req.Name)
brand := strings.TrimSpace(req.Brand)
var sku *string
if req.Sku != nil {
trimmed := strings.ToUpper(strings.TrimSpace(*req.Sku))
if trimmed != "" {
sku = &trimmed
}
}
if exists, err := s.Repository.NameExists(ctx, name, nil); err != nil {
s.Log.Errorf("Failed to check product name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with name %s already exists", name))
}
if sku != nil {
if exists, err := s.Repository.SkuExists(ctx, *sku, nil); err != nil {
s.Log.Errorf("Failed to check product sku: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product sku")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with sku %s already exists", *sku))
}
}
if err := common.EnsureRelations(ctx,
common.RelationCheck{Name: "Uom", ID: &req.UomID, Exists: s.Repository.UomExists},
common.RelationCheck{Name: "Product category", ID: &req.ProductCategoryID, Exists: s.Repository.CategoryExists},
); err != nil {
return nil, err
}
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
var err error
if len(supplierIDs) > 0 {
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
}
if len(suppliers) != len(supplierIDs) {
actual := make([]uint, len(suppliers))
for i, supplier := range suppliers {
actual[i] = supplier.Id
}
missing := utils.MissingUintIDs(supplierIDs, actual)
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
}
for _, sup := range suppliers {
if strings.ToUpper(sup.Category) != string(utils.SupplierCategorySapronak) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category SAPRONAK", sup.Id))
}
}
}
productFlags, flagErr := normalizeProductFlags(req.Flags)
if flagErr != nil {
return nil, flagErr
}
createBody := &entity.Product{
Name: name,
Brand: brand,
Sku: sku,
UomId: req.UomID,
ProductCategoryId: req.ProductCategoryID,
ProductPrice: req.ProductPrice,
SellingPrice: req.SellingPrice,
Tax: req.Tax,
ExpiryPeriod: req.ExpiryPeriod,
CreatedBy: 1,
}
err = s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(ctx, createBody, nil); err != nil {
return err
}
if err := s.Repository.SyncFlags(ctx, tx, createBody.Id, productFlags); err != nil {
return err
}
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
})
if err != nil {
s.Log.Errorf("Failed to create product: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Product, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name cannot be empty")
}
if exists, err := s.Repository.NameExists(c.Context(), name, &id); err != nil {
s.Log.Errorf("Failed to check product name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with name %s already exists", name))
}
updateBody["name"] = name
}
if req.Brand != nil {
brand := strings.TrimSpace(*req.Brand)
if brand == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Brand cannot be empty")
}
updateBody["brand"] = brand
}
if req.Sku != nil {
sku := strings.ToUpper(strings.TrimSpace(*req.Sku))
if sku == "" {
updateBody["sku"] = nil
} else {
if exists, err := s.Repository.SkuExists(c.Context(), sku, &id); err != nil {
s.Log.Errorf("Failed to check product sku: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product sku")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with sku %s already exists", sku))
}
updateBody["sku"] = sku
}
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Uom", ID: req.UomID, Exists: s.Repository.UomExists},
common.RelationCheck{Name: "Product category", ID: req.ProductCategoryID, Exists: s.Repository.CategoryExists},
); err != nil {
return nil, err
}
if req.UomID != nil {
updateBody["uom_id"] = *req.UomID
}
if req.ProductCategoryID != nil {
updateBody["product_category_id"] = *req.ProductCategoryID
}
if req.ProductPrice != nil {
updateBody["product_price"] = *req.ProductPrice
}
if req.SellingPrice != nil {
updateBody["selling_price"] = req.SellingPrice
}
if req.Tax != nil {
updateBody["tax"] = req.Tax
}
if req.ExpiryPeriod != nil {
updateBody["expiry_period"] = req.ExpiryPeriod
}
ctx := c.Context()
var suppliers []entity.Supplier
var supplierIDs []uint
var supplierUpdate bool
if req.SupplierIDs != nil {
supplierUpdate = true
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
if len(supplierIDs) > 0 {
var err error
suppliers, err = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
}
if len(suppliers) != len(supplierIDs) {
actual := make([]uint, len(suppliers))
for i, supplier := range suppliers {
actual[i] = supplier.Id
}
missing := utils.MissingUintIDs(supplierIDs, actual)
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
}
for _, sup := range suppliers {
if strings.ToUpper(sup.Category) != string(utils.SupplierCategorySapronak) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category SAPRONAK", sup.Id))
}
}
}
}
var (
flagUpdate bool
flagValues []string
)
if req.Flags != nil {
flagUpdate = true
var flagErr error
flagValues, flagErr = normalizeProductFlags(*req.Flags)
if flagErr != nil {
return nil, flagErr
}
}
if len(updateBody) == 0 && !supplierUpdate && !flagUpdate {
return s.GetOne(c, id)
}
err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if len(updateBody) > 0 {
if err := repoTx.PatchOne(ctx, id, updateBody, nil); err != nil {
return err
}
} else {
if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
return err
}
}
if supplierUpdate {
var ids []uint
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
return err
}
}
if flagUpdate {
if err := s.Repository.SyncFlags(ctx, tx, id, flagValues); err != nil {
return err
}
}
return nil
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
s.Log.Errorf("Failed to update product: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s productService) DeleteOne(c *fiber.Ctx, id uint) error {
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := s.Repository.SyncSuppliersDiff(c.Context(), tx, id, nil); err != nil {
return err
}
if err := s.Repository.DeleteFlags(c.Context(), tx, id); err != nil {
return err
}
return repoTx.DeleteOne(c.Context(), id)
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Product not found")
}
s.Log.Errorf("Failed to delete product: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,35 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Brand string `json:"brand" validate:"required_strict,min=2"`
Sku *string `json:"sku,omitempty" validate:"omitempty"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
ProductCategoryID uint `json:"product_category_id" validate:"required,gt=0"`
ProductPrice float64 `json:"product_price" validate:"required"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
Brand *string `json:"brand,omitempty" validate:"omitempty,min=2"`
Sku *string `json:"sku,omitempty" validate:"omitempty"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
ProductCategoryID *uint `json:"product_category_id,omitempty" validate:"omitempty,gt=0"`
ProductPrice *float64 `json:"product_price,omitempty" validate:"omitempty"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1"`
Search string `query:"search" validate:"omitempty,max=50"`
}
+23 -1
View File
@@ -7,15 +7,37 @@ 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"
fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs"
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks"
productcategories "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories"
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
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"
products "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products"
banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks"
// 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{},
suppliers.SupplierModule{},
fcrs.FcrModule{},
nonstocks.NonstockModule{},
productcategories.ProductCategoryModule{},
products.ProductModule{},
banks.BankModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }

Some files were not shown because too many files have changed in this diff Show More