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

This commit is contained in:
Hafizh A. Y
2025-10-02 10:51:15 +07:00
parent dbc1f79a36
commit e8905be856
79 changed files with 3745 additions and 169 deletions
+4 -1
View File
@@ -27,7 +27,7 @@ WAIT_DB := docker run --rm --network $(NETWORK) postgres:alpine \
.DEFAULT_GOAL := start
# --- Daftar phony targets ---
.PHONY: start build lint gen \
.PHONY: start build test lint gen \
db-up wait-db \
migration-% migrate-up migrate-down migrate-fresh \
seed \
@@ -40,6 +40,9 @@ start:
build:
@go build -o tmp/app ./cmd/api
test:
@go test ./test/...
lint:
@golangci-lint run
+11 -1
View File
@@ -24,8 +24,11 @@ require (
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // 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/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/universal-translator v0.18.1 // 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/now v1.1.5 // 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/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/pelletier/go-toml/v2 v2.2.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/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
@@ -71,4 +76,9 @@ require (
golang.org/x/text v0.22.0 // indirect
gopkg.in/ini.v1 v1.67.0 // 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/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/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/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
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/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
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/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.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
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/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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
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/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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/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/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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
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-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-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-20220722155257-8c9f86f7a55f/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=
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/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/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=
+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 {
return nil
}
return validate
}
@@ -1,20 +1,32 @@
DROP TABLE IF EXISTS fcr_standards;
DROP INDEX IF EXISTS suppliers_name_unique;
DROP INDEX IF EXISTS products_sku_unique;
DROP INDEX IF EXISTS products_name_unique;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS flags;
DROP INDEX IF EXISTS customers_name_unique;
DROP INDEX IF EXISTS customers_email_unique;
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_name_unique;
DROP TABLE IF EXISTS product_categories;
DROP INDEX IF EXISTS nonstocks_name_unique;
DROP TABLE IF EXISTS nonstocks;
DROP INDEX IF EXISTS banks_name_unique;
DROP TABLE IF EXISTS banks;
DROP INDEX IF EXISTS kandangs_name_unique;
DROP TABLE IF EXISTS warehouses;
DROP TABLE IF EXISTS kandangs;
DROP INDEX IF EXISTS locations_name_unique;
DROP TABLE IF EXISTS locations;
DROP INDEX IF EXISTS areas_name_unique;
DROP TABLE IF EXISTS areas;
DROP INDEX IF EXISTS uoms_name_unique;
DROP TABLE IF EXISTS uoms;
DROP TABLE IF EXISTS suppliers;
DROP INDEX IF EXISTS fcrs_name_unique;
DROP TABLE IF EXISTS fcrs;
DROP TABLE IF EXISTS projects;
DROP INDEX IF EXISTS users_id_user_unique;
@@ -31,8 +31,9 @@ CREATE TABLE product_categories (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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;
-- UOM
@@ -42,8 +43,9 @@ CREATE TABLE uoms (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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 (
@@ -51,8 +53,8 @@ CREATE TABLE products (
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),
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),
@@ -60,8 +62,9 @@ CREATE TABLE products (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id)
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
-- BANKS
@@ -74,8 +77,9 @@ CREATE TABLE banks (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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
CREATE TABLE areas (
@@ -84,51 +88,56 @@ CREATE TABLE areas (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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
CREATE TABLE locations (
id BIGSERIAL PRIMARY KEY,
name VARCHAR 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(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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
CREATE TABLE kandangs (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(191) NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations(id),
pic_id BIGINT NOT NULL REFERENCES users(id),
name VARCHAR NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE,
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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
CREATE TABLE warehouses (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations(id),
kandang_id BIGINT REFERENCES kandangs(id),
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
location_id BIGINT REFERENCES locations(id) ON DELETE SET NULL ON UPDATE CASCADE,
kandang_id BIGINT REFERENCES kandangs(id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
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,
address TEXT NOT NULL,
phone VARCHAR(20) NOT NULL,
@@ -138,19 +147,21 @@ CREATE TABLE customers (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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
CREATE TABLE nonstocks (
id BIGSERIAL PRIMARY KEY,
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(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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
CREATE TABLE fcrs (
@@ -159,8 +170,9 @@ CREATE TABLE fcrs (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
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 (
id BIGSERIAL PRIMARY KEY,
@@ -191,8 +203,9 @@ CREATE TABLE suppliers (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id)
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL;
-- PROJECTS
CREATE TABLE projects (
@@ -200,5 +213,5 @@ CREATE TABLE projects (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL REFERENCES users(id)
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
+2 -2
View File
@@ -3,7 +3,7 @@ package seed
import (
"fmt"
mUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
@@ -11,7 +11,7 @@ import (
func Run(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// ===== Users (user) =====
user := mUser.User{
user := entity.User{
Email: "admin@mbugroup.id",
IdUser: 1,
Name: "Super Admin",
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Area struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Locations []Location `gorm:"foreignKey:AreaId;references:Id"`
}
+26
View File
@@ -0,0 +1,26 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Customer struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
PicId uint `gorm:"not null"`
Type string `gorm:"not null"`
Address string `gorm:"not null"`
Phone string `gorm:"not null"`
Email string `gorm:"not null"`
AccountNumber string `gorm:"not null"`
Balance float64 `gorm:"default:0"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
}
+22
View File
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Kandang struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
}
+21
View File
@@ -0,0 +1,21 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Location struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Address string `gorm:"not null"`
AreaId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Uom struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+17
View File
@@ -0,0 +1,17 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type User struct {
Id uint `gorm:"primaryKey"`
IdUser int64 `gorm:"uniqueIndex"`
Email string `gorm:"uniqueIndex"`
Name string `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
+25
View File
@@ -0,0 +1,25 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Warehouse struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Type string `gorm:"not null"`
AreaId uint `gorm:"not null"`
LocationId *uint
KandangId *uint
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
Location *Location `gorm:"foreignKey:LocationId;references:Id"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
}
+64
View File
@@ -0,0 +1,64 @@
package trim
import (
"bytes"
"encoding/json"
"strings"
"github.com/gofiber/fiber/v2"
)
// JSONBody trims whitespace from string fields in JSON request bodies.
func JSONBody() fiber.Handler {
return func(c *fiber.Ctx) error {
contentType := c.Get(fiber.HeaderContentType)
if !strings.Contains(contentType, fiber.MIMEApplicationJSON) {
return c.Next()
}
body := c.Body()
if len(body) == 0 {
return c.Next()
}
var payload any
if err := json.Unmarshal(body, &payload); err != nil {
return c.Next()
}
trimStrings(payload)
buf := bytes.Buffer{}
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(payload); err != nil {
return err
}
trimmedBody := bytes.TrimSpace(buf.Bytes())
c.Request().SetBody(trimmedBody)
return c.Next()
}
}
func trimStrings(value any) {
switch v := value.(type) {
case map[string]any:
for key, val := range v {
if str, ok := val.(string); ok {
v[key] = strings.TrimSpace(str)
continue
}
trimStrings(val)
}
case []any:
for i, elem := range v {
if str, ok := elem.(string); ok {
v[i] = strings.TrimSpace(str)
continue
}
trimStrings(elem)
}
}
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type AreaController struct {
AreaService service.AreaService
}
func NewAreaController(areaService service.AreaService) *AreaController {
return &AreaController{
AreaService: areaService,
}
}
func (u *AreaController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.AreaService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.AreaListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all areas successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToAreaListDTOs(result),
})
}
func (u *AreaController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.AreaService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get area successfully",
Data: dto.ToAreaListDTO(*result),
})
}
func (u *AreaController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.AreaService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create area successfully",
Data: dto.ToAreaListDTO(*result),
})
}
func (u *AreaController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.AreaService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update area successfully",
Data: dto.ToAreaListDTO(*result),
})
}
func (u *AreaController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.AreaService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete area successfully",
})
}
@@ -0,0 +1,58 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type AreaBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaListDTO struct {
AreaBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type AreaDetailDTO struct {
AreaListDTO
}
// === Mapper Functions ===
func ToAreaBaseDTO(e entity.Area) AreaBaseDTO {
return AreaBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToAreaListDTO(e entity.Area) AreaListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return AreaListDTO{
AreaBaseDTO: ToAreaBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToAreaListDTOs(e []entity.Area) []AreaListDTO {
result := make([]AreaListDTO, len(e))
for i, r := range e {
result[i] = ToAreaListDTO(r)
}
return result
}
+26
View File
@@ -0,0 +1,26 @@
package areas
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rArea "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/repositories"
sArea "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type AreaModule struct{}
func (AreaModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
areaRepo := rArea.NewAreaRepository(db)
userRepo := rUser.NewUserRepository(db)
areaService := sArea.NewAreaService(areaRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
AreaRoutes(router, userService, areaService)
}
@@ -0,0 +1,31 @@
package repository
import (
"context"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type AreaRepository interface {
repository.BaseRepository[entity.Area]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type AreaRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Area]
db *gorm.DB
}
func NewAreaRepository(db *gorm.DB) AreaRepository {
return &AreaRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Area](db),
db: db,
}
}
func (r *AreaRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Area](ctx, r.db, name, excludeID)
}
+28
View File
@@ -0,0 +1,28 @@
package areas
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/controllers"
area "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func AreaRoutes(v1 fiber.Router, u user.UserService, s area.AreaService) {
ctrl := controller.NewAreaController(s)
route := v1.Group("/areas")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,145 @@
package service
import (
"errors"
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type AreaService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Area, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Area, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Area, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Area, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type areaService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.AreaRepository
}
func NewAreaService(repo repository.AreaRepository, validate *validator.Validate) AreaService {
return &areaService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s areaService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Area, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
areas, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get areas: %+v", err)
return nil, 0, err
}
return areas, total, nil
}
func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) {
area, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Area not found")
}
if err != nil {
s.Log.Errorf("Failed get area by id: %+v", err)
return nil, err
}
return area, nil
}
func (s *areaService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Area, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check area name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check area name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Area with name %s already exists", req.Name))
}
//TODO: created by dummy
createBody := &entity.Area{
Name: req.Name,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create area: %+v", err)
return nil, err
}
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
}
func (s areaService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Area, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check area name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check area name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Area with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Area not found")
}
s.Log.Errorf("Failed to update area: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s areaService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Area not found")
}
s.Log.Errorf("Failed to delete area: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,15 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type CustomerController struct {
CustomerService service.CustomerService
}
func NewCustomerController(customerService service.CustomerService) *CustomerController {
return &CustomerController{
CustomerService: customerService,
}
}
func (u *CustomerController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.CustomerService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.CustomerListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all customers successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToCustomerListDTOs(result),
})
}
func (u *CustomerController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.CustomerService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get customer successfully",
Data: dto.ToCustomerListDTO(*result),
})
}
func (u *CustomerController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.CustomerService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create customer successfully",
Data: dto.ToCustomerListDTO(*result),
})
}
func (u *CustomerController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.CustomerService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update customer successfully",
Data: dto.ToCustomerListDTO(*result),
})
}
func (u *CustomerController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.CustomerService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete customer successfully",
})
}
@@ -0,0 +1,79 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type CustomerBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
PicId uint `json:"pic_id"`
Type string `json:"type"`
Address string `json:"address"`
Phone string `json:"phone"`
Email string `json:"email"`
AccountNumber string `json:"account_number"`
Pic *userDTO.UserBaseDTO `json:"pic"`
}
type CustomerListDTO struct {
CustomerBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CustomerDetailDTO struct {
CustomerListDTO
}
// === Mapper Functions ===
func ToCustomerBaseDTO(e entity.Customer) CustomerBaseDTO {
var pic *userDTO.UserBaseDTO
if e.Pic.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.Pic)
pic = &mapped
}
return CustomerBaseDTO{
Id: e.Id,
Name: e.Name,
PicId: e.PicId,
Type: e.Type,
Address: e.Address,
Phone: e.Phone,
Email: e.Email,
AccountNumber: e.AccountNumber,
Pic: pic,
}
}
func ToCustomerListDTO(e entity.Customer) CustomerListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return CustomerListDTO{
CustomerBaseDTO: ToCustomerBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToCustomerListDTOs(e []entity.Customer) []CustomerListDTO {
result := make([]CustomerListDTO, len(e))
for i, r := range e {
result[i] = ToCustomerListDTO(r)
}
return result
}
@@ -0,0 +1,26 @@
package customers
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
sCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type CustomerModule struct{}
func (CustomerModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
customerRepo := rCustomer.NewCustomerRepository(db)
userRepo := rUser.NewUserRepository(db)
customerService := sCustomer.NewCustomerService(customerRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
CustomerRoutes(router, userService, customerService)
}
@@ -0,0 +1,35 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type CustomerRepository interface {
repository.BaseRepository[entity.Customer]
PicExists(ctx context.Context, areaId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type CustomerRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Customer]
db *gorm.DB
}
func NewCustomerRepository(db *gorm.DB) CustomerRepository {
return &CustomerRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Customer](db),
db: db,
}
}
func (r *CustomerRepositoryImpl) PicExists(ctx context.Context, picId uint) (bool, error) {
return repository.Exists[entity.User](ctx, r.db, picId)
}
func (r *CustomerRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Customer](ctx, r.db, name, excludeID)
}
@@ -0,0 +1,28 @@
package customers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/controllers"
customer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func CustomerRoutes(v1 fiber.Router, u user.UserService, s customer.CustomerService) {
ctrl := controller.NewCustomerController(s)
route := v1.Group("/customers")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,179 @@
package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type CustomerService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Customer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Customer, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Customer, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Customer, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type customerService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.CustomerRepository
}
func NewCustomerService(repo repository.CustomerRepository, validate *validator.Validate) CustomerService {
return &customerService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s customerService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").Preload("Pic")
}
func (s customerService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Customer, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
customers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get customers: %+v", err)
return nil, 0, err
}
return customers, total, nil
}
func (s customerService) GetOne(c *fiber.Ctx, id uint) (*entity.Customer, error) {
customer, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Customer not found")
}
if err != nil {
s.Log.Errorf("Failed get customer by id: %+v", err)
return nil, err
}
return customer, nil
}
func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Customer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check customer name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check customer name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Customer with name %s already exists", req.Name))
}
typ := strings.ToUpper(req.Type)
if !utils.IsValidCustomerSupplierType(typ) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid customer type")
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Pic", ID: &req.PicId, Exists: s.Repository.PicExists},
); err != nil {
return nil, err
}
//TODO: created by dummy
createBody := &entity.Customer{
Name: req.Name,
PicId: req.PicId,
Type: typ,
Address: req.Address,
Phone: req.Phone,
Email: req.Email,
AccountNumber: req.AccountNumber,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create customer: %+v", err)
return nil, err
}
return createBody, nil
}
func (s customerService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Customer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check customer name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check customer name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Customer with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.PicId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists}); err != nil {
return nil, err
}
updateBody["pic_id"] = *req.PicId
}
if req.Type != nil {
typ := strings.ToUpper(*req.Type)
if !utils.IsValidCustomerSupplierType(typ) {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid customer type")
}
updateBody["type"] = typ
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Customer not found")
}
s.Log.Errorf("Failed to update customer: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s customerService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Customer not found")
}
s.Log.Errorf("Failed to delete customer: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,27 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
Type string `json:"type" validate:"required_strict"`
Address string `json:"address" validate:"required_strict"`
Phone string `json:"phone" validate:"required_strict,max=20"`
Email string `json:"email" validate:"required_strict,email"`
AccountNumber string `json:"account_number" validate:"required_strict"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
Type *string `json:"type,omitempty" validate:"omitempty"`
Address *string `json:"address,omitempty" validate:"omitempty"`
Phone *string `json:"phone,omitempty" validate:"omitempty"`
Email *string `json:"email,omitempty" validate:"omitempty"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type KandangController struct {
KandangService service.KandangService
}
func NewKandangController(kandangService service.KandangService) *KandangController {
return &KandangController{
KandangService: kandangService,
}
}
func (u *KandangController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.KandangService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.KandangListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all kandangs successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToKandangListDTOs(result),
})
}
func (u *KandangController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.KandangService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get kandang successfully",
Data: dto.ToKandangListDTO(*result),
})
}
func (u *KandangController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.KandangService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create kandang successfully",
Data: dto.ToKandangListDTO(*result),
})
}
func (u *KandangController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.KandangService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update kandang successfully",
Data: dto.ToKandangListDTO(*result),
})
}
func (u *KandangController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.KandangService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete kandang successfully",
})
}
@@ -0,0 +1,75 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type KandangBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Pic *userDTO.UserBaseDTO `json:"pic"`
}
type KandangListDTO struct {
KandangBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type KandangDetailDTO struct {
KandangListDTO
}
// === Mapper Functions ===
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
var location *locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
location = &mapped
}
var pic *userDTO.UserBaseDTO
if e.Pic.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.Pic)
pic = &mapped
}
return KandangBaseDTO{
Id: e.Id,
Name: e.Name,
Location: location,
Pic: pic,
}
}
func ToKandangListDTO(e entity.Kandang) KandangListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return KandangListDTO{
KandangBaseDTO: ToKandangBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToKandangListDTOs(e []entity.Kandang) []KandangListDTO {
result := make([]KandangListDTO, len(e))
for i, r := range e {
result[i] = ToKandangListDTO(r)
}
return result
}
@@ -0,0 +1,26 @@
package kandangs
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
sKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type KandangModule struct{}
func (KandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
kandangRepo := rKandang.NewKandangRepository(db)
userRepo := rUser.NewUserRepository(db)
kandangService := sKandang.NewKandangService(kandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
KandangRoutes(router, userService, kandangService)
}
@@ -0,0 +1,40 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type KandangRepository interface {
repository.BaseRepository[entity.Kandang]
LocationExists(ctx context.Context, areaId uint) (bool, error)
PicExists(ctx context.Context, areaId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type KandangRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Kandang]
db *gorm.DB
}
func NewKandangRepository(db *gorm.DB) KandangRepository {
return &KandangRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Kandang](db),
db: db,
}
}
func (r *KandangRepositoryImpl) LocationExists(ctx context.Context, locationId uint) (bool, error) {
return repository.Exists[entity.Location](ctx, r.db, locationId)
}
func (r *KandangRepositoryImpl) PicExists(ctx context.Context, picId uint) (bool, error) {
return repository.Exists[entity.User](ctx, r.db, picId)
}
func (r *KandangRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Kandang](ctx, r.db, name, excludeID)
}
+28
View File
@@ -0,0 +1,28 @@
package kandangs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService) {
ctrl := controller.NewKandangController(s)
route := v1.Group("/kandangs")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,169 @@
package service
import (
"errors"
"fmt"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type KandangService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Kandang, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Kandang, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Kandang, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type kandangService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.KandangRepository
}
func NewKandangService(repo repository.KandangRepository, validate *validator.Validate) KandangService {
return &kandangService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").Preload("Location").Preload("Pic")
}
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get kandangs: %+v", err)
return nil, 0, err
}
return kandangs, total, nil
}
func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) {
kandang, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
if err != nil {
s.Log.Errorf("Failed get kandang by id: %+v", err)
return nil, err
}
return kandang, nil
}
func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Kandang, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check kandang name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check kandang name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang with name %s already exists", req.Name))
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
common.RelationCheck{Name: "Pic", ID: &req.PicId, Exists: s.Repository.PicExists},
); err != nil {
return nil, err
}
//TODO: created by dummy
createBody := &entity.Kandang{
Name: req.Name,
LocationId: req.LocationId,
PicId: req.PicId,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create kandang: %+v", err)
return nil, err
}
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
}
func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Kandang, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check kandang name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check kandang name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.LocationId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists}); err != nil {
return nil, err
}
updateBody["location_id"] = *req.LocationId
}
if req.PicId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists}); err != nil {
return nil, err
}
updateBody["pic_id"] = *req.PicId
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
s.Log.Errorf("Failed to update kandang: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s kandangService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
s.Log.Errorf("Failed to delete kandang: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,19 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type LocationController struct {
LocationService service.LocationService
}
func NewLocationController(locationService service.LocationService) *LocationController {
return &LocationController{
LocationService: locationService,
}
}
func (u *LocationController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.LocationService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.LocationListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all locations successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToLocationListDTOs(result),
})
}
func (u *LocationController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.LocationService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get location successfully",
Data: dto.ToLocationListDTO(*result),
})
}
func (u *LocationController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.LocationService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create location successfully",
Data: dto.ToLocationListDTO(*result),
})
}
func (u *LocationController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.LocationService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update location successfully",
Data: dto.ToLocationListDTO(*result),
})
}
func (u *LocationController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.LocationService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete location successfully",
})
}
@@ -0,0 +1,69 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type LocationBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
Area *areaDTO.AreaBaseDTO `json:"area"`
}
type LocationListDTO struct {
LocationBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type LocationDetailDTO struct {
LocationListDTO
}
// === Mapper Functions ===
func ToLocationBaseDTO(e entity.Location) LocationBaseDTO {
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
return LocationBaseDTO{
Id: e.Id,
Name: e.Name,
Address: e.Address,
Area: area,
}
}
func ToLocationListDTO(e entity.Location) LocationListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return LocationListDTO{
LocationBaseDTO: ToLocationBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToLocationListDTOs(e []entity.Location) []LocationListDTO {
result := make([]LocationListDTO, len(e))
for i, r := range e {
result[i] = ToLocationListDTO(r)
}
return result
}
@@ -0,0 +1,26 @@
package locations
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rLocation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/repositories"
sLocation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type LocationModule struct{}
func (LocationModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
locationRepo := rLocation.NewLocationRepository(db)
userRepo := rUser.NewUserRepository(db)
locationService := sLocation.NewLocationService(locationRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
LocationRoutes(router, userService, locationService)
}
@@ -0,0 +1,35 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type LocationRepository interface {
repository.BaseRepository[entity.Location]
AreaExists(ctx context.Context, areaId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type LocationRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Location]
db *gorm.DB
}
func NewLocationRepository(db *gorm.DB) LocationRepository {
return &LocationRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Location](db),
db: db,
}
}
func (r *LocationRepositoryImpl) AreaExists(ctx context.Context, areaId uint) (bool, error) {
return repository.Exists[entity.Area](ctx, r.db, areaId)
}
func (r *LocationRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Location](ctx, r.db, name, excludeID)
}
@@ -0,0 +1,28 @@
package locations
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/controllers"
location "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func LocationRoutes(v1 fiber.Router, u user.UserService, s location.LocationService) {
ctrl := controller.NewLocationController(s)
route := v1.Group("/locations")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,171 @@
package service
import (
"errors"
"fmt"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type LocationService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Location, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Location, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Location, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Location, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type locationService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.LocationRepository
}
func NewLocationService(repo repository.LocationRepository, validate *validator.Validate) LocationService {
return &locationService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s locationService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Area").Preload("CreatedUser")
}
func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Location, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
locations, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get locations: %+v", err)
return nil, 0, err
}
return locations, total, nil
}
func (s locationService) GetOne(c *fiber.Ctx, id uint) (*entity.Location, error) {
location, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Location not found")
}
if err != nil {
s.Log.Errorf("Failed get location by id: %+v", err)
return nil, err
}
return location, nil
}
func (s *locationService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Location, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check location name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check location name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Location with name %s already exists", req.Name))
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
); err != nil {
return nil, err
}
//TODO: created by dummy
createBody := &entity.Location{
Name: req.Name,
Address: req.Address,
AreaId: req.AreaId,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create location: %+v", err)
return nil, err
}
created, err := s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
if err != nil {
s.Log.Errorf("Failed to reload created location: %+v", err)
return nil, err
}
return created, nil
}
func (s locationService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Location, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check location name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check location name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Location with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.Address != nil {
updateBody["address"] = *req.Address
}
if req.AreaId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Area", ID: req.AreaId, Exists: s.Repository.AreaExists}); err != nil {
return nil, err
}
updateBody["area_id"] = *req.AreaId
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Location not found")
}
s.Log.Errorf("Failed to update location: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s locationService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Location not found")
}
s.Log.Errorf("Failed to delete location: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,19 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Address string `json:"address" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Address *string `json:"address,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
+11 -1
View File
@@ -7,15 +7,25 @@ import (
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
areas "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas"
customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers"
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
// MODULE IMPORTS
)
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
group := router.Group("/master")
group := router.Group("/master-data")
allModules := []modules.Module{
uoms.UomModule{},
areas.AreaModule{},
locations.LocationModule{},
kandangs.KandangModule{},
warehouses.WarehouseModule{},
customers.CustomerModule{},
// MODULE REGISTRY
}
+24 -15
View File
@@ -3,20 +3,22 @@ package dto
import (
"time"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/models"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type UomBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Id uint `json:"id"`
Name string `json:"name"`
}
type UomListDTO struct {
UomBaseDTO
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UomDetailDTO struct {
@@ -25,24 +27,31 @@ type UomDetailDTO struct {
// === Mapper Functions ===
func ToUomBaseDTO(m model.Uom) UomBaseDTO {
func ToUomBaseDTO(e entity.Uom) UomBaseDTO {
return UomBaseDTO{
Id: m.Id,
Name: m.Name,
Id: e.Id,
Name: e.Name,
}
}
func ToUomListDTO(m model.Uom) UomListDTO {
func ToUomListDTO(e entity.Uom) UomListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return UomListDTO{
UomBaseDTO: ToUomBaseDTO(m),
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
UomBaseDTO: ToUomBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToUomListDTOs(m []model.Uom) []UomListDTO {
result := make([]UomListDTO, len(m))
for i, r := range m {
func ToUomListDTOs(e []entity.Uom) []UomListDTO {
result := make([]UomListDTO, len(e))
for i, r := range e {
result[i] = ToUomListDTO(r)
}
return result
@@ -1,19 +0,0 @@
package model
import (
"time"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models"
"gorm.io/gorm"
)
type Uom struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
CreatedBy int64 `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser model.User `gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -1,21 +1,30 @@
package repository
import (
model "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/models"
"gitlab.com/mbugroup/lti-api.git/internal/repository"
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type UomRepository interface {
repository.BaseRepository[model.Uom]
repository.BaseRepository[entity.Uom]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type UomRepositoryImpl struct {
*repository.BaseRepositoryImpl[model.Uom]
*repository.BaseRepositoryImpl[entity.Uom]
db *gorm.DB
}
func NewUomRepository(db *gorm.DB) UomRepository {
return &UomRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[model.Uom](db),
BaseRepositoryImpl: repository.NewBaseRepository[entity.Uom](db),
db: db,
}
}
func (r *UomRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Uom](ctx, r.db, name, excludeID)
}
@@ -2,8 +2,9 @@ package service
import (
"errors"
"fmt"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/models"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -15,10 +16,10 @@ import (
)
type UomService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]model.Uom, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*model.Uom, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*model.Uom, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*model.Uom, error)
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Uom, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Uom, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Uom, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Uom, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
@@ -35,7 +36,12 @@ func NewUomService(repo repository.UomRepository, validate *validator.Validate)
Repository: repo,
}
}
func (s uomService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.Uom, int64, error) {
func (s uomService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s uomService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Uom, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
@@ -43,6 +49,7 @@ func (s uomService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.Uom,
offset := (params.Page - 1) * params.Limit
uoms, 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+"%")
}
@@ -56,8 +63,8 @@ func (s uomService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.Uom,
return uoms, total, nil
}
func (s uomService) GetOne(c *fiber.Ctx, id uint) (*model.Uom, error) {
uom, err := s.Repository.GetByID(c.Context(), id, nil)
func (s uomService) GetOne(c *fiber.Ctx, id uint) (*entity.Uom, error) {
uom, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uom not found")
}
@@ -68,12 +75,20 @@ func (s uomService) GetOne(c *fiber.Ctx, id uint) (*model.Uom, error) {
return uom, nil
}
func (s *uomService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.Uom, error) {
func (s *uomService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Uom, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
createBody := &model.Uom{
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check uom name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check uom name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Uom with name %s already exists", req.Name))
}
//TODO: created by dummy
createBody := &entity.Uom{
Name: req.Name,
CreatedBy: 1,
}
@@ -83,10 +98,10 @@ func (s *uomService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.Uom
return nil, err
}
return createBody, nil
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
}
func (s uomService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*model.Uom, error) {
func (s uomService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Uom, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
@@ -94,9 +109,19 @@ func (s uomService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*m
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check uom name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check uom name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Uom with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
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, "Uom not found")
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type WarehouseController struct {
WarehouseService service.WarehouseService
}
func NewWarehouseController(warehouseService service.WarehouseService) *WarehouseController {
return &WarehouseController{
WarehouseService: warehouseService,
}
}
func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.WarehouseService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.WarehouseListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all warehouses successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToWarehouseListDTOs(result),
})
}
func (u *WarehouseController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.WarehouseService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get warehouse successfully",
Data: dto.ToWarehouseListDTO(*result),
})
}
func (u *WarehouseController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.WarehouseService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create warehouse successfully",
Data: dto.ToWarehouseListDTO(*result),
})
}
func (u *WarehouseController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.WarehouseService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update warehouse successfully",
Data: dto.ToWarehouseListDTO(*result),
})
}
func (u *WarehouseController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.WarehouseService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete warehouse successfully",
})
}
@@ -0,0 +1,87 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type WarehouseBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Area *areaDTO.AreaBaseDTO `json:"area"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang"`
}
type WarehouseListDTO struct {
WarehouseBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WarehouseDetailDTO struct {
WarehouseListDTO
}
// === Mapper Functions ===
func ToWarehouseBaseDTO(e entity.Warehouse) WarehouseBaseDTO {
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
var location *locationDTO.LocationBaseDTO
if e.Location != nil && e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(*e.Location)
location = &mapped
}
var kandang *kandangDTO.KandangBaseDTO
if e.Kandang != nil && e.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangBaseDTO(*e.Kandang)
kandang = &mapped
}
return WarehouseBaseDTO{
Id: e.Id,
Name: e.Name,
Type: e.Type,
Area: area,
Location: location,
Kandang: kandang,
}
}
func ToWarehouseListDTO(e entity.Warehouse) WarehouseListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return WarehouseListDTO{
WarehouseBaseDTO: ToWarehouseBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToWarehouseListDTOs(e []entity.Warehouse) []WarehouseListDTO {
result := make([]WarehouseListDTO, len(e))
for i, r := range e {
result[i] = ToWarehouseListDTO(r)
}
return result
}
@@ -0,0 +1,26 @@
package warehouses
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
sWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type WarehouseModule struct{}
func (WarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
warehouseService := sWarehouse.NewWarehouseService(warehouseRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
WarehouseRoutes(router, userService, warehouseService)
}
@@ -0,0 +1,45 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type WarehouseRepository interface {
repository.BaseRepository[entity.Warehouse]
AreaExists(ctx context.Context, areaId uint) (bool, error)
LocationExists(ctx context.Context, locationId uint) (bool, error)
KandangExists(ctx context.Context, kandangId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type WarehouseRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Warehouse]
db *gorm.DB
}
func NewWarehouseRepository(db *gorm.DB) WarehouseRepository {
return &WarehouseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Warehouse](db),
db: db,
}
}
func (r *WarehouseRepositoryImpl) AreaExists(ctx context.Context, areaId uint) (bool, error) {
return repository.Exists[entity.Area](ctx, r.db, areaId)
}
func (r *WarehouseRepositoryImpl) LocationExists(ctx context.Context, locationId uint) (bool, error) {
return repository.Exists[entity.Location](ctx, r.db, locationId)
}
func (r *WarehouseRepositoryImpl) KandangExists(ctx context.Context, kandangId uint) (bool, error) {
return repository.Exists[entity.Kandang](ctx, r.db, kandangId)
}
func (r *WarehouseRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Warehouse](ctx, r.db, name, excludeID)
}
@@ -0,0 +1,28 @@
package warehouses
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/controllers"
warehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func WarehouseRoutes(v1 fiber.Router, u user.UserService, s warehouse.WarehouseService) {
ctrl := controller.NewWarehouseController(s)
route := v1.Group("/warehouses")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,268 @@
package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type WarehouseService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Warehouse, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Warehouse, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Warehouse, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Warehouse, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type warehouseService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.WarehouseRepository
}
func NewWarehouseService(repo repository.WarehouseRepository, validate *validator.Validate) WarehouseService {
return &warehouseService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s warehouseService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").Preload("Area").Preload("Location").Preload("Kandang")
}
func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Warehouse, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get warehouses: %+v", err)
return nil, 0, err
}
return warehouses, total, nil
}
func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) {
warehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
if err != nil {
s.Log.Errorf("Failed get warehouse by id: %+v", err)
return nil, err
}
return warehouse, nil
}
func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Warehouse, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check warehouse name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Warehouse with name %s already exists", req.Name))
}
typ := strings.ToUpper(req.Type)
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, req.LocationId, req.KandangId); err != nil {
return nil, err
}
//? Check relation area, location, and kandang
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists},
common.RelationCheck{Name: "Kandang", ID: req.KandangId, Exists: s.Repository.KandangExists},
); err != nil {
return nil, err
}
//TODO: created by dummy
createBody := &entity.Warehouse{
Name: req.Name,
Type: typ,
AreaId: req.AreaId,
CreatedBy: 1,
}
if req.LocationId != nil {
createBody.LocationId = req.LocationId
}
if req.KandangId != nil {
createBody.KandangId = req.KandangId
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create warehouse: %+v", err)
return nil, err
}
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations)
}
func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Warehouse, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
existing, err := s.GetOne(c, id)
if err != nil {
s.Log.Errorf("Failed to get warehouse for update: %+v", err)
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check warehouse name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Warehouse with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.Type != nil {
normalizedType := strings.ToUpper(*req.Type)
updateBody["type"] = normalizedType
req.Type = &normalizedType
}
if req.AreaId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Area", ID: req.AreaId, Exists: s.Repository.AreaExists}); err != nil {
return nil, err
}
updateBody["area_id"] = *req.AreaId
}
if req.LocationId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists}); err != nil {
return nil, err
}
updateBody["location_id"] = req.LocationId
}
if req.KandangId != nil {
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Kandang", ID: req.KandangId, Exists: s.Repository.KandangExists}); err != nil {
return nil, err
}
updateBody["kandang_id"] = req.KandangId
}
finalType := strings.ToUpper(existing.Type)
if req.Type != nil {
finalType = *req.Type
}
finalAreaId := existing.AreaId
if req.AreaId != nil {
finalAreaId = *req.AreaId
}
finalLocationId := existing.LocationId
if req.LocationId != nil {
finalLocationId = req.LocationId
}
finalKandangId := existing.KandangId
if req.KandangId != nil {
finalKandangId = req.KandangId
}
if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, finalLocationId, finalKandangId); err != nil {
return nil, err
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
s.Log.Errorf("Failed to update warehouse: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
s.Log.Errorf("Failed to delete warehouse: %+v", err)
return err
}
return nil
}
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID *uint, kandangID *uint) error {
switch utils.WarehouseType(typ) {
case utils.WarehouseTypeArea:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is AREA")
}
if locationID != nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
}
if kandangID != nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
}
return nil
case utils.WarehouseTypeLokasi:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is LOCATION")
}
if locationID == nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is LOCATION")
}
if *locationID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is LOCATION")
}
if kandangID != nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
}
return nil
case utils.WarehouseTypeKandang:
if areaID == nil || *areaID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is KANDANG")
}
if locationID == nil {
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is KANDANG")
}
if *locationID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is KANDANG")
}
if kandangID == nil {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id is required when type is KANDANG")
}
if *kandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must be greater than 0 when type is KANDANG")
}
return nil
default:
return fiber.NewError(fiber.StatusBadRequest, "Invalid warehouse type")
}
}
@@ -0,0 +1,23 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Type string `json:"type" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Type *string `json:"type,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
+8 -6
View File
@@ -3,7 +3,7 @@ package dto
import (
"time"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
// === DTO Structs ===
@@ -27,14 +27,16 @@ type UserDetailDTO struct {
// === Mapper Functions ===
func ToUserBaseDTO(m model.User) UserBaseDTO {
func ToUserBaseDTO(m entity.User) UserBaseDTO {
return UserBaseDTO{
Id: m.Id,
Name: m.Name,
Id: m.Id,
IdUser: m.IdUser,
Email: m.Email,
Name: m.Name,
}
}
func ToUserListDTO(m model.User) UserListDTO {
func ToUserListDTO(m entity.User) UserListDTO {
return UserListDTO{
UserBaseDTO: ToUserBaseDTO(m),
CreatedAt: m.CreatedAt,
@@ -42,7 +44,7 @@ func ToUserListDTO(m model.User) UserListDTO {
}
}
func ToUserListDTOs(m []model.User) []UserListDTO {
func ToUserListDTOs(m []entity.User) []UserListDTO {
result := make([]UserListDTO, len(m))
for i, r := range m {
result[i] = ToUserListDTO(r)
@@ -1,17 +0,0 @@
package model
import (
"time"
"gorm.io/gorm"
)
type User struct {
Id uint `gorm:"primaryKey"`
IdUser int64 `gorm:"uniqueIndex"`
Email string `gorm:"uniqueIndex"`
Name string `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
@@ -1,21 +1,21 @@
package repository
import (
model "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models"
"gitlab.com/mbugroup/lti-api.git/internal/repository"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type UserRepository interface {
repository.BaseRepository[model.User]
repository.BaseRepository[entity.User]
}
type UserRepositoryImpl struct {
*repository.BaseRepositoryImpl[model.User]
*repository.BaseRepositoryImpl[entity.User]
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &UserRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[model.User](db),
BaseRepositoryImpl: repository.NewBaseRepository[entity.User](db),
}
}
+15 -11
View File
@@ -3,7 +3,7 @@ package service
import (
"errors"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/users/models"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/users/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -15,10 +15,10 @@ import (
)
type UserService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]model.User, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*model.User, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*model.User, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*model.User, error)
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.User, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.User, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.User, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.User, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
@@ -35,7 +35,7 @@ func NewUserService(repo repository.UserRepository, validate *validator.Validate
Repository: repo,
}
}
func (s userService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.User, int64, error) {
func (s userService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.User, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
@@ -56,7 +56,7 @@ func (s userService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.Use
return users, total, nil
}
func (s userService) GetOne(c *fiber.Ctx, id uint) (*model.User, error) {
func (s userService) GetOne(c *fiber.Ctx, id uint) (*entity.User, error) {
user, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
@@ -68,13 +68,13 @@ func (s userService) GetOne(c *fiber.Ctx, id uint) (*model.User, error) {
return user, nil
}
func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.User, error) {
func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.User, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
createBody := &model.User{
Name: req.Name,
createBody := &entity.User{
Name: req.Name,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -85,7 +85,7 @@ func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.Us
return createBody, nil
}
func (s userService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*model.User, error) {
func (s userService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.User, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
@@ -96,6 +96,10 @@ func (s userService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*
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, "User not found")
+3 -1
View File
@@ -1,8 +1,9 @@
package route
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/validation"
"gitlab.com/mbugroup/lti-api.git/internal/modules"
"gitlab.com/mbugroup/lti-api.git/internal/validation"
trim "gitlab.com/mbugroup/lti-api.git/internal/middleware/trim"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
@@ -15,6 +16,7 @@ import (
func Routes(app *fiber.App, db *gorm.DB) {
validate := validation.Validator()
api := app.Group("/api")
api.Use(trim.JSONBody())
// root modules di sini
allModules := []modules.Module{
+39
View File
@@ -9,6 +9,29 @@ const (
FlagIsActive FlagType = "IS_ACTIVE"
)
// -------------------------------------------------------------------
// WarehouseType
// -------------------------------------------------------------------
type WarehouseType string
const (
WarehouseTypeArea WarehouseType = "AREA"
WarehouseTypeLokasi WarehouseType = "LOKASI"
WarehouseTypeKandang WarehouseType = "KANDANG"
)
// -------------------------------------------------------------------
// WarehouseType
// -------------------------------------------------------------------
type CustomerSupplierType string
const (
CustomerSupplierTypeBisnis CustomerSupplierType = "BISNIS"
CustomerSupplierTypeIndividual CustomerSupplierType = "INDIVIDUAL"
)
// -------------------------------------------------------------------
// Validators
// -------------------------------------------------------------------
@@ -21,6 +44,22 @@ func IsValidFlagType(v string) bool {
return false
}
func IsValidWarehouseType(v string) bool {
switch WarehouseType(v) {
case WarehouseTypeArea, WarehouseTypeLokasi, WarehouseTypeKandang:
return true
}
return false
}
func IsValidCustomerSupplierType(v string) bool {
switch CustomerSupplierType(v) {
case CustomerSupplierTypeBisnis, CustomerSupplierTypeIndividual:
return true
}
return false
}
// example use
/**
+1 -1
View File
@@ -3,8 +3,8 @@ package utils
import (
"errors"
"gitlab.com/mbugroup/lti-api.git/internal/common/validation"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"gitlab.com/mbugroup/lti-api.git/internal/validation"
"github.com/gofiber/fiber/v2"
)
+13
View File
@@ -0,0 +1,13 @@
package utils
import "strings"
// NormalizeTrim returns the input string without leading/trailing whitespace.
func NormalizeTrim(value string) string {
return strings.TrimSpace(value)
}
// NormalizeUpper returns the trimmed, upper-case version of value.
func NormalizeUpper(value string) string {
return strings.ToUpper(NormalizeTrim(value))
}
+27
View File
@@ -0,0 +1,27 @@
package test
import (
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestAreaIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
t.Run("create area trims name", func(t *testing.T) {
areaID := createArea(t, app, " Area Trim ")
if got := fetchAreaName(t, db, areaID); got != "Area Trim" {
t.Fatalf("expected trimmed name, got %q", got)
}
})
t.Run("duplicate area returns conflict", func(t *testing.T) {
createArea(t, app, "Duplicate Area")
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/areas", map[string]any{"name": "Duplicate Area"})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 conflict, got %d: %s", resp.StatusCode, string(body))
}
})
}
@@ -0,0 +1,189 @@
package test
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
func TestCustomerIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
t.Run("creating customer without existing pic fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{
"name": "Invalid Customer",
"pic_id": 9999,
"type": utils.CustomerSupplierTypeBisnis,
"address": "Somewhere",
"phone": "0800000000",
"email": "invalid@example.com",
"account_number": "ACC-INVALID",
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when pic is missing, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("creating customer with invalid type fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{
"name": "Invalid Type",
"pic_id": 1,
"type": "UNKNOWN",
"address": "Somewhere",
"phone": "081234567891",
"email": "invalid-type@example.com",
"account_number": "ACC-INVALID-TYPE",
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when type is invalid, got %d: %s", resp.StatusCode, string(body))
}
})
const customerName = "Customer Alpha"
var customerID uint
t.Run("creating customer succeeds", func(t *testing.T) {
customerID = createCustomer(t, app, customerName, 1)
customer := fetchCustomer(t, db, customerID)
if customer.Name != customerName {
t.Fatalf("expected name %q, got %q", customerName, customer.Name)
}
if customer.PicId != 1 {
t.Fatalf("expected pic id 1, got %d", customer.PicId)
}
if customer.Type != string(utils.CustomerSupplierTypeBisnis) {
t.Fatalf("expected type %q, got %q", utils.CustomerSupplierTypeBisnis, customer.Type)
}
})
t.Run("creating customer with duplicate name fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{
"name": customerName,
"pic_id": 1,
"type": utils.CustomerSupplierTypeBisnis,
"address": "Duplicate address",
"phone": "0811111111",
"email": "duplicate@example.com",
"account_number": "ACC-DUP",
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when creating duplicate customer, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("getting existing customer succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching customer, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Id uint `json:"id"`
Name string `json:"name"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse customer response: %v", err)
}
if payload.Data.Id != customerID {
t.Fatalf("expected id %d, got %d", customerID, payload.Data.Id)
}
if payload.Data.Name != customerName {
t.Fatalf("expected name %q, got %q", customerName, payload.Data.Name)
}
})
const updatedName = "Customer Gamma"
t.Run("updating customer name succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{
"name": updatedName,
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating customer, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Name string `json:"name"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse update response: %v", err)
}
if payload.Data.Name != updatedName {
t.Fatalf("expected updated name %q, got %q", updatedName, payload.Data.Name)
}
customer := fetchCustomer(t, db, customerID)
if customer.Name != updatedName {
t.Fatalf("expected persisted name %q, got %q", updatedName, customer.Name)
}
})
t.Run("updating customer type succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{
"type": utils.CustomerSupplierTypeIndividual,
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating customer type, got %d: %s", resp.StatusCode, string(body))
}
customer := fetchCustomer(t, db, customerID)
if customer.Type != string(utils.CustomerSupplierTypeIndividual) {
t.Fatalf("expected persisted type %q, got %q", utils.CustomerSupplierTypeIndividual, customer.Type)
}
})
t.Run("updating customer with invalid type fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{
"type": "random-type",
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating with invalid type, got %d: %s", resp.StatusCode, string(body))
}
customer := fetchCustomer(t, db, customerID)
if customer.Type != string(utils.CustomerSupplierTypeIndividual) {
t.Fatalf("expected type to remain %q after invalid update, got %q", utils.CustomerSupplierTypeIndividual, customer.Type)
}
})
t.Run("updating non existent customer returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, "/api/master-data/customers/9999", map[string]any{
"name": "Does Not Matter",
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when updating missing customer, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("deleting customer succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting customer, got %d: %s", resp.StatusCode, string(body))
}
var customer entities.Customer
if err := db.First(&customer, customerID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("expected customer to be deleted, got error %v", err)
}
})
t.Run("deleting non existent customer returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when deleting missing customer, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("getting deleted customer returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when fetching deleted customer, got %d: %s", resp.StatusCode, string(body))
}
})
}
@@ -0,0 +1,36 @@
package test
import (
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestKandangIntegration(t *testing.T) {
app, _ := setupIntegrationApp(t)
areaID := createArea(t, app, "Area Kandang")
locationID := createLocation(t, app, "Location For Kandang", "Address", areaID)
t.Run("create kandang success", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{
"name": "Kandang OK",
"location_id": locationID,
"pic_id": 1,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("create kandang with unknown location fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{
"name": "Kandang Fail",
"location_id": 999,
"pic_id": 1,
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404, got %d: %s", resp.StatusCode, string(body))
}
})
}
@@ -0,0 +1,35 @@
package test
import (
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestLocationIntegration(t *testing.T) {
app, _ := setupIntegrationApp(t)
t.Run("creating location without existing area fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/locations", map[string]any{
"name": "Loc A",
"address": "Address",
"area_id": 999, // non-existent
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("creating location succeeds", func(t *testing.T) {
areaID := createArea(t, app, "Area For Location")
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/locations", map[string]any{
"name": "Location OK",
"address": "Addr",
"area_id": areaID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
})
}
+188
View File
@@ -0,0 +1,188 @@
package test
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"github.com/glebarez/sqlite"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/route"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
func setupIntegrationApp(t *testing.T) (*fiber.App, *gorm.DB) {
t.Helper()
dir := t.TempDir()
dsn := filepath.Join(dir, "integration.db")
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatalf("failed to open sqlite database: %v", err)
}
if err := db.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
t.Fatalf("failed to enable foreign keys: %v", err)
}
if err := db.AutoMigrate(
&entities.User{},
&entities.Area{},
&entities.Location{},
&entities.Kandang{},
&entities.Warehouse{},
&entities.Uom{},
&entities.Customer{},
); err != nil {
t.Fatalf("auto migrate failed: %v", err)
}
seedUser := entities.User{
Id: 1,
IdUser: 1001,
Email: "tester@example.com",
Name: "Tester",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := db.Create(&seedUser).Error; err != nil {
t.Fatalf("failed to seed user: %v", err)
}
app := fiber.New()
route.Routes(app, db)
return app, db
}
func doJSONRequest(t *testing.T, app *fiber.App, method, path string, payload any) (*http.Response, []byte) {
t.Helper()
var body io.Reader
if payload != nil {
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(payload); err != nil {
t.Fatalf("failed to encode payload: %v", err)
}
body = buf
}
req := httptest.NewRequest(method, path, body)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("failed to read response body: %v", err)
}
return resp, data
}
func parseID(t *testing.T, body []byte) uint {
t.Helper()
var resp struct {
Data struct {
Id uint `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal(body, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
return resp.Data.Id
}
func createArea(t *testing.T, app *fiber.App, name string) uint {
t.Helper()
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/areas", map[string]any{"name": name})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating area, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func createLocation(t *testing.T, app *fiber.App, name, address string, areaID uint) uint {
t.Helper()
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/locations", map[string]any{
"name": name,
"address": address,
"area_id": areaID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating location, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func createKandang(t *testing.T, app *fiber.App, name string, locationID, picID uint) uint {
t.Helper()
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{
"name": name,
"location_id": locationID,
"pic_id": picID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating kandang, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func createCustomer(t *testing.T, app *fiber.App, name string, picID uint) uint {
t.Helper()
identifier := strings.ToLower(strings.ReplaceAll(name, " ", "_"))
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{
"name": name,
"pic_id": picID,
"type": utils.CustomerSupplierTypeBisnis,
"address": "Customer address",
"phone": "081234567890",
"email": fmt.Sprintf("%s@example.com", identifier),
"account_number": fmt.Sprintf("ACC-%s", identifier),
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating customer, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func fetchCustomer(t *testing.T, db *gorm.DB, id uint) entities.Customer {
t.Helper()
var customer entities.Customer
if err := db.Preload("Pic").Preload("CreatedUser").First(&customer, id).Error; err != nil {
t.Fatalf("failed to fetch customer: %v", err)
}
return customer
}
func fetchAreaName(t *testing.T, db *gorm.DB, id uint) string {
t.Helper()
var area entities.Area
if err := db.First(&area, id).Error; err != nil {
t.Fatalf("failed to fetch area: %v", err)
}
return area.Name
}
func fetchWarehouse(t *testing.T, db *gorm.DB, id uint) entities.Warehouse {
t.Helper()
var wh entities.Warehouse
if err := db.First(&wh, id).Error; err != nil {
t.Fatalf("failed to fetch warehouse: %v", err)
}
return wh
}
@@ -0,0 +1,96 @@
package test
import (
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestWarehouseIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
areaID := createArea(t, app, "Warehouse Area")
locationID := createLocation(t, app, "Location WH", "Addr", areaID)
kandangID := createKandang(t, app, "Kandang WH", locationID, 1)
t.Run("type AREA only needs area_id", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Area",
"type": "AREA",
"area_id": areaID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("type AREA rejects location_id", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Area Invalid",
"type": "AREA",
"area_id": areaID,
"location_id": locationID,
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("type LOKASI requires location_id", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Lokasi Fail",
"type": "LOKASI",
"area_id": areaID,
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("type LOKASI success", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Lokasi",
"type": "LOKASI",
"area_id": areaID,
"location_id": locationID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
whID := parseID(t, body)
wh := fetchWarehouse(t, db, whID)
if wh.LocationId == nil || *wh.LocationId != locationID {
t.Fatalf("expected location_id %d, got %v", locationID, wh.LocationId)
}
})
t.Run("type KANDANG requires all ids", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Kandang Fail",
"type": "KANDANG",
"area_id": areaID,
"location_id": locationID,
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("type KANDANG success", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{
"name": "WH Kandang",
"type": "KANDANG",
"area_id": areaID,
"location_id": locationID,
"kandang_id": kandangID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
}
whID := parseID(t, body)
wh := fetchWarehouse(t, db, whID)
if wh.KandangId == nil || *wh.KandangId != kandangID {
t.Fatalf("expected kandang_id %d, got %v", kandangID, wh.KandangId)
}
})
}
+5 -4
View File
@@ -44,10 +44,11 @@ func main() {
TplName string
}{
{
TplPath: "tools/templates/model.tmpl",
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "models"),
OutSuffix: ".model.go",
TplName: "model",
TplPath: "tools/templates/entity.tmpl",
// Centralize entities at internal/entities
OutDir: filepath.Join("internal", "entities"),
OutSuffix: ".go",
TplName: "entity",
},
{
TplPath: "tools/templates/validation.tmpl",
+20 -11
View File
@@ -3,7 +3,8 @@
import (
"time"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/models"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
@@ -15,6 +16,7 @@ type {{Pascal .Entity}}BaseDTO struct {
type {{Pascal .Entity}}ListDTO struct {
{{Pascal .Entity}}BaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -25,24 +27,31 @@ type {{Pascal .Entity}}DetailDTO struct {
// === Mapper Functions ===
func To{{Pascal .Entity}}BaseDTO(m model.{{Pascal .Entity}}) {{Pascal .Entity}}BaseDTO {
func To{{Pascal .Entity}}BaseDTO(e entity.{{Pascal .Entity}}) {{Pascal .Entity}}BaseDTO {
return {{Pascal .Entity}}BaseDTO{
Id: m.Id,
Name: m.Name,
Id: e.Id,
Name: e.Name,
}
}
func To{{Pascal .Entity}}ListDTO(m model.{{Pascal .Entity}}) {{Pascal .Entity}}ListDTO {
func To{{Pascal .Entity}}ListDTO(e entity.{{Pascal .Entity}}) {{Pascal .Entity}}ListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return {{Pascal .Entity}}ListDTO{
{{Pascal .Entity}}BaseDTO: To{{Pascal .Entity}}BaseDTO(m),
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
{{Pascal .Entity}}BaseDTO: To{{Pascal .Entity}}BaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func To{{Pascal .Entity}}ListDTOs(m []model.{{Pascal .Entity}}) []{{Pascal .Entity}}ListDTO {
result := make([]{{Pascal .Entity}}ListDTO, len(m))
for i, r := range m {
func To{{Pascal .Entity}}ListDTOs(e []entity.{{Pascal .Entity}}) []{{Pascal .Entity}}ListDTO {
result := make([]{{Pascal .Entity}}ListDTO, len(e))
for i, r := range e {
result[i] = To{{Pascal .Entity}}ListDTO(r)
}
return result
+19
View File
@@ -0,0 +1,19 @@
{{define "entity"}}package entities
import (
"time"
"gorm.io/gorm"
)
type {{Pascal .Entity}} struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
{{end}}
-17
View File
@@ -1,17 +0,0 @@
{{define "model"}}package model
import (
"time"
)
type {{Pascal .Entity}} struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
CreatedBy int64 `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser model.User `gorm:"foreignKey:CreatedBy;references:Id"`
}
{{end}}
+5 -5
View File
@@ -1,22 +1,22 @@
{{define "repository"}}package repository
import (
model "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/models"
"gitlab.com/mbugroup/lti-api.git/internal/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type {{Pascal .Entity}}Repository interface {
repository.BaseRepository[model.{{Pascal .Entity}}]
repository.BaseRepository[entity.{{Pascal .Entity}}]
}
type {{Pascal .Entity}}RepositoryImpl struct {
*repository.BaseRepositoryImpl[model.{{Pascal .Entity}}]
*repository.BaseRepositoryImpl[entity.{{Pascal .Entity}}]
}
func New{{Pascal .Entity}}Repository(db *gorm.DB) {{Pascal .Entity}}Repository {
return &{{Pascal .Entity}}RepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[model.{{Pascal .Entity}}](db),
BaseRepositoryImpl: repository.NewBaseRepository[entity.{{Pascal .Entity}}](db),
}
}
{{end}}
+21 -11
View File
@@ -3,7 +3,7 @@
import (
"errors"
model "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/models"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -15,10 +15,10 @@ import (
)
type {{Pascal .Entity}}Service interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]model.{{Pascal .Entity}}, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*model.{{Pascal .Entity}}, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*model.{{Pascal .Entity}}, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*model.{{Pascal .Entity}}, error)
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.{{Pascal .Entity}}, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.{{Pascal .Entity}}, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.{{Pascal .Entity}}, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.{{Pascal .Entity}}, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
@@ -35,7 +35,12 @@ func New{{Pascal .Entity}}Service(repo repository.{{Pascal .Entity}}Repository,
Repository: repo,
}
}
func (s {{Camel .Entity}}Service) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.{{Pascal .Entity}}, int64, error) {
func (s {{Camel .Entity}}Service) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s {{Camel .Entity}}Service) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.{{Pascal .Entity}}, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
@@ -43,6 +48,7 @@ func (s {{Camel .Entity}}Service) GetAll(c *fiber.Ctx, params *validation.Query)
offset := (params.Page - 1) * params.Limit
{{Camel .Entity}}s, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
@@ -56,8 +62,8 @@ func (s {{Camel .Entity}}Service) GetAll(c *fiber.Ctx, params *validation.Query)
return {{Camel .Entity}}s, total, nil
}
func (s {{Camel .Entity}}Service) GetOne(c *fiber.Ctx, id uint) (*model.{{Pascal .Entity}}, error) {
{{Camel .Entity}}, err := s.Repository.GetByID(c.Context(), id, nil)
func (s {{Camel .Entity}}Service) GetOne(c *fiber.Ctx, id uint) (*entity.{{Pascal .Entity}}, error) {
{{Camel .Entity}}, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "{{Pascal .Entity}} not found")
}
@@ -68,12 +74,12 @@ func (s {{Camel .Entity}}Service) GetOne(c *fiber.Ctx, id uint) (*model.{{Pascal
return {{Camel .Entity}}, nil
}
func (s *{{Camel .Entity}}Service) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.{{Pascal .Entity}}, error) {
func (s *{{Camel .Entity}}Service) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.{{Pascal .Entity}}, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
createBody := &model.{{Pascal .Entity}}{
createBody := &entity.{{Pascal .Entity}}{
Name: req.Name,
}
@@ -85,7 +91,7 @@ func (s *{{Camel .Entity}}Service) CreateOne(c *fiber.Ctx, req *validation.Creat
return createBody, nil
}
func (s {{Camel .Entity}}Service) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*model.{{Pascal .Entity}}, error) {
func (s {{Camel .Entity}}Service) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.{{Pascal .Entity}}, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
@@ -96,6 +102,10 @@ func (s {{Camel .Entity}}Service) UpdateOne(c *fiber.Ctx, req *validation.Update
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, "{{Pascal .Entity}} not found")