Feat(BE-36,37,38,39): finish master data management api

This commit is contained in:
Hafizh A. Y
2025-10-03 21:04:21 +07:00
parent e8905be856
commit 2d49ffe4cd
103 changed files with 6974 additions and 117 deletions
-2
View File
@@ -60,11 +60,9 @@ func setupFiberApp() *fiber.App {
app := fiber.New(config.FiberConfig()) app := fiber.New(config.FiberConfig())
// Middleware setup // Middleware setup
app.Use("/api/auth", middleware.LimiterConfig())
app.Use(middleware.LoggerConfig()) app.Use(middleware.LoggerConfig())
app.Use(helmet.New()) app.Use(helmet.New())
app.Use(compress.New()) app.Use(compress.New())
app.Use(cors.New())
app.Use(middleware.RecoverConfig()) app.Use(middleware.RecoverConfig())
origins := "*" origins := "*"
@@ -1,9 +1,12 @@
DROP TABLE IF EXISTS fcr_standards; DROP TABLE IF EXISTS fcr_standards;
DROP INDEX IF EXISTS suppliers_name_unique; DROP INDEX IF EXISTS suppliers_name_unique;
DROP TABLE IF EXISTS product_suppliers;
DROP INDEX IF EXISTS products_sku_unique; DROP INDEX IF EXISTS products_sku_unique;
DROP INDEX IF EXISTS products_name_unique; DROP INDEX IF EXISTS products_name_unique;
DROP TABLE IF EXISTS products; DROP TABLE IF EXISTS products;
DROP INDEX IF EXISTS flags_flagable_lookup;
DROP INDEX IF EXISTS flags_unique_flagable;
DROP TABLE IF EXISTS flags; DROP TABLE IF EXISTS flags;
DROP INDEX IF EXISTS customers_name_unique; DROP INDEX IF EXISTS customers_name_unique;
DROP INDEX IF EXISTS customers_email_unique; DROP INDEX IF EXISTS customers_email_unique;
@@ -13,6 +16,7 @@ DROP INDEX IF EXISTS product_categories_code_unique;
DROP INDEX IF EXISTS product_categories_name_unique; DROP INDEX IF EXISTS product_categories_name_unique;
DROP TABLE IF EXISTS product_categories; DROP TABLE IF EXISTS product_categories;
DROP INDEX IF EXISTS nonstocks_name_unique; DROP INDEX IF EXISTS nonstocks_name_unique;
DROP TABLE IF EXISTS nonstock_suppliers;
DROP TABLE IF EXISTS nonstocks; DROP TABLE IF EXISTS nonstocks;
DROP INDEX IF EXISTS banks_name_unique; DROP INDEX IF EXISTS banks_name_unique;
DROP TABLE IF EXISTS banks; DROP TABLE IF EXISTS banks;
@@ -19,15 +19,17 @@ CREATE TABLE flags (
flagable_id BIGINT NOT NULL, flagable_id BIGINT NOT NULL,
flagable_type VARCHAR(50) NOT NULL, flagable_type VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
deleted_at TIMESTAMPTZ
); );
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
-- PRODUCT CATEGORIES -- PRODUCT CATEGORIES
CREATE TABLE product_categories ( CREATE TABLE product_categories (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
code VARCHAR(3) NOT NULL, code VARCHAR(10) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
@@ -47,26 +49,6 @@ CREATE TABLE uoms (
); );
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX uoms_name_unique ON uoms (name) WHERE deleted_at IS NULL;
-- PRODUCTS
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
brand VARCHAR NOT NULL,
sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_category_id BIGINT NOT NULL REFERENCES product_categories(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_price NUMERIC(15,3) NOT NULL,
selling_price NUMERIC(15,3),
tax NUMERIC(15,3),
expiry_period INT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
-- BANKS -- BANKS
CREATE TABLE banks ( CREATE TABLE banks (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
@@ -192,6 +174,7 @@ CREATE TABLE suppliers (
alias VARCHAR(5) NOT NULL, alias VARCHAR(5) NOT NULL,
pic VARCHAR NOT NULL, pic VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
category VARCHAR(20) NOT NULL,
hatchery VARCHAR, hatchery VARCHAR,
phone VARCHAR(20) NOT NULL, phone VARCHAR(20) NOT NULL,
email VARCHAR NOT NULL, email VARCHAR NOT NULL,
@@ -207,6 +190,40 @@ CREATE TABLE suppliers (
); );
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL;
CREATE TABLE nonstock_suppliers (
nonstock_id BIGINT NOT NULL REFERENCES nonstocks(id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (nonstock_id, supplier_id)
);
-- PRODUCTS
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
brand VARCHAR NOT NULL,
sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_category_id BIGINT NOT NULL REFERENCES product_categories(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_price NUMERIC(15,3) NOT NULL,
selling_price NUMERIC(15,3),
tax NUMERIC(15,3),
expiry_period INT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
CREATE TABLE product_suppliers (
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (product_id, supplier_id)
);
-- PROJECTS -- PROJECTS
CREATE TABLE projects ( CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
+758 -7
View File
@@ -1,26 +1,777 @@
package seed package seed
import ( import (
"errors"
"fmt" "fmt"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm" "gorm.io/gorm"
) )
func Run(db *gorm.DB) error { func Run(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error { return db.Transaction(func(tx *gorm.DB) error {
// ===== Users (user) ===== users, err := seedUsers(tx)
user := entity.User{ if err != nil {
Email: "admin@mbugroup.id", return err
IdUser: 1,
Name: "Super Admin",
} }
if err := tx.Where("email = ?", user.Email).FirstOrCreate(&user).Error; err != nil { adminID := users["admin"]
uoms, err := seedUoms(tx, adminID)
if err != nil {
return err return err
} }
fmt.Println("✅ Seeder successfully") areas, err := seedAreas(tx, adminID)
if err != nil {
return err
}
locations, err := seedLocations(tx, adminID, areas)
if err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users)
if err != nil {
return err
}
if err := seedWarehouses(tx, adminID, areas, locations, kandangs); err != nil {
return err
}
productCategories, err := seedProductCategories(tx, adminID)
if err != nil {
return err
}
suppliers, err := seedSuppliers(tx, adminID)
if err != nil {
return err
}
if err := seedCustomers(tx, adminID, users); err != nil {
return err
}
if err := seedFcr(tx, adminID); err != nil {
return err
}
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
return err
}
if err := seedNonstocks(tx, adminID, uoms, suppliers); err != nil {
return err
}
if err := seedBanks(tx, adminID); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed")
return nil return nil
}) })
} }
func seedUsers(tx *gorm.DB) (map[string]uint, error) {
seeds := []struct {
Key string
Data entity.User
}{
{
Key: "admin",
Data: entity.User{Email: "admin@mbugroup.id", IdUser: 1, Name: "Super Admin"},
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
var user entity.User
err := tx.Where("email = ?", seed.Data.Email).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
user = seed.Data
if err := tx.Create(&user).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Key] = user.Id
}
return result, nil
}
func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Kilogram", "Gram", "Liter", "Unit", "Ekor"}
result := make(map[string]uint, len(names))
for _, name := range names {
var uom entity.Uom
err := tx.Where("name = ?", name).First(&uom).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
uom = entity.Uom{Name: name, CreatedBy: createdBy}
if err := tx.Create(&uom).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[name] = uom.Id
}
return result, nil
}
func seedAreas(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Priangan", "Banten"}
result := make(map[string]uint, len(names))
for _, name := range names {
var area entity.Area
err := tx.Where("name = ?", name).First(&area).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
area = entity.Area{Name: name, CreatedBy: createdBy}
if err := tx.Create(&area).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[name] = area.Id
}
return result, nil
}
func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Address string
Area string
}{
{"Singaparna", "Tasik", "Priangan"},
{"Cikaum", "Cikaum", "Banten"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
areaID, ok := areas[seed.Area]
if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area)
}
var loc entity.Location
err := tx.Where("name = ?", seed.Name).First(&loc).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
loc = entity.Location{
Name: seed.Name,
Address: seed.Address,
AreaId: areaID,
CreatedBy: createdBy,
}
if err := tx.Create(&loc).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = loc.Id
}
return result, nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Location string
PicKey string
}{
{"Singaparna 1", "Singaparna", "admin"},
{"Singaparna 2", "Singaparna", "admin"},
{"Cikaum 1", "Cikaum", "admin"},
{"Cikaum 2", "Cikaum", "admin"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
locID, ok := locations[seed.Location]
if !ok {
return nil, fmt.Errorf("location %s not seeded", seed.Location)
}
picID, ok := users[seed.PicKey]
if !ok {
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
}
var kandang entity.Kandang
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
kandang = entity.Kandang{
Name: seed.Name,
LocationId: locID,
PicId: picID,
CreatedBy: createdBy,
}
if err := tx.Create(&kandang).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = kandang.Id
}
return result, nil
}
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct {
Name string
Type string
Area string
Location *string
Kandang *string
}{
{Name: "Gudang Priangan", Type: string(utils.WarehouseTypeArea), Area: "Priangan"},
{Name: "Gudang Singaparna", Type: string(utils.WarehouseTypeLokasi), Area: "Priangan", Location: strPtr("Singaparna")},
{Name: "Gudang Singaparna 1", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 1")},
{Name: "Gudang Singaparna 2", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 2")},
{Name: "Gudang Banten", Type: string(utils.WarehouseTypeArea), Area: "Banten"},
{Name: "Gudang Cikaum", Type: string(utils.WarehouseTypeLokasi), Area: "Banten", Location: strPtr("Cikaum")},
{Name: "Gudang Cikaum 1", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 1")},
{Name: "Gudang Cikaum 2", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 2")},
}
for _, seed := range seeds {
areaID, ok := areas[seed.Area]
if !ok {
return fmt.Errorf("area %s not seeded", seed.Area)
}
var warehouse entity.Warehouse
err := tx.Where("name = ?", seed.Name).First(&warehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
warehouse = entity.Warehouse{
Name: seed.Name,
Type: seed.Type,
AreaId: areaID,
CreatedBy: createdBy,
}
} else if err != nil {
return err
}
if seed.Location != nil {
locID, ok := locations[*seed.Location]
if !ok {
return fmt.Errorf("location %s not seeded", *seed.Location)
}
warehouse.LocationId = uintPtr(locID)
}
if seed.Kandang != nil {
kandangID, ok := kandangs[*seed.Kandang]
if !ok {
return fmt.Errorf("kandang %s not seeded", *seed.Kandang)
}
warehouse.KandangId = uintPtr(kandangID)
}
if warehouse.Id == 0 {
if err := tx.Create(&warehouse).Error; err != nil {
return err
}
} else {
if err := tx.Model(&entity.Warehouse{}).Where("id = ?", warehouse.Id).Updates(map[string]any{
"type": warehouse.Type,
"area_id": warehouse.AreaId,
"location_id": warehouse.LocationId,
"kandang_id": warehouse.KandangId,
}).Error; err != nil {
return err
}
}
}
return nil
}
func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct {
Name string
Code string
}{
{"Bahan Baku", "RAW"},
{"Day Old Chick", "DOC"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
var category entity.ProductCategory
err := tx.Where("name = ?", seed.Name).First(&category).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
category = entity.ProductCategory{Name: seed.Name, Code: seed.Code, CreatedBy: createdBy}
if err := tx.Create(&category).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.ProductCategory{}).Where("id = ?", category.Id).Updates(map[string]any{
"code": seed.Code,
}).Error; err != nil {
return nil, err
}
}
result[seed.Name] = category.Id
}
return result, nil
}
func seedSuppliers(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct {
Name string
Alias string
Category string
Email string
Phone string
Address string
}{
{"PT CHAROEN POKPHAND INDONESIA Tbk", "CPI", string(utils.SupplierCategorySapronak), "cpi@gmail.com", "081200000001", "Jl. Pakan 1, Bekasi"},
{"BOP Vendor", "BOP", string(utils.SupplierCategoryBOP), "bop@gmail.com", "081200000002", "Jl. Veteriner 3, Bogor"},
{"Ekspedisi", "EKS", string(utils.SupplierCategoryBOP), "bop@gmail.com", "081200000002", "Jl. Veteriner 3, Bogor"},
}
result := make(map[string]uint, len(seeds))
for idx, seed := range seeds {
var supplier entity.Supplier
err := tx.Where("name = ?", seed.Name).First(&supplier).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
supplier = entity.Supplier{
Name: seed.Name,
Alias: seed.Alias,
Pic: "John Doe",
Type: string(utils.CustomerSupplierTypeBisnis),
Category: seed.Category,
Phone: seed.Phone,
Email: seed.Email,
Address: seed.Address,
DueDate: 30,
CreatedBy: createdBy,
AccountNumber: strPtr(fmt.Sprintf("%03d", idx+1)),
}
if err := tx.Create(&supplier).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = supplier.Id
}
return result, nil
}
func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
seeds := []struct {
Name string
PicKey string
Address string
Phone string
Email string
}{
{"Abdul Azis", "admin", "Jl. Raya Utama 1, Bekasi", "082100000001", "abdul.azis@gmail.com"},
}
for idx, seed := range seeds {
picID, ok := users[seed.PicKey]
if !ok {
return fmt.Errorf("user %s not seeded", seed.PicKey)
}
var customer entity.Customer
err := tx.Where("name = ?", seed.Name).First(&customer).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
customer = entity.Customer{
Name: seed.Name,
PicId: picID,
Type: string(utils.CustomerSupplierTypeBisnis),
Address: seed.Address,
Phone: seed.Phone,
Email: seed.Email,
AccountNumber: *strPtr(fmt.Sprintf("%03d", idx+1)),
CreatedBy: createdBy,
}
if err := tx.Create(&customer).Error; err != nil {
return err
}
} else if err != nil {
return err
}
}
return nil
}
func seedFcr(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
Name string
Standards []struct {
Weight float64
FcrNumber float64
Mortality float64
}
}{
{
Name: "FCR Layer",
Standards: []struct {
Weight float64
FcrNumber float64
Mortality float64
}{
{Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0},
{Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5},
},
},
}
for _, seed := range seeds {
var fcr entity.Fcr
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
if err := tx.Create(&fcr).Error; err != nil {
return err
}
} else if err != nil {
return err
}
for _, std := range seed.Standards {
var standard entity.FcrStandard
err := tx.Where("fcr_id = ? AND weight = ?", fcr.Id, std.Weight).First(&standard).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
standard = entity.FcrStandard{
FcrID: fcr.Id,
Weight: std.Weight,
FcrNumber: std.FcrNumber,
Mortality: std.Mortality,
}
if err := tx.Create(&standard).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
"fcr_number": std.FcrNumber,
"mortality": std.Mortality,
}).Error; err != nil {
return err
}
}
}
}
return nil
}
func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
seeds := []struct {
Name string
Brand string
Sku string
Uom string
Category string
Price float64
Selling *float64
Tax *float64
Expiry *int
Suppliers []string
Flags []utils.FlagType
}{
{
Name: "DOC Broiler",
Brand: "MBU Broiler",
Sku: "BRO0001",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 7500,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagDOC},
},
{
Name: "281 SPECIAL STARTER",
Brand: "281 STARTER",
Sku: "281",
Uom: "Kilogram",
Category: "Bahan Baku",
Price: 7850,
Expiry: intPtr(60),
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
},
}
for _, seed := range seeds {
uomID, ok := uoms[seed.Uom]
if !ok {
return fmt.Errorf("uom %s not seeded", seed.Uom)
}
categoryID, ok := categories[seed.Category]
if !ok {
return fmt.Errorf("product category %s not seeded", seed.Category)
}
var product entity.Product
err := tx.Where("name = ?", seed.Name).First(&product).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
selling := seed.Selling
tax := seed.Tax
product = entity.Product{
Name: seed.Name,
Brand: seed.Brand,
Sku: &seed.Sku,
UomId: uomID,
ProductCategoryId: categoryID,
ProductPrice: seed.Price,
SellingPrice: selling,
Tax: tax,
ExpiryPeriod: seed.Expiry,
CreatedBy: createdBy,
}
if err := tx.Create(&product).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
updates := map[string]any{
"brand": seed.Brand,
"uom_id": uomID,
"product_category_id": categoryID,
"product_price": seed.Price,
"selling_price": seed.Selling,
"tax": seed.Tax,
"expiry_period": seed.Expiry,
}
if seed.Sku != "" {
updates["sku"] = seed.Sku
}
if err := tx.Model(&entity.Product{}).Where("id = ?", product.Id).Updates(updates).Error; err != nil {
return err
}
}
for _, supplierName := range seed.Suppliers {
supplierID, ok := suppliers[supplierName]
if !ok {
return fmt.Errorf("supplier %s not seeded", supplierName)
}
var existing entity.ProductSupplier
err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.ProductSupplier{ProductID: product.Id, SupplierID: supplierID}
if err := tx.Create(&link).Error; err != nil {
return err
}
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
}
if err := seedFlags(tx, product.Id, entity.FlagableTypeProduct, seed.Flags); err != nil {
return err
}
}
return nil
}
func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error {
seeds := []struct {
Name string
Uom string
Suppliers []string
Flags []utils.FlagType
}{
{
Name: "Expedisi DOC",
Uom: "Ekor",
Suppliers: []string{"Ekspedisi"},
Flags: []utils.FlagType{utils.FlagEkspedisi},
},
{
Name: "Solar",
Uom: "Liter",
Suppliers: []string{"BOP Vendor"},
Flags: []utils.FlagType{},
},
}
for _, seed := range seeds {
uomID, ok := uoms[seed.Uom]
if !ok {
return fmt.Errorf("uom %s not seeded", seed.Uom)
}
var nonstock entity.Nonstock
err := tx.Where("name = ?", seed.Name).First(&nonstock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
nonstock = entity.Nonstock{
Name: seed.Name,
UomId: uomID,
CreatedBy: createdBy,
}
if err := tx.Create(&nonstock).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{
"uom_id": uomID,
}).Error; err != nil {
return err
}
}
for _, supplierName := range seed.Suppliers {
supplierID, ok := suppliers[supplierName]
if !ok {
return fmt.Errorf("supplier %s not seeded", supplierName)
}
var existing entity.NonstockSupplier
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.NonstockSupplier{NonstockID: nonstock.Id, SupplierID: supplierID}
if err := tx.Create(&link).Error; err != nil {
return err
}
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
}
if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil {
return err
}
}
return nil
}
func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error {
if len(flags) == 0 {
return nil
}
for _, flag := range flags {
name := strings.ToUpper(string(flag))
var existing entity.Flag
err := tx.Where("name = ? AND flagable_id = ? AND flagable_type = ?", name, flagableID, flagableType).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
record := entity.Flag{
Name: name,
FlagableID: flagableID,
FlagableType: flagableType,
}
if err := tx.Create(&record).Error; err != nil {
return err
}
} else if err != nil {
return err
}
}
return nil
}
func seedBanks(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
Name string
Alias string
Owner *string
AccountNumber string
}{
{
Name: "Bank Central Asia",
Alias: "BCA",
AccountNumber: "1234567890",
Owner: ptr("PT MBU Group"),
},
{
Name: "Bank Rakyat Indonesia",
Alias: "BRI",
AccountNumber: "9876543210",
Owner: ptr("PT MBU Group"),
},
{
Name: "Bank Mandiri",
Alias: "MAND",
AccountNumber: "1122334455",
Owner: ptr("PT MBU Group"),
},
}
for _, seed := range seeds {
var bank entity.Bank
err := tx.Where("name = ?", seed.Name).First(&bank).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
bank = entity.Bank{
Name: seed.Name,
Alias: seed.Alias,
Owner: seed.Owner,
AccountNumber: seed.AccountNumber,
CreatedBy: createdBy,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := tx.Create(&bank).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
// update data jika sudah ada
if err := tx.Model(&entity.Bank{}).Where("id = ?", bank.Id).Updates(map[string]any{
"alias": seed.Alias,
"owner": seed.Owner,
"account_number": seed.AccountNumber,
"updated_at": time.Now(),
}).Error; err != nil {
return err
}
}
}
return nil
}
func ptr[T any](v T) *T {
return &v
}
func strPtr(s string) *string {
return &s
}
func intPtr(v int) *int {
return &v
}
func uintPtr(v uint) *uint {
return &v
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Area struct { type Area struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"` Name string `gorm:"not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+21
View File
@@ -0,0 +1,21 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Bank struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
Alias string `gorm:"not null;size:5"`
Owner *string `gorm:""`
AccountNumber string `gorm:"not null;size:50"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Constant struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+4 -4
View File
@@ -8,13 +8,13 @@ import (
type Customer struct { type Customer struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"` Name string `gorm:"not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
PicId uint `gorm:"not null"` PicId uint `gorm:"not null"`
Type string `gorm:"not null"` Type string `gorm:"not null;size:50"`
Address string `gorm:"not null"` Address string `gorm:"not null"`
Phone string `gorm:"not null"` Phone string `gorm:"not null;size:20"`
Email string `gorm:"not null"` Email string `gorm:"not null"`
AccountNumber string `gorm:"not null"` AccountNumber string `gorm:"not null;size:50"`
Balance float64 `gorm:"default:0"` Balance float64 `gorm:"default:0"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Fcr struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Standards []FcrStandard `gorm:"foreignKey:FcrID;references:Id"`
}
+20
View File
@@ -0,0 +1,20 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type FcrStandard struct {
Id uint `gorm:"primaryKey"`
FcrID uint `gorm:"not null;index"`
Weight float64 `gorm:"type:numeric(15,3);not null"`
FcrNumber float64 `gorm:"type:numeric(15,3);not null"`
Mortality float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Fcr Fcr `gorm:"foreignKey:FcrID;references:Id"`
}
+17
View File
@@ -0,0 +1,17 @@
package entities
import "time"
const (
FlagableTypeProduct = "products"
FlagableTypeNonstock = "nonstocks"
)
type Flag struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:flags_unique_flagable"`
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"`
FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Kandang struct { type Kandang struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"` Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"` PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Location struct { type Location struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"` Name string `gorm:"not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
Address string `gorm:"not null"` Address string `gorm:"not null"`
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
+22
View File
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Nonstock struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
UomId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
Suppliers []Supplier `gorm:"many2many:nonstock_suppliers;joinForeignKey:NonstockID;joinReferences:SupplierID"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"`
}
+9
View File
@@ -0,0 +1,9 @@
package entities
import "time"
type NonstockSupplier struct {
NonstockID uint `gorm:"primaryKey"`
SupplierID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProductCategory struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Product struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
Brand string `gorm:"not null"`
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
UomId uint `gorm:"not null"`
ProductCategoryId uint `gorm:"not null"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Tax *float64 `gorm:"type:numeric(15,3)"`
ExpiryPeriod *int `gorm:""`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
Suppliers []Supplier `gorm:"many2many:product_suppliers;joinForeignKey:ProductID;joinReferences:SupplierID"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
}
+9
View File
@@ -0,0 +1,9 @@
package entities
import "time"
type ProductSupplier struct {
ProductID uint `gorm:"primaryKey"`
SupplierID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Supplier struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
Alias string `gorm:"not null;size:5"`
Pic string `gorm:"not null"`
Type string `gorm:"not null;size:50"`
Category string `gorm:"not null;size:20"`
Hatchery *string `gorm:"size:255"`
Phone string `gorm:"not null;size:20"`
Email string `gorm:"not null"`
Address string `gorm:"not null"`
Npwp *string `gorm:"size:50"`
AccountNumber *string `gorm:"size:50"`
Balance float64 `gorm:"type:numeric(15,3);default:0"`
DueDate int `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Uom struct { type Uom struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"` Name string `gorm:"not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
@@ -0,0 +1,25 @@
package controller
import (
service "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/services"
"github.com/gofiber/fiber/v2"
)
type ConstantController struct {
ConstantService service.ConstantService
}
func NewConstantController(constantService service.ConstantService) *ConstantController {
return &ConstantController{
ConstantService: constantService,
}
}
func (ctrl *ConstantController) GetAll(c *fiber.Ctx) error {
data, err := ctrl.ConstantService.GetAll(c)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.Status(fiber.StatusOK).JSON(data)
}
+20
View File
@@ -0,0 +1,20 @@
package constants
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rConstant "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/repositories"
sConstant "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/services"
)
type ConstantModule struct{}
func (ConstantModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
constantRepo := rConstant.NewConstantRepository(db)
constantService := sConstant.NewConstantService(constantRepo, validate)
ConstantRoutes(router, constantService)
}
@@ -0,0 +1,46 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
type ConstantRepository interface {
GetConstants() map[string]interface{}
}
type ConstantRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Constant]
}
func NewConstantRepository(db *gorm.DB) ConstantRepository {
return &ConstantRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Constant](db),
}
}
func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
flagList := make([]string, 0)
for f := range utils.AllFlagTypes() {
flagList = append(flagList, string(f))
}
return map[string]interface{}{
"flags": flagList,
"warehouse_types": []string{
"AREA",
"LOKASI",
"KANDANG",
},
"supplier_categories": []string{
"BOP",
"SAPRONAK",
},
"customer_supplier_types": []string{
"BISNIS",
"INDIVIDUAL",
},
}
}
+17
View File
@@ -0,0 +1,17 @@
package constants
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/controllers"
constant "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/services"
"github.com/gofiber/fiber/v2"
)
func ConstantRoutes(v1 fiber.Router, s constant.ConstantService) {
ctrl := controller.NewConstantController(s)
route := v1.Group("/constants")
route.Get("/", ctrl.GetAll)
}
@@ -0,0 +1,26 @@
package service
import (
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/repositories"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type ConstantService interface {
GetAll(ctx *fiber.Ctx) (map[string]interface{}, error)
}
type constantService struct {
Repository repository.ConstantRepository
}
func NewConstantService(repo repository.ConstantRepository, validate *validator.Validate) ConstantService {
return &constantService{
Repository: repo,
}
}
func (s constantService) GetAll(c *fiber.Ctx) (map[string]interface{}, error) {
return s.Repository.GetConstants(), nil
}
@@ -56,3 +56,9 @@ func ToAreaListDTOs(e []entity.Area) []AreaListDTO {
} }
return result return result
} }
func ToAreaDetailDTO(e entity.Area) AreaDetailDTO {
return AreaDetailDTO{
AreaListDTO: ToAreaListDTO(e),
}
}
@@ -98,7 +98,7 @@ func (s *areaService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.A
return nil, err return nil, err
} }
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations) return s.GetOne(c, createBody.Id)
} }
func (s areaService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Area, error) { func (s areaService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Area, error) {
@@ -5,7 +5,7 @@ type Create struct {
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
} }
type Query struct { type Query struct {
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type BankController struct {
BankService service.BankService
}
func NewBankController(bankService service.BankService) *BankController {
return &BankController{
BankService: bankService,
}
}
func (u *BankController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.BankService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.BankListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all banks successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToBankListDTOs(result),
})
}
func (u *BankController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.BankService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get bank successfully",
Data: dto.ToBankListDTO(*result),
})
}
func (u *BankController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.BankService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create bank successfully",
Data: dto.ToBankListDTO(*result),
})
}
func (u *BankController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.BankService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update bank successfully",
Data: dto.ToBankListDTO(*result),
})
}
func (u *BankController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.BankService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete bank successfully",
})
}
@@ -0,0 +1,70 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type BankBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Owner *string `json:"owner"`
AccountNumber string `json:"account_number"`
}
type BankListDTO struct {
BankBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type BankDetailDTO struct {
BankListDTO
}
// === Mapper Functions ===
func ToBankBaseDTO(e entity.Bank) BankBaseDTO {
return BankBaseDTO{
Id: e.Id,
Name: e.Name,
Alias: e.Alias,
Owner: e.Owner,
AccountNumber: e.AccountNumber,
}
}
func ToBankListDTO(e entity.Bank) BankListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return BankListDTO{
BankBaseDTO: ToBankBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToBankListDTOs(e []entity.Bank) []BankListDTO {
result := make([]BankListDTO, len(e))
for i, r := range e {
result[i] = ToBankListDTO(r)
}
return result
}
func ToBankDetailDTO(e entity.Bank) BankDetailDTO {
return BankDetailDTO{
BankListDTO: ToBankListDTO(e),
}
}
+26
View File
@@ -0,0 +1,26 @@
package banks
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rBank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/repositories"
sBank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type BankModule struct{}
func (BankModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
bankRepo := rBank.NewBankRepository(db)
userRepo := rUser.NewUserRepository(db)
bankService := sBank.NewBankService(bankRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
BankRoutes(router, userService, bankService)
}
@@ -0,0 +1,30 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type BankRepository interface {
repository.BaseRepository[entity.Bank]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type BankRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Bank]
db *gorm.DB
}
func NewBankRepository(db *gorm.DB) BankRepository {
return &BankRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Bank](db),
db: db,
}
}
func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID)
}
+28
View File
@@ -0,0 +1,28 @@
package banks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/controllers"
bank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func BankRoutes(v1 fiber.Router, u user.UserService, s bank.BankService) {
ctrl := controller.NewBankController(s)
route := v1.Group("/banks")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,156 @@
package service
import (
"errors"
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type BankService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Bank, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Bank, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Bank, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Bank, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type bankService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.BankRepository
}
func NewBankService(repo repository.BankRepository, validate *validator.Validate) BankService {
return &bankService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s bankService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s bankService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Bank, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
banks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get banks: %+v", err)
return nil, 0, err
}
return banks, total, nil
}
func (s bankService) GetOne(c *fiber.Ctx, id uint) (*entity.Bank, error) {
bank, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Bank not found")
}
if err != nil {
s.Log.Errorf("Failed get bank by id: %+v", err)
return nil, err
}
return bank, nil
}
func (s *bankService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Bank, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check bank name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name))
}
createBody := &entity.Bank{
Name: req.Name,
Alias: req.Alias,
Owner: req.Owner,
AccountNumber: req.AccountNumber,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create bank: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s bankService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Bank, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check bank name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.Alias != nil {
updateBody["alias"] = *req.Alias
}
if req.Owner != nil {
updateBody["owner"] = *req.Owner
}
if req.AccountNumber != nil {
updateBody["account_number"] = *req.AccountNumber
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Bank not found")
}
s.Log.Errorf("Failed to update bank: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s bankService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Bank not found")
}
s.Log.Errorf("Failed to delete bank: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Alias string `json:"alias" validate:"required_strict"`
Owner *string `json:"owner,omitempty" validate:"omitempty"`
AccountNumber string `json:"account_number" validate:"required_strict,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Alias *string `json:"alias,omitempty" validate:"omitempty"`
Owner *string `json:"owner,omitempty" validate:"omitempty"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -18,6 +18,7 @@ type CustomerBaseDTO struct {
Phone string `json:"phone"` Phone string `json:"phone"`
Email string `json:"email"` Email string `json:"email"`
AccountNumber string `json:"account_number"` AccountNumber string `json:"account_number"`
Balance float64 `json:"balance"`
Pic *userDTO.UserBaseDTO `json:"pic"` Pic *userDTO.UserBaseDTO `json:"pic"`
} }
@@ -77,3 +78,9 @@ func ToCustomerListDTOs(e []entity.Customer) []CustomerListDTO {
} }
return result return result
} }
func ToCustomerDetailDTO(e entity.Customer) CustomerDetailDTO {
return CustomerDetailDTO{
CustomerListDTO: ToCustomerListDTO(e),
}
}
@@ -117,7 +117,7 @@ func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, err return nil, err
} }
return createBody, nil return s.GetOne(c, createBody.Id)
} }
func (s customerService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Customer, error) { func (s customerService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Customer, error) {
@@ -137,17 +137,18 @@ func (s customerService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
updateBody["name"] = *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 { if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists}); err != nil {
return nil, err return nil, err
} }
if req.PicId != nil {
updateBody["pic_id"] = *req.PicId updateBody["pic_id"] = *req.PicId
} }
if req.Type != nil { if req.Type != nil {
typ := strings.ToUpper(*req.Type) typ := strings.ToUpper(*req.Type)
if !utils.IsValidCustomerSupplierType(typ) { if !utils.IsValidCustomerSupplierType(typ) {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid customer type") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid customer type")
} }
updateBody["type"] = typ updateBody["type"] = typ
} }
@@ -11,7 +11,7 @@ type Create struct {
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"` PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
Type *string `json:"type,omitempty" validate:"omitempty"` Type *string `json:"type,omitempty" validate:"omitempty"`
Address *string `json:"address,omitempty" validate:"omitempty"` Address *string `json:"address,omitempty" validate:"omitempty"`
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type FcrController struct {
FcrService service.FcrService
}
func NewFcrController(fcrService service.FcrService) *FcrController {
return &FcrController{
FcrService: fcrService,
}
}
func (u *FcrController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.FcrService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.FcrListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all fcrs successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToFcrListDTOs(result),
})
}
func (u *FcrController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.FcrService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get fcr successfully",
Data: dto.ToFcrDetailDTO(*result),
})
}
func (u *FcrController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.FcrService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create fcr successfully",
Data: dto.ToFcrDetailDTO(*result),
})
}
func (u *FcrController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.FcrService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update fcr successfully",
Data: dto.ToFcrDetailDTO(*result),
})
}
func (u *FcrController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.FcrService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete fcr successfully",
})
}
@@ -0,0 +1,86 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type FcrBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type FcrStandardDTO struct {
Id uint `json:"id"`
Weight float64 `json:"weight"`
FcrNumber float64 `json:"fcr_number"`
Mortality float64 `json:"mortality"`
}
type FcrListDTO struct {
FcrBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type FcrDetailDTO struct {
FcrListDTO
Standards []FcrStandardDTO `json:"fcr_standards"`
}
// === Mapper Functions ===
func ToFcrBaseDTO(e entity.Fcr) FcrBaseDTO {
return FcrBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToFcrListDTO(e entity.Fcr) FcrListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return FcrListDTO{
FcrBaseDTO: ToFcrBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToFcrListDTOs(e []entity.Fcr) []FcrListDTO {
result := make([]FcrListDTO, len(e))
for i, r := range e {
result[i] = ToFcrListDTO(r)
}
return result
}
func ToFcrDetailDTO(e entity.Fcr) FcrDetailDTO {
return FcrDetailDTO{
FcrListDTO: ToFcrListDTO(e),
Standards: ToFcrStandardDTOs(e.Standards),
}
}
func ToFcrStandardDTOs(standards []entity.FcrStandard) []FcrStandardDTO {
result := make([]FcrStandardDTO, len(standards))
for i, s := range standards {
result[i] = FcrStandardDTO{
Id: s.Id,
Weight: s.Weight,
FcrNumber: s.FcrNumber,
Mortality: s.Mortality,
}
}
return result
}
+25
View File
@@ -0,0 +1,25 @@
package fcrs
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rFcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/repositories"
sFcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type FcrModule struct{}
func (FcrModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
fcrRepo := rFcr.NewFcrRepository(db)
userRepo := rUser.NewUserRepository(db)
fcrService := sFcr.NewFcrService(fcrRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
FcrRoutes(router, userService, fcrService)
}
@@ -0,0 +1,90 @@
package repository
import (
"context"
"errors"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type FcrRepository interface {
repository.BaseRepository[entity.Fcr]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SyncStandardsDiff(ctx context.Context, tx *gorm.DB, fcrID uint, standards []entity.FcrStandard) error
}
type FcrRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Fcr]
}
func NewFcrRepository(db *gorm.DB) FcrRepository {
return &FcrRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Fcr](db),
}
}
func (r *FcrRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Fcr](ctx, r.DB(), name, excludeID)
}
func (r *FcrRepositoryImpl) SyncStandardsDiff(ctx context.Context, tx *gorm.DB, fcrID uint, standards []entity.FcrStandard) error {
db := tx
if db == nil {
db = r.DB()
}
var existing []entity.FcrStandard
if err := db.WithContext(ctx).
Where("fcr_id = ?", fcrID).
Find(&existing).
Error; err != nil {
return err
}
existingMap := make(map[float64]entity.FcrStandard)
for _, st := range existing {
existingMap[st.Weight] = st
}
newMap := make(map[float64]entity.FcrStandard)
for _, st := range standards {
st.FcrID = fcrID
newMap[st.Weight] = st
}
baseRepo := repository.NewBaseRepository[entity.FcrStandard](db)
for weight, newStd := range newMap {
if current, ok := existingMap[weight]; ok {
if current.FcrNumber != newStd.FcrNumber || current.Mortality != newStd.Mortality {
update := map[string]any{
"fcr_number": newStd.FcrNumber,
"mortality": newStd.Mortality,
}
if err := baseRepo.PatchOne(ctx, current.Id, update, nil); err != nil {
return err
}
}
} else {
entry := newStd
if err := baseRepo.CreateOne(ctx, &entry, nil); err != nil {
return err
}
}
}
for weight, current := range existingMap {
if _, keep := newMap[weight]; !keep {
if err := baseRepo.DeleteOne(ctx, current.Id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
}
}
return nil
}
+28
View File
@@ -0,0 +1,28 @@
package fcrs
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/controllers"
fcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func FcrRoutes(v1 fiber.Router, u user.UserService, s fcr.FcrService) {
ctrl := controller.NewFcrController(s)
route := v1.Group("/fcrs")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,219 @@
package service
import (
"errors"
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type FcrService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Fcr, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Fcr, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Fcr, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Fcr, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type fcrService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.FcrRepository
}
func NewFcrService(repo repository.FcrRepository, validate *validator.Validate) FcrService {
return &fcrService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s fcrService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Standards", func(db *gorm.DB) *gorm.DB {
return db.Order("weight ASC")
})
}
func (s fcrService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Fcr, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
fcrs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get fcrs: %+v", err)
return nil, 0, err
}
return fcrs, total, nil
}
func (s fcrService) GetOne(c *fiber.Ctx, id uint) (*entity.Fcr, error) {
fcr, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Fcr not found")
}
if err != nil {
s.Log.Errorf("Failed get fcr by id: %+v", err)
return nil, err
}
return fcr, nil
}
func (s *fcrService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Fcr, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check fcr name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check fcr name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Fcr with name %s already exists", req.Name))
}
createBody := &entity.Fcr{
Name: req.Name,
CreatedBy: 1,
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
if len(req.FcrStandards) == 0 {
return nil
}
standards := make([]entity.FcrStandard, len(req.FcrStandards))
for i, std := range req.FcrStandards {
standards[i] = entity.FcrStandard{
FcrID: createBody.Id,
Weight: std.Weight,
FcrNumber: std.FcrNumber,
Mortality: std.Mortality,
}
}
if err := s.Repository.SyncStandardsDiff(c.Context(), tx, createBody.Id, standards); err != nil {
return err
}
return nil
})
if err != nil {
s.Log.Errorf("Failed to create fcr: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s fcrService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Fcr, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check fcr name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check fcr name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Fcr with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if len(updateBody) == 0 && req.FcrStandards == nil {
return s.GetOne(c, id)
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if len(updateBody) > 0 {
if err := repoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return err
}
} else {
if _, err := repoTx.GetByID(c.Context(), id, nil); err != nil {
return err
}
}
if req.FcrStandards != nil {
standards := make([]entity.FcrStandard, len(req.FcrStandards))
for i, std := range req.FcrStandards {
standards[i] = entity.FcrStandard{
FcrID: id,
Weight: std.Weight,
FcrNumber: std.FcrNumber,
Mortality: std.Mortality,
}
}
if err := s.Repository.SyncStandardsDiff(c.Context(), tx, id, standards); err != nil {
return err
}
}
return nil
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Fcr not found")
}
s.Log.Errorf("Failed to update fcr: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s fcrService) DeleteOne(c *fiber.Ctx, id uint) error {
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := s.Repository.SyncStandardsDiff(c.Context(), tx, id, nil); err != nil {
return err
}
return repoTx.DeleteOne(c.Context(), id)
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Fcr not found")
}
s.Log.Errorf("Failed to delete fcr: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,23 @@
package validation
type FcrStandard struct {
Weight float64 `json:"weight" validate:"required,gte=0"`
FcrNumber float64 `json:"fcr_number" validate:"required,gte=0"`
Mortality float64 `json:"mortality" validate:"required,gte=0"`
}
type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"`
FcrStandards []FcrStandard `json:"fcr_standards" validate:"required,min=1,dive"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty_strict,min=3,max=50"`
FcrStandards []FcrStandard `json:"fcr_standards,omitempty" validate:"omitempty,min=1,dive"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -73,3 +73,9 @@ func ToKandangListDTOs(e []entity.Kandang) []KandangListDTO {
} }
return result return result
} }
func ToKandangDetailDTO(e entity.Kandang) KandangDetailDTO {
return KandangDetailDTO{
KandangListDTO: ToKandangListDTO(e),
}
}
@@ -108,7 +108,7 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations) return s.GetOne(c, createBody.Id)
} }
func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Kandang, error) { func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Kandang, error) {
@@ -128,17 +128,18 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["name"] = *req.Name updateBody["name"] = *req.Name
} }
if req.LocationId != nil { if err := common.EnsureRelations(c.Context(),
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists}); err != nil { 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 return nil, err
} }
if req.LocationId != nil {
updateBody["location_id"] = *req.LocationId updateBody["location_id"] = *req.LocationId
} }
if req.PicId != nil { 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 updateBody["pic_id"] = *req.PicId
} }
@@ -7,7 +7,7 @@ type Create struct {
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"` PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
} }
@@ -67,3 +67,9 @@ func ToLocationListDTOs(e []entity.Location) []LocationListDTO {
} }
return result return result
} }
func ToLocationDetailDTO(e entity.Location) LocationDetailDTO {
return LocationDetailDTO{
LocationListDTO: ToLocationListDTO(e),
}
}
@@ -107,13 +107,7 @@ func (s *locationService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, err return nil, err
} }
created, err := s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations) return s.GetOne(c, createBody.Id)
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) { func (s locationService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Location, error) {
@@ -137,10 +131,11 @@ func (s locationService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
updateBody["address"] = *req.Address 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 { if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Area", ID: req.AreaId, Exists: s.Repository.AreaExists}); err != nil {
return nil, err return nil, err
} }
if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId updateBody["area_id"] = *req.AreaId
} }
@@ -7,7 +7,7 @@ type Create struct {
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
Address *string `json:"address,omitempty" validate:"omitempty"` Address *string `json:"address,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
} }
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type NonstockController struct {
NonstockService service.NonstockService
}
func NewNonstockController(nonstockService service.NonstockService) *NonstockController {
return &NonstockController{
NonstockService: nonstockService,
}
}
func (u *NonstockController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.NonstockService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.NonstockListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all nonstocks successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToNonstockListDTOs(result),
})
}
func (u *NonstockController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.NonstockService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get nonstock successfully",
Data: dto.ToNonstockDetailDTO(*result),
})
}
func (u *NonstockController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.NonstockService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create nonstock successfully",
Data: dto.ToNonstockDetailDTO(*result),
})
}
func (u *NonstockController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.NonstockService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update nonstock successfully",
Data: dto.ToNonstockDetailDTO(*result),
})
}
func (u *NonstockController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.NonstockService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete nonstock successfully",
})
}
@@ -0,0 +1,97 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type NonstockBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
UomID uint `json:"uom_id"`
}
type NonstockListDTO struct {
NonstockBaseDTO
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
Flags []string `json:"flags"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type NonstockDetailDTO struct {
NonstockListDTO
Flags []string `json:"flags"`
}
// === Mapper Functions ===
func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO {
return NonstockBaseDTO{
Id: e.Id,
Name: e.Name,
UomID: e.UomId,
}
}
func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
suppliers := make([]supplierDTO.SupplierBaseDTO, len(e.Suppliers))
for i, s := range e.Suppliers {
suppliers[i] = supplierDTO.ToSupplierBaseDTO(s)
}
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return NonstockListDTO{
NonstockBaseDTO: ToNonstockBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Uom: uomRef,
Suppliers: suppliers,
Flags: flags,
}
}
func ToNonstockListDTOs(e []entity.Nonstock) []NonstockListDTO {
result := make([]NonstockListDTO, len(e))
for i, r := range e {
result[i] = ToNonstockListDTO(r)
}
return result
}
func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return NonstockDetailDTO{
NonstockListDTO: ToNonstockListDTO(e),
Flags: flags,
}
}
@@ -0,0 +1,26 @@
package nonstocks
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
sNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type NonstockModule struct{}
func (NonstockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
nonstockRepo := rNonstock.NewNonstockRepository(db)
userRepo := rUser.NewUserRepository(db)
nonstockService := sNonstock.NewNonstockService(nonstockRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
NonstockRoutes(router, userService, nonstockService)
}
@@ -0,0 +1,172 @@
package repository
import (
"context"
"errors"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type NonstockRepository interface {
repository.BaseRepository[entity.Nonstock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error
UomExists(ctx context.Context, uomID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error
DeleteFlags(ctx context.Context, tx *gorm.DB, nonstockID uint) error
GetFlags(ctx context.Context, nonstockID uint) ([]entity.Flag, error)
}
type NonstockRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Nonstock]
}
func NewNonstockRepository(db *gorm.DB) NonstockRepository {
return &NonstockRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Nonstock](db),
}
}
func (r *NonstockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Nonstock](ctx, r.DB(), name, excludeID)
}
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error {
db := tx
if db == nil {
db = r.DB()
}
if supplierIDs == nil {
return db.WithContext(ctx).
Where("nonstock_id = ?", nonstockID).
Delete(&entity.NonstockSupplier{}).
Error
}
var existing []entity.NonstockSupplier
if err := db.WithContext(ctx).
Where("nonstock_id = ?", nonstockID).
Find(&existing).
Error; err != nil {
return err
}
existingMap := make(map[uint]struct{}, len(existing))
for _, rel := range existing {
existingMap[rel.SupplierID] = struct{}{}
}
incomingMap := make(map[uint]struct{}, len(supplierIDs))
for _, id := range supplierIDs {
incomingMap[id] = struct{}{}
if _, exists := existingMap[id]; exists {
continue
}
record := entity.NonstockSupplier{NonstockID: nonstockID, SupplierID: id}
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err
}
}
for _, rel := range existing {
if _, keep := incomingMap[rel.SupplierID]; !keep {
if err := db.WithContext(ctx).
Where("nonstock_id = ? AND supplier_id = ?", nonstockID, rel.SupplierID).
Delete(&entity.NonstockSupplier{}).
Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
}
}
return nil
}
func (r *NonstockRepositoryImpl) UomExists(ctx context.Context, uomID uint) (bool, error) {
var count int64
if err := r.DB().WithContext(ctx).
Model(&entity.Uom{}).
Where("id = ?", uomID).
Count(&count).
Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *NonstockRepositoryImpl) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) {
if len(supplierIDs) == 0 {
return nil, nil
}
var suppliers []entity.Supplier
if err := r.DB().WithContext(ctx).
Select("id", "category").
Where("id IN ?", supplierIDs).
Find(&suppliers).
Error; err != nil {
return nil, err
}
return suppliers, nil
}
func (r *NonstockRepositoryImpl) SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error {
db := tx
if db == nil {
db = r.DB()
}
// Hapus flags lama terlebih dahulu
if err := db.WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", nonstockID, entity.FlagableTypeNonstock).
Delete(&entity.Flag{}).
Error; err != nil {
return err
}
// Insert flags baru jika ada
if len(flags) > 0 {
newFlags := make([]entity.Flag, len(flags))
for i, f := range flags {
newFlags[i] = entity.Flag{
Name: f,
FlagableID: nonstockID,
FlagableType: entity.FlagableTypeNonstock,
}
}
if err := db.WithContext(ctx).Create(&newFlags).Error; err != nil {
return err
}
}
return nil
}
func (r *NonstockRepositoryImpl) DeleteFlags(ctx context.Context, tx *gorm.DB, nonstockID uint) error {
db := tx
if db == nil {
db = r.DB()
}
return db.WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", nonstockID, entity.FlagableTypeNonstock).
Delete(&entity.Flag{}).
Error
}
func (r *NonstockRepositoryImpl) GetFlags(ctx context.Context, nonstockID uint) ([]entity.Flag, error) {
var flags []entity.Flag
if err := r.DB().WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", nonstockID, entity.FlagableTypeNonstock).
Find(&flags).
Error; err != nil {
return nil, err
}
return flags, nil
}
@@ -0,0 +1,28 @@
package nonstocks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/controllers"
nonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func NonstockRoutes(v1 fiber.Router, u user.UserService, s nonstock.NonstockService) {
ctrl := controller.NewNonstockController(s)
route := v1.Group("/nonstocks")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,308 @@
package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type NonstockService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Nonstock, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Nonstock, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Nonstock, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Nonstock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type nonstockService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.NonstockRepository
}
func NewNonstockService(repo repository.NonstockRepository, validate *validator.Validate) NonstockService {
return &nonstockService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s nonstockService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Uom").
Preload("Flags").
Preload("Suppliers", func(db *gorm.DB) *gorm.DB {
return db.Order("suppliers.name ASC")
})
}
func (s nonstockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Nonstock, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
nonstocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get nonstocks: %+v", err)
return nil, 0, err
}
return nonstocks, total, nil
}
func (s nonstockService) GetOne(c *fiber.Ctx, id uint) (*entity.Nonstock, error) {
nonstock, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Nonstock not found")
}
if err != nil {
s.Log.Errorf("Failed get nonstock by id: %+v", err)
return nil, err
}
return nonstock, nil
}
func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Nonstock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
ctx := c.Context()
name := strings.TrimSpace(req.Name)
if exists, err := s.Repository.NameExists(ctx, name, nil); err != nil {
s.Log.Errorf("Failed to check nonstock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check nonstock name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Nonstock with name %s already exists", name))
}
if err := common.EnsureRelations(ctx, common.RelationCheck{Name: "Uom", ID: &req.UomID, Exists: s.Repository.UomExists}); err != nil {
return nil, err
}
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
if len(supplierIDs) > 0 {
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
}
if len(supplierList) != len(supplierIDs) {
actualIDs := make([]uint, len(supplierList))
for i, supplier := range supplierList {
actualIDs[i] = supplier.Id
}
missing := utils.MissingUintIDs(supplierIDs, actualIDs)
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
}
for _, sup := range supplierList {
if strings.ToUpper(sup.Category) != string(utils.SupplierCategoryBOP) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category BOP", sup.Id))
}
}
}
nonstockFlags, flagErr := normalizeNonstockFlags(req.Flags)
if flagErr != nil {
return nil, flagErr
}
createBody := &entity.Nonstock{
Name: req.Name,
UomId: req.UomID,
CreatedBy: 1,
}
err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(ctx, createBody, nil); err != nil {
return err
}
if err := s.Repository.SyncFlags(ctx, tx, createBody.Id, nonstockFlags); err != nil {
return err
}
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
})
if err != nil {
s.Log.Errorf("Failed to create nonstock: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Nonstock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
ctx := c.Context()
if err := common.EnsureRelations(ctx, common.RelationCheck{Name: "Uom", ID: req.UomID, Exists: s.Repository.UomExists}); err != nil {
return nil, err
}
if req.Name != nil {
if exists, err := s.Repository.NameExists(ctx, *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check nonstock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check nonstock name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Nonstock with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.UomID != nil {
updateBody["uom_id"] = *req.UomID
}
var supplierIDs []uint
var supplierUpdate bool
if req.SupplierIDs != nil {
supplierUpdate = true
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
if len(supplierIDs) > 0 {
var supplierList []entity.Supplier
var supplierErr error
supplierList, supplierErr = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
}
if len(supplierList) != len(supplierIDs) {
actualIDs := make([]uint, len(supplierList))
for i, supplier := range supplierList {
actualIDs[i] = supplier.Id
}
missing := utils.MissingUintIDs(supplierIDs, actualIDs)
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
}
for _, sup := range supplierList {
if strings.ToUpper(sup.Category) != string(utils.SupplierCategoryBOP) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category BOP", sup.Id))
}
}
}
}
var (
flagUpdate bool
flagValues []string
)
if req.Flags != nil {
flagUpdate = true
var flagErr error
flagValues, flagErr = normalizeNonstockFlags(*req.Flags)
if flagErr != nil {
return nil, flagErr
}
}
if len(updateBody) == 0 && !supplierUpdate && !flagUpdate {
return s.GetOne(c, id)
}
err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if len(updateBody) > 0 {
if err := repoTx.PatchOne(ctx, id, updateBody, nil); err != nil {
return err
}
} else {
if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
return err
}
}
if supplierUpdate {
var ids []uint
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
return err
}
}
if flagUpdate {
if err := s.Repository.SyncFlags(ctx, tx, id, flagValues); err != nil {
return err
}
}
return nil
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Nonstock not found")
}
s.Log.Errorf("Failed to update nonstock: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s nonstockService) DeleteOne(c *fiber.Ctx, id uint) error {
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := s.Repository.SyncSuppliersDiff(c.Context(), tx, id, nil); err != nil {
return err
}
if err := s.Repository.DeleteFlags(c.Context(), tx, id); err != nil {
return err
}
return repoTx.DeleteOne(c.Context(), id)
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Nonstock not found")
}
s.Log.Errorf("Failed to delete nonstock: %+v", err)
return err
}
return nil
}
func normalizeNonstockFlags(raw []string) ([]string, error) {
normalized, invalid := utils.NormalizeFlagsForGroup(raw, utils.FlagGroupNonstock)
if len(invalid) > 0 {
invalidStr := strings.Join(utils.FlagTypesToStrings(invalid), ", ")
allowedStr := strings.Join(utils.FlagTypesToStrings(utils.AllowedFlagTypes(utils.FlagGroupNonstock)), ", ")
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid nonstock flags: %s. Allowed flags: %s", invalidStr, allowedStr))
}
return utils.FlagTypesToStrings(normalized), nil
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ProductCategoryController struct {
ProductCategoryService service.ProductCategoryService
}
func NewProductCategoryController(productCategoryService service.ProductCategoryService) *ProductCategoryController {
return &ProductCategoryController{
ProductCategoryService: productCategoryService,
}
}
func (u *ProductCategoryController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.ProductCategoryService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductCategoryListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all product categories successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProductCategoryListDTOs(result),
})
}
func (u *ProductCategoryController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.ProductCategoryService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get product category successfully",
Data: dto.ToProductCategoryDetailDTO(*result),
})
}
func (u *ProductCategoryController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProductCategoryService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create product category successfully",
Data: dto.ToProductCategoryDetailDTO(*result),
})
}
func (u *ProductCategoryController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProductCategoryService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update product category successfully",
Data: dto.ToProductCategoryDetailDTO(*result),
})
}
func (u *ProductCategoryController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ProductCategoryService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete product category successfully",
})
}
@@ -0,0 +1,66 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type ProductCategoryBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
}
type ProductCategoryListDTO struct {
ProductCategoryBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ProductCategoryDetailDTO struct {
ProductCategoryListDTO
}
// === Mapper Functions ===
func ToProductCategoryBaseDTO(e entity.ProductCategory) ProductCategoryBaseDTO {
return ProductCategoryBaseDTO{
Id: e.Id,
Name: e.Name,
Code: e.Code,
}
}
func ToProductCategoryListDTO(e entity.ProductCategory) ProductCategoryListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return ProductCategoryListDTO{
ProductCategoryBaseDTO: ToProductCategoryBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToProductCategoryListDTOs(e []entity.ProductCategory) []ProductCategoryListDTO {
result := make([]ProductCategoryListDTO, len(e))
for i, r := range e {
result[i] = ToProductCategoryListDTO(r)
}
return result
}
func ToProductCategoryDetailDTO(e entity.ProductCategory) ProductCategoryDetailDTO {
return ProductCategoryDetailDTO{
ProductCategoryListDTO: ToProductCategoryListDTO(e),
}
}
@@ -0,0 +1,25 @@
package productcategories
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rProductCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/repositories"
sProductCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProductCategoryModule struct{}
func (ProductCategoryModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
productCategoryRepo := rProductCategory.NewProductCategoryRepository(db)
userRepo := rUser.NewUserRepository(db)
productCategoryService := sProductCategory.NewProductCategoryService(productCategoryRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProductCategoryRoutes(router, userService, productCategoryService)
}
@@ -0,0 +1,44 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ProductCategoryRepository interface {
repository.BaseRepository[entity.ProductCategory]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
CodeExists(ctx context.Context, code string, excludeID *uint) (bool, error)
}
type ProductCategoryRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProductCategory]
}
func NewProductCategoryRepository(db *gorm.DB) ProductCategoryRepository {
return &ProductCategoryRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductCategory](db),
}
}
func (r *ProductCategoryRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.ProductCategory](ctx, r.DB(), name, excludeID)
}
func (r *ProductCategoryRepositoryImpl) CodeExists(ctx context.Context, code string, excludeID *uint) (bool, error) {
var count int64
q := r.DB().WithContext(ctx).
Model(new(entity.ProductCategory)).
Where("code = ?", code).
Where("deleted_at IS NULL")
if excludeID != nil {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
@@ -0,0 +1,28 @@
package productcategories
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/controllers"
productCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProductCategoryRoutes(v1 fiber.Router, u user.UserService, s productCategory.ProductCategoryService) {
ctrl := controller.NewProductCategoryController(s)
route := v1.Group("/product-categories")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,175 @@
package service
import (
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ProductCategoryService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductCategory, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductCategory, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductCategory, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductCategory, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type productCategoryService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProductCategoryRepository
}
func NewProductCategoryService(repo repository.ProductCategoryRepository, validate *validator.Validate) ProductCategoryService {
return &productCategoryService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s productCategoryService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s productCategoryService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductCategory, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
productCategories, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get product categories: %+v", err)
return nil, 0, err
}
return productCategories, total, nil
}
func (s productCategoryService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductCategory, error) {
productCategory, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product category not found")
}
if err != nil {
s.Log.Errorf("Failed get product category by id: %+v", err)
return nil, err
}
return productCategory, nil
}
func (s *productCategoryService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProductCategory, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
ctx := c.Context()
name := strings.TrimSpace(req.Name)
code := strings.ToUpper(strings.TrimSpace(req.Code))
if exists, err := s.Repository.NameExists(ctx, name, nil); err != nil {
s.Log.Errorf("Failed to check product category name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product category name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product category with name %s already exists", name))
}
if exists, err := s.Repository.CodeExists(ctx, code, nil); err != nil {
s.Log.Errorf("Failed to check product category code: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product category code")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product category with code %s already exists", code))
}
createBody := &entity.ProductCategory{
Name: name,
Code: code,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(ctx, createBody, nil); err != nil {
s.Log.Errorf("Failed to create product category: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s productCategoryService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductCategory, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name cannot be empty")
}
if exists, err := s.Repository.NameExists(c.Context(), name, &id); err != nil {
s.Log.Errorf("Failed to check product category name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product category name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product category with name %s already exists", name))
}
updateBody["name"] = name
}
if req.Code != nil {
code := strings.ToUpper(strings.TrimSpace(*req.Code))
if code == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Code cannot be empty")
}
if exists, err := s.Repository.CodeExists(c.Context(), code, &id); err != nil {
s.Log.Errorf("Failed to check product category code: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product category code")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product category with code %s already exists", code))
}
updateBody["code"] = code
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product category not found")
}
s.Log.Errorf("Failed to update product category: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s productCategoryService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Product category not found")
}
s.Log.Errorf("Failed to delete product category: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,17 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Code string `json:"code" validate:"required_strict,max=10"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
Code *string `json:"code,omitempty" validate:"omitempty,max=10"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ProductController struct {
ProductService service.ProductService
}
func NewProductController(productService service.ProductService) *ProductController {
return &ProductController{
ProductService: productService,
}
}
func (u *ProductController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.ProductService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all products successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProductListDTOs(result),
})
}
func (u *ProductController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.ProductService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get product successfully",
Data: dto.ToProductDetailDTO(*result),
})
}
func (u *ProductController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProductService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create product successfully",
Data: dto.ToProductDetailDTO(*result),
})
}
func (u *ProductController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProductService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update product successfully",
Data: dto.ToProductDetailDTO(*result),
})
}
func (u *ProductController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ProductService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete product successfully",
})
}
@@ -0,0 +1,116 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type ProductBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductListDTO struct {
ProductBaseDTO
Brand string `json:"brand"`
Sku *string `json:"sku,omitempty"`
ProductPrice float64 `json:"product_price"`
SellingPrice *float64 `json:"selling_price,omitempty"`
Tax *float64 `json:"tax,omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty"`
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
Flags []string `json:"flags"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ProductDetailDTO struct {
ProductListDTO
Flags []string `json:"flags"`
}
// === Mapper Functions ===
func ToProductBaseDTO(e entity.Product) ProductBaseDTO {
return ProductBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToProductListDTO(e entity.Product) ProductListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
var categoryRef *productCategoryDTO.ProductCategoryBaseDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
categoryRef = &mapped
}
suppliers := make([]supplierDTO.SupplierBaseDTO, len(e.Suppliers))
for i, s := range e.Suppliers {
suppliers[i] = supplierDTO.ToSupplierBaseDTO(s)
}
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return ProductListDTO{
Brand: e.Brand,
Sku: e.Sku,
ProductPrice: e.ProductPrice,
SellingPrice: e.SellingPrice,
Tax: e.Tax,
ExpiryPeriod: e.ExpiryPeriod,
ProductBaseDTO: ToProductBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Uom: uomRef,
ProductCategory: categoryRef,
Suppliers: suppliers,
Flags: flags,
}
}
func ToProductListDTOs(e []entity.Product) []ProductListDTO {
result := make([]ProductListDTO, len(e))
for i, r := range e {
result[i] = ToProductListDTO(r)
}
return result
}
func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return ProductDetailDTO{
ProductListDTO: ToProductListDTO(e),
Flags: flags,
}
}
@@ -0,0 +1,26 @@
package products
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
sProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProductModule struct{}
func (ProductModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
productRepo := rProduct.NewProductRepository(db)
userRepo := rUser.NewUserRepository(db)
productService := sProduct.NewProductService(productRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProductRoutes(router, userService, productService)
}
@@ -0,0 +1,196 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ProductRepository interface {
repository.BaseRepository[entity.Product]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error)
UomExists(ctx context.Context, uomID uint) (bool, error)
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
GetFlags(ctx context.Context, productID uint) ([]entity.Flag, error)
}
type ProductRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Product]
}
func NewProductRepository(db *gorm.DB) ProductRepository {
return &ProductRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Product](db),
}
}
func (r *ProductRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Product](ctx, r.DB(), name, excludeID)
}
func (r *ProductRepositoryImpl) SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error) {
var count int64
q := r.DB().WithContext(ctx).
Model(new(entity.Product)).
Where("sku = ?", sku).
Where("deleted_at IS NULL")
if excludeID != nil {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductRepositoryImpl) UomExists(ctx context.Context, uomID uint) (bool, error) {
var count int64
if err := r.DB().WithContext(ctx).
Model(&entity.Uom{}).
Where("id = ?", uomID).
Count(&count).
Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductRepositoryImpl) CategoryExists(ctx context.Context, categoryID uint) (bool, error) {
var count int64
if err := r.DB().WithContext(ctx).
Model(&entity.ProductCategory{}).
Where("id = ?", categoryID).
Count(&count).
Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductRepositoryImpl) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) {
if len(supplierIDs) == 0 {
return nil, nil
}
var suppliers []entity.Supplier
if err := r.DB().WithContext(ctx).
Select("id", "category").
Where("id IN ?", supplierIDs).
Find(&suppliers).
Error; err != nil {
return nil, err
}
return suppliers, nil
}
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error {
db := tx
if db == nil {
db = r.DB()
}
if supplierIDs == nil {
return db.WithContext(ctx).
Where("product_id = ?", productID).
Delete(&entity.ProductSupplier{}).
Error
}
var existing []entity.ProductSupplier
if err := db.WithContext(ctx).
Where("product_id = ?", productID).
Find(&existing).
Error; err != nil {
return err
}
existingMap := make(map[uint]struct{}, len(existing))
for _, rel := range existing {
existingMap[rel.SupplierID] = struct{}{}
}
incomingMap := make(map[uint]struct{}, len(supplierIDs))
for _, id := range supplierIDs {
incomingMap[id] = struct{}{}
if _, exists := existingMap[id]; exists {
continue
}
record := entity.ProductSupplier{ProductID: productID, SupplierID: id}
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err
}
}
for _, rel := range existing {
if _, keep := incomingMap[rel.SupplierID]; !keep {
if err := db.WithContext(ctx).
Where("product_id = ? AND supplier_id = ?", productID, rel.SupplierID).
Delete(&entity.ProductSupplier{}).
Error; err != nil {
return err
}
}
}
return nil
}
func (r *ProductRepositoryImpl) SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error {
db := tx
if db == nil {
db = r.DB()
}
// Hapus flags lama terlebih dahulu
if err := db.WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", productID, entity.FlagableTypeProduct).
Delete(&entity.Flag{}).
Error; err != nil {
return err
}
// Insert flags baru jika ada
if len(flags) > 0 {
newFlags := make([]entity.Flag, len(flags))
for i, f := range flags {
newFlags[i] = entity.Flag{
Name: f,
FlagableID: productID,
FlagableType: entity.FlagableTypeProduct,
}
}
if err := db.WithContext(ctx).Create(&newFlags).Error; err != nil {
return err
}
}
return nil
}
func (r *ProductRepositoryImpl) DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error {
db := tx
if db == nil {
db = r.DB()
}
return db.WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", productID, entity.FlagableTypeProduct).
Delete(&entity.Flag{}).
Error
}
func (r *ProductRepositoryImpl) GetFlags(ctx context.Context, productID uint) ([]entity.Flag, error) {
var flags []entity.Flag
if err := r.DB().WithContext(ctx).
Where("flagable_id = ? AND flagable_type = ?", productID, entity.FlagableTypeProduct).
Find(&flags).
Error; err != nil {
return nil, err
}
return flags, nil
}
+28
View File
@@ -0,0 +1,28 @@
package products
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/controllers"
product "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProductRoutes(v1 fiber.Router, u user.UserService, s product.ProductService) {
ctrl := controller.NewProductController(s)
route := v1.Group("/products")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,384 @@
package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ProductService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Product, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Product, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Product, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type productService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProductRepository
}
func normalizeProductFlags(raw []string) ([]string, error) {
normalized, invalid := utils.NormalizeFlagsForGroup(raw, utils.FlagGroupProduct)
if len(invalid) > 0 {
invalidStr := strings.Join(utils.FlagTypesToStrings(invalid), ", ")
allowedStr := strings.Join(utils.FlagTypesToStrings(utils.AllowedFlagTypes(utils.FlagGroupProduct)), ", ")
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid product flags: %s. Allowed flags: %s", invalidStr, allowedStr))
}
return utils.FlagTypesToStrings(normalized), nil
}
func NewProductService(repo repository.ProductRepository, validate *validator.Validate) ProductService {
return &productService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s productService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Uom").
Preload("ProductCategory").
Preload("Flags").
Preload("Suppliers", func(db *gorm.DB) *gorm.DB {
return db.Order("suppliers.name ASC")
})
}
func (s productService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
products, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get products: %+v", err)
return nil, 0, err
}
return products, total, nil
}
func (s productService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) {
product, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
if err != nil {
s.Log.Errorf("Failed get product by id: %+v", err)
return nil, err
}
return product, nil
}
func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Product, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
ctx := c.Context()
name := strings.TrimSpace(req.Name)
brand := strings.TrimSpace(req.Brand)
var sku *string
if req.Sku != nil {
trimmed := strings.ToUpper(strings.TrimSpace(*req.Sku))
if trimmed != "" {
sku = &trimmed
}
}
if exists, err := s.Repository.NameExists(ctx, name, nil); err != nil {
s.Log.Errorf("Failed to check product name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with name %s already exists", name))
}
if sku != nil {
if exists, err := s.Repository.SkuExists(ctx, *sku, nil); err != nil {
s.Log.Errorf("Failed to check product sku: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product sku")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with sku %s already exists", *sku))
}
}
if err := common.EnsureRelations(ctx,
common.RelationCheck{Name: "Uom", ID: &req.UomID, Exists: s.Repository.UomExists},
common.RelationCheck{Name: "Product category", ID: &req.ProductCategoryID, Exists: s.Repository.CategoryExists},
); err != nil {
return nil, err
}
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
var err error
if len(supplierIDs) > 0 {
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
}
if len(suppliers) != len(supplierIDs) {
actual := make([]uint, len(suppliers))
for i, supplier := range suppliers {
actual[i] = supplier.Id
}
missing := utils.MissingUintIDs(supplierIDs, actual)
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
}
for _, sup := range suppliers {
if strings.ToUpper(sup.Category) != string(utils.SupplierCategorySapronak) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category SAPRONAK", sup.Id))
}
}
}
productFlags, flagErr := normalizeProductFlags(req.Flags)
if flagErr != nil {
return nil, flagErr
}
createBody := &entity.Product{
Name: name,
Brand: brand,
Sku: sku,
UomId: req.UomID,
ProductCategoryId: req.ProductCategoryID,
ProductPrice: req.ProductPrice,
SellingPrice: req.SellingPrice,
Tax: req.Tax,
ExpiryPeriod: req.ExpiryPeriod,
CreatedBy: 1,
}
err = s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(ctx, createBody, nil); err != nil {
return err
}
if err := s.Repository.SyncFlags(ctx, tx, createBody.Id, productFlags); err != nil {
return err
}
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
})
if err != nil {
s.Log.Errorf("Failed to create product: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Product, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name cannot be empty")
}
if exists, err := s.Repository.NameExists(c.Context(), name, &id); err != nil {
s.Log.Errorf("Failed to check product name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with name %s already exists", name))
}
updateBody["name"] = name
}
if req.Brand != nil {
brand := strings.TrimSpace(*req.Brand)
if brand == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Brand cannot be empty")
}
updateBody["brand"] = brand
}
if req.Sku != nil {
sku := strings.ToUpper(strings.TrimSpace(*req.Sku))
if sku == "" {
updateBody["sku"] = nil
} else {
if exists, err := s.Repository.SkuExists(c.Context(), sku, &id); err != nil {
s.Log.Errorf("Failed to check product sku: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product sku")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with sku %s already exists", sku))
}
updateBody["sku"] = sku
}
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Uom", ID: req.UomID, Exists: s.Repository.UomExists},
common.RelationCheck{Name: "Product category", ID: req.ProductCategoryID, Exists: s.Repository.CategoryExists},
); err != nil {
return nil, err
}
if req.UomID != nil {
updateBody["uom_id"] = *req.UomID
}
if req.ProductCategoryID != nil {
updateBody["product_category_id"] = *req.ProductCategoryID
}
if req.ProductPrice != nil {
updateBody["product_price"] = *req.ProductPrice
}
if req.SellingPrice != nil {
updateBody["selling_price"] = req.SellingPrice
}
if req.Tax != nil {
updateBody["tax"] = req.Tax
}
if req.ExpiryPeriod != nil {
updateBody["expiry_period"] = req.ExpiryPeriod
}
ctx := c.Context()
var suppliers []entity.Supplier
var supplierIDs []uint
var supplierUpdate bool
if req.SupplierIDs != nil {
supplierUpdate = true
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
if len(supplierIDs) > 0 {
var err error
suppliers, err = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
}
if len(suppliers) != len(supplierIDs) {
actual := make([]uint, len(suppliers))
for i, supplier := range suppliers {
actual[i] = supplier.Id
}
missing := utils.MissingUintIDs(supplierIDs, actual)
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
}
for _, sup := range suppliers {
if strings.ToUpper(sup.Category) != string(utils.SupplierCategorySapronak) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category SAPRONAK", sup.Id))
}
}
}
}
var (
flagUpdate bool
flagValues []string
)
if req.Flags != nil {
flagUpdate = true
var flagErr error
flagValues, flagErr = normalizeProductFlags(*req.Flags)
if flagErr != nil {
return nil, flagErr
}
}
if len(updateBody) == 0 && !supplierUpdate && !flagUpdate {
return s.GetOne(c, id)
}
err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if len(updateBody) > 0 {
if err := repoTx.PatchOne(ctx, id, updateBody, nil); err != nil {
return err
}
} else {
if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
return err
}
}
if supplierUpdate {
var ids []uint
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
return err
}
}
if flagUpdate {
if err := s.Repository.SyncFlags(ctx, tx, id, flagValues); err != nil {
return err
}
}
return nil
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
s.Log.Errorf("Failed to update product: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s productService) DeleteOne(c *fiber.Ctx, id uint) error {
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := s.Repository.SyncSuppliersDiff(c.Context(), tx, id, nil); err != nil {
return err
}
if err := s.Repository.DeleteFlags(c.Context(), tx, id); err != nil {
return err
}
return repoTx.DeleteOne(c.Context(), id)
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Product not found")
}
s.Log.Errorf("Failed to delete product: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,35 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Brand string `json:"brand" validate:"required_strict,min=2"`
Sku *string `json:"sku,omitempty" validate:"omitempty"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
ProductCategoryID uint `json:"product_category_id" validate:"required,gt=0"`
ProductPrice float64 `json:"product_price" validate:"required"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
Brand *string `json:"brand,omitempty" validate:"omitempty,min=2"`
Sku *string `json:"sku,omitempty" validate:"omitempty"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
ProductCategoryID *uint `json:"product_category_id,omitempty" validate:"omitempty,gt=0"`
ProductPrice *float64 `json:"product_price,omitempty" validate:"omitempty"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1"`
Search string `query:"search" validate:"omitempty,max=50"`
}
+12
View File
@@ -9,10 +9,16 @@ import (
areas "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas" areas "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas"
customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers" customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers"
fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs"
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs" kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations" locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks"
productcategories "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories"
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms" uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses" warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
products "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products"
banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -26,6 +32,12 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
kandangs.KandangModule{}, kandangs.KandangModule{},
warehouses.WarehouseModule{}, warehouses.WarehouseModule{},
customers.CustomerModule{}, customers.CustomerModule{},
suppliers.SupplierModule{},
fcrs.FcrModule{},
nonstocks.NonstockModule{},
productcategories.ProductCategoryModule{},
products.ProductModule{},
banks.BankModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type SupplierController struct {
SupplierService service.SupplierService
}
func NewSupplierController(supplierService service.SupplierService) *SupplierController {
return &SupplierController{
SupplierService: supplierService,
}
}
func (u *SupplierController) 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.SupplierService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.SupplierListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all suppliers successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToSupplierListDTOs(result),
})
}
func (u *SupplierController) 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.SupplierService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get supplier successfully",
Data: dto.ToSupplierListDTO(*result),
})
}
func (u *SupplierController) 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.SupplierService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create supplier successfully",
Data: dto.ToSupplierListDTO(*result),
})
}
func (u *SupplierController) 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.SupplierService.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 supplier successfully",
Data: dto.ToSupplierListDTO(*result),
})
}
func (u *SupplierController) 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.SupplierService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete supplier successfully",
})
}
@@ -0,0 +1,88 @@
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 SupplierBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Category string `json:"category"`
}
type SupplierListDTO struct {
SupplierBaseDTO
Pic string `json:"pic"`
Type string `json:"type"`
Hatchery *string `json:"hatchery,omitempty"`
Phone string `json:"phone"`
Email string `json:"email"`
Address string `json:"address"`
Npwp *string `json:"npwp,omitempty"`
AccountNumber *string `json:"account_number,omitempty"`
Balance float64 `json:"balance"`
DueDate int `json:"due_date"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type SupplierDetailDTO struct {
SupplierListDTO
}
// === Mapper Functions ===
func ToSupplierBaseDTO(e entity.Supplier) SupplierBaseDTO {
return SupplierBaseDTO{
Id: e.Id,
Name: e.Name,
Alias: e.Alias,
Category: e.Category,
}
}
func ToSupplierListDTO(e entity.Supplier) SupplierListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return SupplierListDTO{
Pic: e.Pic,
Type: e.Type,
Hatchery: e.Hatchery,
Phone: e.Phone,
Email: e.Email,
Address: e.Address,
Npwp: e.Npwp,
AccountNumber: e.AccountNumber,
Balance: e.Balance,
DueDate: e.DueDate,
SupplierBaseDTO: ToSupplierBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToSupplierListDTOs(e []entity.Supplier) []SupplierListDTO {
result := make([]SupplierListDTO, len(e))
for i, r := range e {
result[i] = ToSupplierListDTO(r)
}
return result
}
func ToSupplierDetailDTO(e entity.Supplier) SupplierDetailDTO {
return SupplierDetailDTO{
SupplierListDTO: ToSupplierListDTO(e),
}
}
@@ -0,0 +1,26 @@
package suppliers
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
sSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type SupplierModule struct{}
func (SupplierModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
supplierRepo := rSupplier.NewSupplierRepository(db)
userRepo := rUser.NewUserRepository(db)
supplierService := sSupplier.NewSupplierService(supplierRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
SupplierRoutes(router, userService, supplierService)
}
@@ -0,0 +1,30 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type SupplierRepository interface {
repository.BaseRepository[entity.Supplier]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type SupplierRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Supplier]
db *gorm.DB
}
func NewSupplierRepository(db *gorm.DB) SupplierRepository {
return &SupplierRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Supplier](db),
db: db,
}
}
func (r *SupplierRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Supplier](ctx, r.db, name, excludeID)
}
@@ -0,0 +1,28 @@
package suppliers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/controllers"
supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func SupplierRoutes(v1 fiber.Router, u user.UserService, s supplier.SupplierService) {
ctrl := controller.NewSupplierController(s)
route := v1.Group("/suppliers")
// 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,221 @@
package service
import (
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/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 SupplierService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Supplier, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Supplier, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Supplier, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Supplier, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type supplierService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.SupplierRepository
}
func NewSupplierService(repo repository.SupplierRepository, validate *validator.Validate) SupplierService {
return &supplierService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s supplierService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Supplier, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
suppliers, 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 suppliers: %+v", err)
return nil, 0, err
}
return suppliers, total, nil
}
func (s supplierService) GetOne(c *fiber.Ctx, id uint) (*entity.Supplier, error) {
supplier, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found")
}
if err != nil {
s.Log.Errorf("Failed get supplier by id: %+v", err)
return nil, err
}
return supplier, nil
}
func (s *supplierService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Supplier, 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 supplier name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with name %s already exists", req.Name))
}
typ := strings.ToUpper(req.Type)
if !utils.IsValidCustomerSupplierType(typ) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier type")
}
category := strings.ToUpper(req.Category)
if !utils.IsValidSupplierCategory(category) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier category")
}
alias := strings.TrimSpace(strings.ToUpper(req.Alias))
//TODO: created by dummy
createBody := &entity.Supplier{
Name: req.Name,
Alias: alias,
Pic: req.Pic,
Type: typ,
Category: category,
Hatchery: req.Hatchery,
Phone: req.Phone,
Email: req.Email,
Address: req.Address,
Npwp: req.Npwp,
AccountNumber: req.AccountNumber,
DueDate: req.DueDate,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create supplier: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s supplierService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Supplier, 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 supplier name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.Alias != nil {
updateBody["alias"] = strings.TrimSpace(strings.ToUpper(*req.Alias))
}
if req.Pic != nil {
updateBody["pic"] = *req.Pic
}
if req.Type != nil {
typ := strings.ToUpper(*req.Type)
if !utils.IsValidCustomerSupplierType(typ) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier type")
}
updateBody["type"] = typ
}
if req.Category != nil {
category := strings.ToUpper(*req.Category)
if !utils.IsValidSupplierCategory(category) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier category")
}
updateBody["category"] = category
}
if req.Hatchery != nil {
updateBody["hatchery"] = *req.Hatchery
}
if req.Phone != nil {
updateBody["phone"] = *req.Phone
}
if req.Email != nil {
updateBody["email"] = *req.Email
}
if req.Address != nil {
updateBody["address"] = *req.Address
}
if req.Npwp != nil {
updateBody["npwp"] = *req.Npwp
}
if req.AccountNumber != nil {
updateBody["account_number"] = *req.AccountNumber
}
if req.DueDate != nil {
updateBody["due_date"] = *req.DueDate
}
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, "Supplier not found")
}
s.Log.Errorf("Failed to update supplier: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s supplierService) 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, "Supplier not found")
}
s.Log.Errorf("Failed to delete supplier: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,37 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Alias string `json:"alias" validate:"required_strict,max=5"`
Pic string `json:"pic" validate:"required_strict"`
Type string `json:"type" validate:"required_strict"`
Category string `json:"category" validate:"required_strict"`
Hatchery *string `json:"hatchery,omitempty" validate:"omitempty"`
Phone string `json:"phone" validate:"required_strict,max=20"`
Email string `json:"email" validate:"required_strict,email"`
Address string `json:"address" validate:"required_strict"`
Npwp *string `json:"npwp,omitempty" validate:"omitempty,max=50"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
DueDate int `json:"due_date" validate:"required_strict,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
Alias *string `json:"alias,omitempty" validate:"omitempty,max=5"`
Pic *string `json:"pic,omitempty" validate:"omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty"`
Category *string `json:"category,omitempty" validate:"omitempty"`
Hatchery *string `json:"hatchery,omitempty" validate:"omitempty"`
Phone *string `json:"phone,omitempty" validate:"omitempty,max=20"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Address *string `json:"address,omitempty" validate:"omitempty"`
Npwp *string `json:"npwp,omitempty" validate:"omitempty,max=50"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
DueDate *int `json:"due_date,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"`
}
@@ -56,3 +56,9 @@ func ToUomListDTOs(e []entity.Uom) []UomListDTO {
} }
return result return result
} }
func ToUomDetailDTO(e entity.Uom) UomDetailDTO {
return UomDetailDTO{
UomListDTO: ToUomListDTO(e),
}
}
@@ -98,7 +98,7 @@ func (s *uomService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Uo
return nil, err return nil, err
} }
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations) return s.GetOne(c, createBody.Id)
} }
func (s uomService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Uom, error) { func (s uomService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Uom, error) {
@@ -5,7 +5,7 @@ type Create struct {
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
} }
type Query struct { type Query struct {
@@ -85,3 +85,9 @@ func ToWarehouseListDTOs(e []entity.Warehouse) []WarehouseListDTO {
} }
return result return result
} }
func ToWarehouseDetailDTO(e entity.Warehouse) WarehouseDetailDTO {
return WarehouseDetailDTO{
WarehouseListDTO: ToWarehouseListDTO(e),
}
}
@@ -122,7 +122,7 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return nil, err return nil, err
} }
return s.Repository.GetByID(c.Context(), createBody.Id, s.withRelations) return s.GetOne(c, createBody.Id)
} }
func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Warehouse, error) { func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Warehouse, error) {
@@ -152,22 +152,20 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
updateBody["type"] = normalizedType updateBody["type"] = normalizedType
req.Type = &normalizedType req.Type = &normalizedType
} }
if req.AreaId != nil { if err := common.EnsureRelations(c.Context(),
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Area", ID: req.AreaId, Exists: s.Repository.AreaExists}); err != nil { 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 return nil, err
} }
if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId updateBody["area_id"] = *req.AreaId
} }
if req.LocationId != nil { 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 updateBody["location_id"] = req.LocationId
} }
if req.KandangId != nil { 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 updateBody["kandang_id"] = req.KandangId
} }
@@ -9,7 +9,7 @@ type Create struct {
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty"` Type *string `json:"type,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
@@ -5,7 +5,7 @@ type Create struct {
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
} }
type Query struct { type Query struct {
+3 -1
View File
@@ -2,14 +2,15 @@ package route
import ( import (
"gitlab.com/mbugroup/lti-api.git/internal/common/validation" "gitlab.com/mbugroup/lti-api.git/internal/common/validation"
"gitlab.com/mbugroup/lti-api.git/internal/modules"
trim "gitlab.com/mbugroup/lti-api.git/internal/middleware/trim" trim "gitlab.com/mbugroup/lti-api.git/internal/middleware/trim"
"gitlab.com/mbugroup/lti-api.git/internal/modules"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master" master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -22,6 +23,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
allModules := []modules.Module{ allModules := []modules.Module{
users.UserModule{}, users.UserModule{},
master.MasterModule{}, master.MasterModule{},
constants.ConstantModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }
+153 -6
View File
@@ -1,14 +1,68 @@
package utils package utils
import "strings"
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// FlagType // FlagType & Groups
// ------------------------------------------------------------------- // -------------------------------------------------------------------
type FlagType string type FlagType string
type FlagGroup string
const ( const (
FlagIsActive FlagType = "IS_ACTIVE" FlagIsActive FlagType = "IS_ACTIVE"
FlagDOC FlagType = "DOC"
FlagPakan FlagType = "PAKAN"
FlagPreStarter FlagType = "PRE-STARTER"
FlagStarter FlagType = "STARTER"
FlagFinisher FlagType = "FINISHER"
FlagOVK FlagType = "OVK"
FlagObat FlagType = "OBAT"
FlagVitamin FlagType = "VITAMIN"
FlagKimia FlagType = "KIMIA"
FlagEkspedisi FlagType = "EKSPEDISI"
) )
const (
FlagGroupProduct FlagGroup = "PRODUCT"
FlagGroupNonstock FlagGroup = "NONSTOCK"
)
var flagGroupOptions = map[FlagGroup][]FlagType{
FlagGroupProduct: {
FlagDOC,
FlagPakan,
FlagPreStarter,
FlagStarter,
FlagFinisher,
FlagOVK,
FlagObat,
FlagVitamin,
FlagKimia,
},
FlagGroupNonstock: {
FlagEkspedisi,
},
}
var allFlagTypes = func() map[FlagType]struct{} {
m := map[FlagType]struct{}{
FlagIsActive: {},
}
for _, flags := range flagGroupOptions {
for _, flag := range flags {
m[flag] = struct{}{}
}
}
return m
}()
func AllFlagTypes() map[FlagType]struct{} {
return allFlagTypes
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// WarehouseType // WarehouseType
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -32,16 +86,101 @@ const (
CustomerSupplierTypeIndividual CustomerSupplierType = "INDIVIDUAL" CustomerSupplierTypeIndividual CustomerSupplierType = "INDIVIDUAL"
) )
// -------------------------------------------------------------------
// SupplierCategory
// -------------------------------------------------------------------
type SupplierCategory string
const (
SupplierCategoryBOP SupplierCategory = "BOP"
SupplierCategorySapronak SupplierCategory = "SAPRONAK"
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Validators // Validators
// ------------------------------------------------------------------- // -------------------------------------------------------------------
func IsValidFlagType(v string) bool { func IsValidFlagType(v string) bool {
switch FlagType(v) { _, ok := allFlagTypes[FlagType(strings.ToUpper(strings.TrimSpace(v)))]
case FlagIsActive: return ok
return true }
func AllowedFlagTypes(group FlagGroup) []FlagType {
options, ok := flagGroupOptions[group]
if !ok {
return nil
} }
return false result := make([]FlagType, len(options))
copy(result, options)
return result
}
func InvalidFlagsForGroup(flags []FlagType, group FlagGroup) []FlagType {
if len(flags) == 0 {
return nil
}
options, ok := flagGroupOptions[group]
if !ok {
return flags
}
allowed := make(map[FlagType]struct{}, len(options))
for _, flag := range options {
allowed[flag] = struct{}{}
}
invalid := make([]FlagType, 0)
for _, flag := range flags {
if _, ok := allowed[flag]; !ok {
invalid = append(invalid, flag)
}
}
return invalid
}
func NormalizeFlagTypes(flags []string) []FlagType {
if len(flags) == 0 {
return nil
}
seen := make(map[FlagType]struct{}, len(flags))
result := make([]FlagType, 0, len(flags))
for _, flag := range flags {
normalized := FlagType(strings.ToUpper(strings.TrimSpace(flag)))
if normalized == "" {
continue
}
if _, exists := seen[normalized]; exists {
continue
}
seen[normalized] = struct{}{}
result = append(result, normalized)
}
if len(result) == 0 {
return nil
}
return result
}
func FlagTypesToStrings(flags []FlagType) []string {
if len(flags) == 0 {
return nil
}
result := make([]string, len(flags))
for i, flag := range flags {
result[i] = string(flag)
}
return result
}
func NormalizeFlagsForGroup(raw []string, group FlagGroup) (normalized []FlagType, invalid []FlagType) {
normalized = NormalizeFlagTypes(raw)
if len(normalized) == 0 {
return nil, nil
}
invalid = InvalidFlagsForGroup(normalized, group)
if len(invalid) == 0 {
return normalized, nil
}
return normalized, invalid
} }
func IsValidWarehouseType(v string) bool { func IsValidWarehouseType(v string) bool {
@@ -60,10 +199,18 @@ func IsValidCustomerSupplierType(v string) bool {
return false return false
} }
func IsValidSupplierCategory(v string) bool {
switch SupplierCategory(v) {
case SupplierCategoryBOP, SupplierCategorySapronak:
return true
}
return false
}
// example use // example use
/** /**
if !utils.IsValidFlagType(req.FlagName) { if !utils.IsValidFlagType(req.FlagName) {
return fiber.NewError(fiber.StatusBadRequest, "invalid flag type") return fiber.NewError(fiber.StatusBadRequest, "Invalid flag type")
} }
*/ */
+34
View File
@@ -0,0 +1,34 @@
package utils
// UniqueUintSlice returns a new slice containing distinct values in the order of first appearance.
func UniqueUintSlice(values []uint) []uint {
if len(values) == 0 {
return nil
}
seen := make(map[uint]struct{}, len(values))
result := make([]uint, 0, len(values))
for _, v := range values {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
result = append(result, v)
}
return result
}
// MissingUintIDs returns the values present in expected but missing from actual.
func MissingUintIDs(expected, actual []uint) []uint {
lookup := make(map[uint]struct{}, len(actual))
for _, v := range actual {
lookup[v] = struct{}{}
}
missing := make([]uint, 0)
for _, v := range expected {
if _, ok := lookup[v]; !ok {
missing = append(missing, v)
}
}
return missing
}
+127
View File
@@ -0,0 +1,127 @@
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"
)
func TestBankIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
const bankAlias = "BNI"
const bankName = "Bank Negara Indonesia"
const bankOwner = "John Doe"
const bankAccountNumber = "1234567890"
var bankID uint
t.Run("creating bank succeeds", func(t *testing.T) {
bankID = createBank(t, app, bankName, bankAlias, bankAccountNumber, bankOwner)
bank := fetchBank(t, db, bankID)
if bank.Alias != bankAlias {
t.Fatalf("expected alias %q, got %q", bankAlias, bank.Alias)
}
if bank.AccountNumber != bankAccountNumber {
t.Fatalf("expected account number %q, got %q", bankAccountNumber, bank.AccountNumber)
}
})
t.Run("creating bank with duplicate name fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/banks", map[string]any{
"name": bankName,
"alias": "NEWALIAS",
"owner": "Owner Name",
"account_number": "1122334455",
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when creating duplicate bank name, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("getting existing bank succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/banks/%d", bankID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching bank, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
AccountNumber string `json:"account_number"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if payload.Data.Id != bankID {
t.Fatalf("expected id %d, got %d", bankID, payload.Data.Id)
}
if payload.Data.Alias != bankAlias {
t.Fatalf("expected alias %q, got %q", bankAlias, payload.Data.Alias)
}
if payload.Data.AccountNumber != bankAccountNumber {
t.Fatalf("expected account number %q, got %q", bankAccountNumber, payload.Data.AccountNumber)
}
})
const updatedName = "BNI Updated"
t.Run("updating bank name succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/banks/%d", bankID), map[string]any{
"name": updatedName,
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating bank, 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 response: %v", err)
}
if payload.Data.Name != updatedName {
t.Fatalf("expected updated name %q, got %q", updatedName, payload.Data.Name)
}
bank := fetchBank(t, db, bankID)
if bank.Name != updatedName {
t.Fatalf("expected persisted name %q, got %q", updatedName, bank.Name)
}
})
t.Run("updating non existent bank returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, "/api/master-data/banks/9999", map[string]any{
"name": "Does Not Matter",
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when updating missing bank, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("deleting bank succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/banks/%d", bankID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting bank, got %d: %s", resp.StatusCode, string(body))
}
var bank entities.Bank
if err := db.First(&bank, bankID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("expected bank to be deleted, got error %v", err)
}
})
t.Run("deleting non existent bank returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/banks/%d", bankID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when deleting missing bank, got %d: %s", resp.StatusCode, string(body))
}
})
}
@@ -24,7 +24,7 @@ func TestCustomerIntegration(t *testing.T) {
"type": utils.CustomerSupplierTypeBisnis, "type": utils.CustomerSupplierTypeBisnis,
"address": "Somewhere", "address": "Somewhere",
"phone": "0800000000", "phone": "0800000000",
"email": "invalid@example.com", "email": "Invalid@example.com",
"account_number": "ACC-INVALID", "account_number": "ACC-INVALID",
}) })
if resp.StatusCode != fiber.StatusNotFound { if resp.StatusCode != fiber.StatusNotFound {
@@ -32,18 +32,18 @@ func TestCustomerIntegration(t *testing.T) {
} }
}) })
t.Run("creating customer with invalid type fails", func(t *testing.T) { 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{ resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{
"name": "Invalid Type", "name": "Invalid Type",
"pic_id": 1, "pic_id": 1,
"type": "UNKNOWN", "type": "UNKNOWN",
"address": "Somewhere", "address": "Somewhere",
"phone": "081234567891", "phone": "081234567891",
"email": "invalid-type@example.com", "email": "Invalid-type@example.com",
"account_number": "ACC-INVALID-TYPE", "account_number": "ACC-INVALID-TYPE",
}) })
if resp.StatusCode != fiber.StatusBadRequest { if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when type is invalid, got %d: %s", resp.StatusCode, string(body)) t.Fatalf("expected 400 when type is Invalid, got %d: %s", resp.StatusCode, string(body))
} }
}) })
@@ -140,16 +140,16 @@ func TestCustomerIntegration(t *testing.T) {
} }
}) })
t.Run("updating customer with invalid type fails", func(t *testing.T) { 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{ resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{
"type": "random-type", "type": "random-type",
}) })
if resp.StatusCode != fiber.StatusBadRequest { if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating with invalid type, got %d: %s", resp.StatusCode, string(body)) t.Fatalf("expected 400 when updating with Invalid type, got %d: %s", resp.StatusCode, string(body))
} }
customer := fetchCustomer(t, db, customerID) customer := fetchCustomer(t, db, customerID)
if customer.Type != string(utils.CustomerSupplierTypeIndividual) { if customer.Type != string(utils.CustomerSupplierTypeIndividual) {
t.Fatalf("expected type to remain %q after invalid update, got %q", utils.CustomerSupplierTypeIndividual, customer.Type) t.Fatalf("expected type to remain %q after Invalid update, got %q", utils.CustomerSupplierTypeIndividual, customer.Type)
} }
}) })
+218
View File
@@ -0,0 +1,218 @@
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"
)
func TestFcrIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
t.Run("creating fcr without standards fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/fcrs", map[string]any{
"name": "FCR Alpha",
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when standards missing, got %d: %s", resp.StatusCode, string(body))
}
})
initialStandards := []map[string]any{
{
"weight": 7.2,
"fcr_number": 400.0,
"mortality": 12.0,
},
{
"weight": 7.4,
"fcr_number": 410.0,
"mortality": 11.5,
},
}
var fcrID uint
t.Run("creating fcr succeeds", func(t *testing.T) {
fcrID = createFcr(t, app, "FCR Layer", initialStandards)
fcr := fetchFcr(t, db, fcrID)
if fcr.Name != "FCR Layer" {
t.Fatalf("expected name FCR Layer, got %q", fcr.Name)
}
if len(fcr.Standards) != len(initialStandards) {
t.Fatalf("expected %d standards, got %d", len(initialStandards), len(fcr.Standards))
}
if fcr.CreatedBy != 1 {
t.Fatalf("expected created_by 1, got %d", fcr.CreatedBy)
}
})
t.Run("creating duplicate fcr name fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/fcrs", map[string]any{
"name": "FCR Layer",
"fcr_standards": initialStandards,
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when creating duplicate, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("getting fcr detail succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching fcr, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Id uint `json:"id"`
Name string `json:"name"`
FcrStandards []map[string]any `json:"fcr_standards"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse fcr detail: %v", err)
}
if payload.Data.Id != fcrID {
t.Fatalf("expected id %d, got %d", fcrID, payload.Data.Id)
}
if len(payload.Data.FcrStandards) != len(initialStandards) {
t.Fatalf("expected %d standards in response, got %d", len(initialStandards), len(payload.Data.FcrStandards))
}
})
updatedStandards := []map[string]any{
{
"weight": 7.2,
"fcr_number": 395.0,
"mortality": 10.0,
},
{
"weight": 7.5,
"fcr_number": 420.0,
"mortality": 13.0,
},
}
t.Run("updating fcr name and standards succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), map[string]any{
"name": "FCR Layer Updated",
"fcr_standards": updatedStandards,
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating fcr, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Name string `json:"name"`
FcrStandards []map[string]any `json:"fcr_standards"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse update response: %v", err)
}
if payload.Data.Name != "FCR Layer Updated" {
t.Fatalf("expected updated name, got %q", payload.Data.Name)
}
if len(payload.Data.FcrStandards) != len(updatedStandards) {
t.Fatalf("expected %d standards after update, got %d", len(updatedStandards), len(payload.Data.FcrStandards))
}
fcr := fetchFcr(t, db, fcrID)
if fcr.Name != "FCR Layer Updated" {
t.Fatalf("expected persisted name FCR Layer Updated, got %q", fcr.Name)
}
if len(fcr.Standards) != len(updatedStandards) {
t.Fatalf("expected %d persisted standards, got %d", len(updatedStandards), len(fcr.Standards))
}
if fcr.Standards[0].FcrNumber != 395.0 {
t.Fatalf("expected first standard fcr_number 395, got %f", fcr.Standards[0].FcrNumber)
}
})
var otherFcrID uint
t.Run("creating another fcr for duplicate update", func(t *testing.T) {
otherFcrID = createFcr(t, app, "FCR Grower", []map[string]any{
{
"weight": 8.0,
"fcr_number": 430.0,
"mortality": 9.0,
},
})
if otherFcrID == 0 {
t.Fatal("expected other fcr id to be non zero")
}
})
t.Run("updating fcr with duplicate name fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), map[string]any{
"name": "FCR Grower",
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when renaming to existing fcr, got %d: %s", resp.StatusCode, string(body))
}
fcr := fetchFcr(t, db, fcrID)
if fcr.Name != "FCR Layer Updated" {
t.Fatalf("expected name unchanged after failed update, got %q", fcr.Name)
}
})
t.Run("updating fcr with invalid standards fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), map[string]any{
"fcr_standards": []map[string]any{
{
"weight": -1,
"fcr_number": 300,
"mortality": 5,
},
},
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating with invalid standard, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("deleting fcr succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting fcr, got %d: %s", resp.StatusCode, string(body))
}
var fcr entities.Fcr
if err := db.First(&fcr, fcrID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("expected fcr to be deleted, got error %v", err)
}
var standardsCount int64
if err := db.Model(&entities.FcrStandard{}).Where("fcr_id = ?", fcrID).Count(&standardsCount).Error; err != nil {
t.Fatalf("failed counting standards: %v", err)
}
if standardsCount != 0 {
t.Fatalf("expected standards removed, found %d", standardsCount)
}
})
t.Run("deleting fcr again returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when deleting missing fcr, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("getting deleted fcr returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when fetching deleted fcr, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("cleanup other fcr", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/fcrs/%d", otherFcrID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting other fcr, got %d: %s", resp.StatusCode, string(body))
}
})
}
+190 -1
View File
@@ -44,6 +44,16 @@ func setupIntegrationApp(t *testing.T) (*fiber.App, *gorm.DB) {
&entities.Warehouse{}, &entities.Warehouse{},
&entities.Uom{}, &entities.Uom{},
&entities.Customer{}, &entities.Customer{},
&entities.Supplier{},
&entities.Flag{},
&entities.ProductCategory{},
&entities.Nonstock{},
&entities.NonstockSupplier{},
&entities.Product{},
&entities.ProductSupplier{},
&entities.Fcr{},
&entities.FcrStandard{},
&entities.Bank{},
); err != nil { ); err != nil {
t.Fatalf("auto migrate failed: %v", err) t.Fatalf("auto migrate failed: %v", err)
} }
@@ -60,7 +70,7 @@ func setupIntegrationApp(t *testing.T) (*fiber.App, *gorm.DB) {
t.Fatalf("failed to seed user: %v", err) t.Fatalf("failed to seed user: %v", err)
} }
app := fiber.New() app := fiber.New(fiber.Config{ErrorHandler: utils.ErrorHandler})
route.Routes(app, db) route.Routes(app, db)
return app, db return app, db
} }
@@ -129,6 +139,15 @@ func createLocation(t *testing.T, app *fiber.App, name, address string, areaID u
return parseID(t, body) return parseID(t, body)
} }
func createUom(t *testing.T, app *fiber.App, name string) uint {
t.Helper()
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/uoms", map[string]any{"name": name})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating uom, 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 { func createKandang(t *testing.T, app *fiber.App, name string, locationID, picID uint) uint {
t.Helper() t.Helper()
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{ resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{
@@ -169,6 +188,152 @@ func fetchCustomer(t *testing.T, db *gorm.DB, id uint) entities.Customer {
return customer return customer
} }
func createSupplier(t *testing.T, app *fiber.App, name, alias, category string) uint {
t.Helper()
identifier := strings.ToLower(strings.ReplaceAll(name, " ", "_"))
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/suppliers", map[string]any{
"name": name,
"alias": alias,
"pic": "John Doe",
"type": utils.CustomerSupplierTypeBisnis,
"category": category,
"hatchery": "Hatchery A",
"phone": "081234567890",
"email": fmt.Sprintf("%s@supplier.com", identifier),
"address": "Supplier address",
"npwp": "NPWP-123",
"account_number": "ACC-SUPPLIER",
"due_date": 30,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating supplier, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func createProductCategory(t *testing.T, app *fiber.App, name, code string) uint {
t.Helper()
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/product-categories", map[string]any{
"name": name,
"code": code,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating product category, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func fetchProductCategory(t *testing.T, db *gorm.DB, id uint) entities.ProductCategory {
t.Helper()
var pc entities.ProductCategory
if err := db.Preload("CreatedUser").First(&pc, id).Error; err != nil {
t.Fatalf("failed to fetch product category: %v", err)
}
return pc
}
func createProduct(t *testing.T, app *fiber.App, name, brand string, sku *string, uomID, categoryID uint, productPrice float64, supplierIDs []uint, flags []string) uint {
t.Helper()
payload := map[string]any{
"name": name,
"brand": brand,
"uom_id": uomID,
"product_category_id": categoryID,
"product_price": productPrice,
"supplier_ids": supplierIDs,
}
if sku != nil {
payload["sku"] = *sku
}
if len(flags) > 0 {
payload["flags"] = flags
}
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", payload)
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating product, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func fetchProduct(t *testing.T, db *gorm.DB, id uint) entities.Product {
t.Helper()
var product entities.Product
if err := db.Preload("CreatedUser").
Preload("Uom").
Preload("ProductCategory").
Preload("Suppliers", func(tx *gorm.DB) *gorm.DB { return tx.Order("suppliers.name ASC") }).
Preload("Flags", func(tx *gorm.DB) *gorm.DB { return tx.Order("flags.name ASC") }).
First(&product, id).Error; err != nil {
t.Fatalf("failed to fetch product: %v", err)
}
return product
}
func fetchSupplier(t *testing.T, db *gorm.DB, id uint) entities.Supplier {
t.Helper()
var supplier entities.Supplier
if err := db.Preload("CreatedUser").First(&supplier, id).Error; err != nil {
t.Fatalf("failed to fetch supplier: %v", err)
}
return supplier
}
func createFcr(t *testing.T, app *fiber.App, name string, standards []map[string]any) uint {
t.Helper()
payload := map[string]any{
"name": name,
"fcr_standards": standards,
}
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/fcrs", payload)
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating fcr, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func fetchFcr(t *testing.T, db *gorm.DB, id uint) entities.Fcr {
t.Helper()
var fcr entities.Fcr
if err := db.Preload("CreatedUser").
Preload("Standards", func(tx *gorm.DB) *gorm.DB {
return tx.Order("weight ASC")
}).
First(&fcr, id).Error; err != nil {
t.Fatalf("failed to fetch fcr: %v", err)
}
return fcr
}
func createNonstock(t *testing.T, app *fiber.App, name string, uomID uint, supplierIDs []uint, flags []string) uint {
t.Helper()
payload := map[string]any{
"name": name,
"uom_id": uomID,
"supplier_ids": supplierIDs,
}
if len(flags) > 0 {
payload["flags"] = flags
}
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", payload)
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating nonstock, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func fetchNonstock(t *testing.T, db *gorm.DB, id uint) entities.Nonstock {
t.Helper()
var nonstock entities.Nonstock
if err := db.Preload("CreatedUser").
Preload("Uom").
Preload("Suppliers", func(tx *gorm.DB) *gorm.DB { return tx.Order("suppliers.name ASC") }).
Preload("Flags", func(tx *gorm.DB) *gorm.DB { return tx.Order("flags.name ASC") }).
First(&nonstock, id).Error; err != nil {
t.Fatalf("failed to fetch nonstock: %v", err)
}
return nonstock
}
func fetchAreaName(t *testing.T, db *gorm.DB, id uint) string { func fetchAreaName(t *testing.T, db *gorm.DB, id uint) string {
t.Helper() t.Helper()
var area entities.Area var area entities.Area
@@ -186,3 +351,27 @@ func fetchWarehouse(t *testing.T, db *gorm.DB, id uint) entities.Warehouse {
} }
return wh return wh
} }
func createBank(t *testing.T, app *fiber.App, name, alias, accountNumber string, owner any) uint {
t.Helper()
payload := map[string]any{
"name": name,
"alias": alias,
"account_number": accountNumber,
"owner": owner,
}
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/banks", payload)
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating bank, got %d: %s", resp.StatusCode, string(body))
}
return parseID(t, body)
}
func fetchBank(t *testing.T, db *gorm.DB, id uint) entities.Bank {
t.Helper()
var bank entities.Bank
if err := db.Preload("CreatedUser").First(&bank, id).Error; err != nil {
t.Fatalf("failed to fetch bank: %v", err)
}
return bank
}
@@ -0,0 +1,309 @@
package test
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"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 TestNonstockIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
uomID := createUom(t, app, "Unit Piece")
altUomID := createUom(t, app, "Unit Box")
supplierID1 := createSupplier(t, app, "Nonstock Supplier One", "ns1", string(utils.SupplierCategoryBOP))
supplierID2 := createSupplier(t, app, "Nonstock Supplier Two", "ns2", string(utils.SupplierCategoryBOP))
supplierID3 := createSupplier(t, app, "Nonstock Supplier Three", "ns3", string(utils.SupplierCategoryBOP))
sapronakSupplierID := createSupplier(t, app, "SAPRONAK Supplier", "sap1", string(utils.SupplierCategorySapronak))
nonstockFlags := []string{string(utils.FlagEkspedisi)}
t.Run("create nonstock without suppliers succeeds with empty relations", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{
"name": "Supplierless Nonstock",
"uom_id": uomID,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when suppliers omitted, got %d: %s", resp.StatusCode, string(body))
}
id := parseID(t, body)
ns := fetchNonstock(t, db, id)
if len(ns.Suppliers) != 0 {
t.Fatalf("expected no suppliers persisted, found %d", len(ns.Suppliers))
}
if len(ns.Flags) != 0 {
t.Fatalf("expected no flags persisted, found %d", len(ns.Flags))
}
resp, _ = doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", id), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected cleanup delete to succeed, got %d", resp.StatusCode)
}
})
t.Run("create nonstock with unknown supplier fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{
"name": "Unknown Supplier Nonstock",
"uom_id": uomID,
"supplier_ids": []uint{99999},
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when supplier missing, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("create nonstock with sapronak supplier fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{
"name": "Invalid Category Nonstock",
"uom_id": uomID,
"supplier_ids": []uint{sapronakSupplierID},
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when supplier category is not BOP, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("create nonstock with invalid flags fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{
"name": "Invalid Flag Nonstock",
"uom_id": uomID,
"supplier_ids": []uint{supplierID1},
"flags": []string{"UNKNOWN"},
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when flags invalid, got %d: %s", resp.StatusCode, string(body))
}
})
var nonstockID uint
t.Run("create nonstock succeeds", func(t *testing.T) {
nonstockID = createNonstock(t, app, "Layer Feed", uomID, []uint{supplierID1, supplierID2, supplierID1}, nonstockFlags)
if nonstockID == 0 {
t.Fatal("expected nonstock id to be non zero")
}
ns := fetchNonstock(t, db, nonstockID)
if ns.Name != "Layer Feed" {
t.Fatalf("expected name Layer Feed, got %q", ns.Name)
}
if ns.UomId != uomID {
t.Fatalf("expected uom_id %d, got %d", uomID, ns.UomId)
}
if ns.CreatedBy != 1 {
t.Fatalf("expected created_by 1, got %d", ns.CreatedBy)
}
if len(ns.Suppliers) != 2 {
t.Fatalf("expected 2 unique suppliers, got %d", len(ns.Suppliers))
}
if len(ns.Flags) != len(nonstockFlags) {
t.Fatalf("expected %d flags, got %d", len(nonstockFlags), len(ns.Flags))
}
expectedFlags := make(map[string]struct{}, len(nonstockFlags))
for _, flag := range nonstockFlags {
expectedFlags[strings.ToUpper(flag)] = struct{}{}
}
for _, flag := range ns.Flags {
upper := strings.ToUpper(flag.Name)
if _, ok := expectedFlags[upper]; !ok {
t.Fatalf("unexpected flag stored: %s", upper)
}
delete(expectedFlags, upper)
}
if len(expectedFlags) != 0 {
t.Fatalf("missing flags after create: %v", expectedFlags)
}
})
t.Run("get nonstock detail includes suppliers", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching nonstock, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Id uint `json:"id"`
Name string `json:"name"`
UomID uint `json:"uom_id"`
Suppliers []map[string]any `json:"suppliers"`
Flags []string `json:"flags"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse nonstock detail: %v", err)
}
if payload.Data.Id != nonstockID {
t.Fatalf("expected id %d, got %d", nonstockID, payload.Data.Id)
}
if payload.Data.UomID != uomID {
t.Fatalf("expected response uom_id %d, got %d", uomID, payload.Data.UomID)
}
if len(payload.Data.Suppliers) != 2 {
t.Fatalf("expected 2 suppliers in response, got %d", len(payload.Data.Suppliers))
}
if len(payload.Data.Flags) != len(nonstockFlags) {
t.Fatalf("expected %d flags in response, got %d", len(nonstockFlags), len(payload.Data.Flags))
}
expected := make(map[string]struct{}, len(nonstockFlags))
for _, flag := range nonstockFlags {
expected[strings.ToUpper(flag)] = struct{}{}
}
for _, flag := range payload.Data.Flags {
flag = strings.ToUpper(flag)
if _, ok := expected[flag]; !ok {
t.Fatalf("unexpected flag %s returned", flag)
}
delete(expected, flag)
}
if len(expected) != 0 {
t.Fatalf("missing flags in response: %v", expected)
}
})
t.Run("update nonstock with invalid uom fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
"uom_id": 99999,
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when updating with invalid uom, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("update nonstock with invalid supplier fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
"supplier_ids": []uint{supplierID1, 99999},
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when updating with invalid supplier, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("update nonstock with sapronak supplier fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
"supplier_ids": []uint{sapronakSupplierID},
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating with non-BOP supplier, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("update nonstock with invalid flags fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
"flags": []string{"BAD"},
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating flags invalid, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("update nonstock name uom and suppliers succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
"name": "Layer Feed Premium",
"uom_id": altUomID,
"supplier_ids": []uint{supplierID3},
"flags": nonstockFlags,
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating nonstock, got %d: %s", resp.StatusCode, string(body))
}
ns := fetchNonstock(t, db, nonstockID)
if ns.Name != "Layer Feed Premium" {
t.Fatalf("expected name Layer Feed Premium, got %q", ns.Name)
}
if ns.UomId != altUomID {
t.Fatalf("expected uom_id %d, got %d", altUomID, ns.UomId)
}
if len(ns.Suppliers) != 1 || ns.Suppliers[0].Id != supplierID3 {
t.Fatalf("expected suppliers to contain only %d", supplierID3)
}
if len(ns.Flags) != len(nonstockFlags) {
t.Fatalf("expected flags retained, got %d", len(ns.Flags))
}
})
t.Run("clear suppliers succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
"supplier_ids": []uint{},
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when clearing suppliers, got %d: %s", resp.StatusCode, string(body))
}
ns := fetchNonstock(t, db, nonstockID)
if len(ns.Suppliers) != 0 {
t.Fatalf("expected suppliers to be cleared, got %d entries", len(ns.Suppliers))
}
if len(ns.Flags) != len(nonstockFlags) {
t.Fatalf("expected flags unaffected, got %d", len(ns.Flags))
}
})
t.Run("clear nonstock flags succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
"flags": []string{},
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when clearing nonstock flags, got %d: %s", resp.StatusCode, string(body))
}
ns := fetchNonstock(t, db, nonstockID)
if len(ns.Flags) != 0 {
t.Fatalf("expected flags cleared, got %d", len(ns.Flags))
}
})
t.Run("delete nonstock succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting nonstock, got %d: %s", resp.StatusCode, string(body))
}
var ns entities.Nonstock
if err := db.First(&ns, nonstockID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("expected nonstock to be deleted, got error %v", err)
}
var links int64
if err := db.Model(&entities.NonstockSupplier{}).Where("nonstock_id = ?", nonstockID).Count(&links).Error; err != nil {
t.Fatalf("failed counting nonstock suppliers: %v", err)
}
if links != 0 {
t.Fatalf("expected link table cleared, found %d rows", links)
}
var flagCount int64
if err := db.Model(&entities.Flag{}).
Where("flagable_id = ? AND flagable_type = ?", nonstockID, entities.FlagableTypeNonstock).
Count(&flagCount).Error; err != nil {
t.Fatalf("failed counting nonstock flags: %v", err)
}
if flagCount != 0 {
t.Fatalf("expected flags removed, found %d", flagCount)
}
})
t.Run("deleting nonstock twice returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when deleting missing nonstock, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("fetching deleted nonstock returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when fetching deleted nonstock, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("cleanup additional supplier references", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID3), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting supplier, got %d: %s", resp.StatusCode, string(body))
}
doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", sapronakSupplierID), nil)
})
}
@@ -0,0 +1,150 @@
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"
)
func TestProductCategoryIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
t.Run("create product category missing code fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/product-categories", map[string]any{
"name": "Layer Feed",
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when code missing, got %d: %s", resp.StatusCode, string(body))
}
})
var categoryID uint
t.Run("create product category succeeds", func(t *testing.T) {
categoryID = createProductCategory(t, app, "Layer Feed", "LFD")
pc := fetchProductCategory(t, db, categoryID)
if pc.Name != "Layer Feed" {
t.Fatalf("expected name Layer Feed, got %q", pc.Name)
}
if pc.Code != "LFD" {
t.Fatalf("expected code LFD, got %q", pc.Code)
}
if pc.CreatedBy != 1 {
t.Fatalf("expected created_by 1, got %d", pc.CreatedBy)
}
})
t.Run("creating duplicate name fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/product-categories", map[string]any{
"name": "Layer Feed",
"code": "LF2",
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when creating duplicate name, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("creating duplicate code fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/product-categories", map[string]any{
"name": "Layer Feed Premium",
"code": "LFD",
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when creating duplicate code, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("get product category detail", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching product category, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Id uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if payload.Data.Id != categoryID {
t.Fatalf("expected id %d, got %d", categoryID, payload.Data.Id)
}
if payload.Data.Code != "LFD" {
t.Fatalf("expected code LFD, got %q", payload.Data.Code)
}
})
t.Run("update product category with duplicate name fails", func(t *testing.T) {
otherID := createProductCategory(t, app, "Layer Feed Alt", "LFA")
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/product-categories/%d", otherID), map[string]any{
"name": "Layer Feed",
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when updating with duplicate name, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("update product category succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), map[string]any{
"name": "Layer Feed Updated",
"code": "LFU",
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating product category, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Name string `json:"name"`
Code string `json:"code"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse update response: %v", err)
}
if payload.Data.Name != "Layer Feed Updated" {
t.Fatalf("expected updated name, got %q", payload.Data.Name)
}
if payload.Data.Code != "LFU" {
t.Fatalf("expected code LFU, got %q", payload.Data.Code)
}
pc := fetchProductCategory(t, db, categoryID)
if pc.Code != "LFU" {
t.Fatalf("expected persisted code LFU, got %q", pc.Code)
}
})
t.Run("delete product category", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting product category, got %d: %s", resp.StatusCode, string(body))
}
var pc entities.ProductCategory
if err := db.First(&pc, categoryID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("expected product category deleted, got error %v", err)
}
})
t.Run("delete non existing product category returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when deleting missing product category, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("get deleted product category returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when fetching deleted product category, got %d: %s", resp.StatusCode, string(body))
}
})
}
@@ -0,0 +1,410 @@
package test
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"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 TestProductIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
uomID := createUom(t, app, "Kilogram")
categoryID := createProductCategory(t, app, "Feed", "FED")
sapSupplier1 := createSupplier(t, app, "Feed Supplier One", "fs1", string(utils.SupplierCategorySapronak))
sapSupplier2 := createSupplier(t, app, "Feed Supplier Two", "fs2", string(utils.SupplierCategorySapronak))
bopSupplier := createSupplier(t, app, "BOP Supplier", "bop1", string(utils.SupplierCategoryBOP))
productFlags := []string{string(utils.FlagDOC), string(utils.FlagPakan)}
updatedProductFlags := []string{string(utils.FlagFinisher), string(utils.FlagObat)}
t.Run("create product without suppliers succeeds with empty relations", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{
"name": "Supplierless Product",
"brand": "Brand A",
"uom_id": uomID,
"product_category_id": categoryID,
"product_price": 12000,
})
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when suppliers omitted, got %d: %s", resp.StatusCode, string(body))
}
id := parseID(t, body)
product := fetchProduct(t, db, id)
if len(product.Suppliers) != 0 {
t.Fatalf("expected no suppliers persisted, found %d", len(product.Suppliers))
}
if len(product.Flags) != 0 {
t.Fatalf("expected no flags persisted, found %d", len(product.Flags))
}
resp, _ = doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/products/%d", id), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected cleanup delete to succeed, got %d", resp.StatusCode)
}
})
t.Run("create product with invalid uom fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{
"name": "Layer Feed Invalid UOM",
"brand": "Brand A",
"uom_id": 9999,
"product_category_id": categoryID,
"product_price": 12000,
"supplier_ids": []uint{sapSupplier1},
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when uom missing, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("create product with invalid category fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{
"name": "Layer Feed Invalid Category",
"brand": "Brand A",
"uom_id": uomID,
"product_category_id": 9999,
"product_price": 12000,
"supplier_ids": []uint{sapSupplier1},
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when category missing, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("create product with invalid supplier fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{
"name": "Layer Feed Missing Supplier",
"brand": "Brand A",
"uom_id": uomID,
"product_category_id": categoryID,
"product_price": 12000,
"supplier_ids": []uint{99999},
})
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when supplier missing, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("create product with BOP supplier fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{
"name": "Layer Feed Wrong Supplier",
"brand": "Brand A",
"uom_id": uomID,
"product_category_id": categoryID,
"product_price": 12000,
"supplier_ids": []uint{bopSupplier},
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when supplier category invalid, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("create product with invalid flags fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{
"name": "Layer Feed Invalid Flags",
"brand": "Brand A",
"uom_id": uomID,
"product_category_id": categoryID,
"product_price": 12000,
"supplier_ids": []uint{sapSupplier1},
"flags": []string{"INVALID"},
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when flags invalid, got %d: %s", resp.StatusCode, string(body))
}
})
var productID uint
t.Run("create product succeeds", func(t *testing.T) {
sku := "lfd-001"
productID = createProduct(t, app, "Layer Feed", "Brand A", &sku, uomID, categoryID, 15000, []uint{sapSupplier1, sapSupplier2}, productFlags)
product := fetchProduct(t, db, productID)
if product.Name != "Layer Feed" {
t.Fatalf("expected name Layer Feed, got %q", product.Name)
}
if product.Brand != "Brand A" {
t.Fatalf("expected brand Brand A, got %q", product.Brand)
}
if product.Sku == nil || *product.Sku != strings.ToUpper(sku) {
t.Fatalf("expected sku %q, got %+v", strings.ToUpper(sku), product.Sku)
}
if product.UomId != uomID {
t.Fatalf("expected uom_id %d, got %d", uomID, product.UomId)
}
if product.ProductCategoryId != categoryID {
t.Fatalf("expected product_category_id %d, got %d", categoryID, product.ProductCategoryId)
}
if product.CreatedBy != 1 {
t.Fatalf("expected created_by 1, got %d", product.CreatedBy)
}
if len(product.Suppliers) != 2 {
t.Fatalf("expected 2 suppliers, got %d", len(product.Suppliers))
}
if len(product.Flags) != len(productFlags) {
t.Fatalf("expected %d flags, got %d", len(productFlags), len(product.Flags))
}
expectedFlags := make(map[string]struct{}, len(productFlags))
for _, flag := range productFlags {
expectedFlags[strings.ToUpper(flag)] = struct{}{}
}
for _, flag := range product.Flags {
if _, ok := expectedFlags[strings.ToUpper(flag.Name)]; !ok {
t.Fatalf("unexpected flag %s present", flag.Name)
}
delete(expectedFlags, strings.ToUpper(flag.Name))
}
if len(expectedFlags) != 0 {
t.Fatalf("missing expected flags: %v", expectedFlags)
}
})
t.Run("creating duplicate name fails", func(t *testing.T) {
sku := "lfd-002"
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{
"name": "Layer Feed",
"brand": "Brand B",
"sku": sku,
"uom_id": uomID,
"product_category_id": categoryID,
"product_price": 16000,
"supplier_ids": []uint{sapSupplier1},
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when creating duplicate name, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("creating duplicate sku fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{
"name": "Layer Feed Premium",
"brand": "Brand B",
"sku": "LFD-001",
"uom_id": uomID,
"product_category_id": categoryID,
"product_price": 17000,
"supplier_ids": []uint{sapSupplier1},
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when creating duplicate sku, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("get product detail returns nested data", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/products/%d", productID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching product, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Id uint `json:"id"`
Name string `json:"name"`
Brand string `json:"brand"`
Uom *struct {
Id uint `json:"id"`
} `json:"uom"`
Suppliers []map[string]any `json:"suppliers"`
Flags []string `json:"flags"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse product detail: %v", err)
}
if payload.Data.Id != productID {
t.Fatalf("expected id %d, got %d", productID, payload.Data.Id)
}
if payload.Data.Uom == nil || payload.Data.Uom.Id != uomID {
t.Fatalf("expected uom id %d, got %+v", uomID, payload.Data.Uom)
}
if len(payload.Data.Suppliers) != 2 {
t.Fatalf("expected 2 suppliers in response, got %d", len(payload.Data.Suppliers))
}
if len(payload.Data.Flags) != len(productFlags) {
t.Fatalf("expected %d flags in response, got %d", len(productFlags), len(payload.Data.Flags))
}
expected := make(map[string]struct{}, len(productFlags))
for _, flag := range productFlags {
expected[strings.ToUpper(flag)] = struct{}{}
}
for _, flag := range payload.Data.Flags {
flag = strings.ToUpper(flag)
if _, ok := expected[flag]; !ok {
t.Fatalf("unexpected flag %s returned", flag)
}
delete(expected, flag)
}
if len(expected) != 0 {
t.Fatalf("missing expected flags in response: %v", expected)
}
})
t.Run("update product with invalid supplier category fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{
"supplier_ids": []uint{bopSupplier},
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating with non SAPRONAK supplier, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("update product with invalid flags fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{
"flags": []string{"UNKNOWN"},
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating with invalid flags, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("update product succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{
"name": "Layer Feed Updated",
"brand": "Brand C",
"sku": "lfd-100",
"product_price": 18000,
"selling_price": 19000,
"tax": 1000,
"expiry_period": 30,
"supplier_ids": []uint{sapSupplier1},
"flags": updatedProductFlags,
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating product, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Name string `json:"name"`
Brand string `json:"brand"`
Sku *string `json:"sku"`
ProductPrice float64 `json:"product_price"`
SupplierIds []map[string]any `json:"suppliers"`
Flags []string `json:"flags"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse update response: %v", err)
}
if payload.Data.Name != "Layer Feed Updated" {
t.Fatalf("expected updated name, got %q", payload.Data.Name)
}
if len(payload.Data.Flags) != len(updatedProductFlags) {
t.Fatalf("expected %d flags in response, got %d", len(updatedProductFlags), len(payload.Data.Flags))
}
respFlags := make(map[string]struct{}, len(payload.Data.Flags))
for _, flag := range payload.Data.Flags {
respFlags[strings.ToUpper(flag)] = struct{}{}
}
for _, flag := range updatedProductFlags {
if _, ok := respFlags[strings.ToUpper(flag)]; !ok {
t.Fatalf("missing flag %s in response", flag)
}
}
product := fetchProduct(t, db, productID)
if product.Brand != "Brand C" {
t.Fatalf("expected persisted brand Brand C, got %q", product.Brand)
}
if product.Sku == nil || *product.Sku != "LFD-100" {
t.Fatalf("expected persisted sku LFD-100, got %+v", product.Sku)
}
if len(product.Suppliers) != 1 || product.Suppliers[0].Id != sapSupplier1 {
t.Fatalf("expected supplier to be %d", sapSupplier1)
}
if len(product.Flags) != len(updatedProductFlags) {
t.Fatalf("expected %d flags after update, got %d", len(updatedProductFlags), len(product.Flags))
}
expectedFlags := make(map[string]struct{}, len(updatedProductFlags))
for _, flag := range updatedProductFlags {
expectedFlags[strings.ToUpper(flag)] = struct{}{}
}
for _, flag := range product.Flags {
upper := strings.ToUpper(flag.Name)
if _, ok := expectedFlags[upper]; !ok {
t.Fatalf("unexpected flag after update: %s", upper)
}
delete(expectedFlags, upper)
}
if len(expectedFlags) != 0 {
t.Fatalf("missing flags after update: %v", expectedFlags)
}
})
t.Run("clear suppliers succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{
"supplier_ids": []uint{},
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when clearing suppliers, got %d: %s", resp.StatusCode, string(body))
}
product := fetchProduct(t, db, productID)
if len(product.Suppliers) != 0 {
t.Fatalf("expected no suppliers after clearing, got %d", len(product.Suppliers))
}
if len(product.Flags) != len(updatedProductFlags) {
t.Fatalf("expected flags untouched after clearing suppliers, got %d", len(product.Flags))
}
})
t.Run("clear flags succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{
"flags": []string{},
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when clearing flags, got %d: %s", resp.StatusCode, string(body))
}
product := fetchProduct(t, db, productID)
if len(product.Flags) != 0 {
t.Fatalf("expected all flags cleared, got %d", len(product.Flags))
}
})
t.Run("delete product succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/products/%d", productID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting product, got %d: %s", resp.StatusCode, string(body))
}
var product entities.Product
if err := db.First(&product, productID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("expected product deleted, got error %v", err)
}
var links int64
if err := db.Model(&entities.ProductSupplier{}).Where("product_id = ?", productID).Count(&links).Error; err != nil {
t.Fatalf("failed counting product suppliers: %v", err)
}
if links != 0 {
t.Fatalf("expected pivot cleared, found %d rows", links)
}
var flagCount int64
if err := db.Model(&entities.Flag{}).
Where("flagable_id = ? AND flagable_type = ?", productID, entities.FlagableTypeProduct).
Count(&flagCount).Error; err != nil {
t.Fatalf("failed counting product flags: %v", err)
}
if flagCount != 0 {
t.Fatalf("expected flags removed, found %d", flagCount)
}
})
t.Run("delete missing product returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/products/%d", productID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when deleting missing product, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("get deleted product returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/products/%d", productID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when fetching deleted product, got %d: %s", resp.StatusCode, string(body))
}
})
}
@@ -0,0 +1,238 @@
package test
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"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 TestSupplierIntegration(t *testing.T) {
app, db := setupIntegrationApp(t)
t.Run("creating supplier with invalid type fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/suppliers", map[string]any{
"name": "Invalid Supplier",
"alias": "inv01",
"pic": "Jane Doe",
"type": "random-type",
"category": utils.SupplierCategoryBOP,
"hatchery": "Hatchery X",
"phone": "081234567891",
"email": "invalid@supplier.com",
"address": "Somewhere",
"npwp": "NPWP-INVALID",
"account_number": "ACC-INVALID",
"due_date": 30,
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when type is invalid, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("creating supplier with invalid category fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/suppliers", map[string]any{
"name": "Invalid Category Supplier",
"alias": "cat01",
"pic": "Jane Doe",
"type": utils.CustomerSupplierTypeBisnis,
"category": "invalid",
"phone": "081234567892",
"email": "invalid-category@supplier.com",
"address": "Somewhere",
"due_date": 30,
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when category is invalid, got %d: %s", resp.StatusCode, string(body))
}
})
const supplierName = "Supplier Alpha"
const alias = "al001"
var supplierID uint
t.Run("creating supplier succeeds", func(t *testing.T) {
supplierID = createSupplier(t, app, supplierName, alias, string(utils.SupplierCategoryBOP))
supplier := fetchSupplier(t, db, supplierID)
if supplier.Name != supplierName {
t.Fatalf("expected name %q, got %q", supplierName, supplier.Name)
}
if supplier.Alias != strings.ToUpper(alias) {
t.Fatalf("expected alias %q, got %q", strings.ToUpper(alias), supplier.Alias)
}
if supplier.Type != string(utils.CustomerSupplierTypeBisnis) {
t.Fatalf("expected type %q, got %q", utils.CustomerSupplierTypeBisnis, supplier.Type)
}
if supplier.Category != string(utils.SupplierCategoryBOP) {
t.Fatalf("expected category %q, got %q", utils.SupplierCategoryBOP, supplier.Category)
}
if supplier.DueDate != 30 {
t.Fatalf("expected due date 30, got %d", supplier.DueDate)
}
if supplier.CreatedUser.Id != 1 {
t.Fatalf("expected created user id 1, got %d", supplier.CreatedUser.Id)
}
})
t.Run("creating supplier with duplicate name fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/suppliers", map[string]any{
"name": supplierName,
"alias": "dup01",
"pic": "Jane Doe",
"type": utils.CustomerSupplierTypeBisnis,
"category": utils.SupplierCategoryBOP,
"phone": "0811111111",
"email": "duplicate@supplier.com",
"address": "Duplicate address",
"due_date": 15,
})
if resp.StatusCode != fiber.StatusConflict {
t.Fatalf("expected 409 when creating duplicate supplier, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("getting existing supplier succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching supplier, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Type string `json:"type"`
Category string `json:"category"`
CreatedUser *struct {
Id uint `json:"id"`
} `json:"created_user"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse supplier response: %v", err)
}
if payload.Data.Id != supplierID {
t.Fatalf("expected id %d, got %d", supplierID, payload.Data.Id)
}
if payload.Data.Name != supplierName {
t.Fatalf("expected name %q, got %q", supplierName, payload.Data.Name)
}
if payload.Data.Alias != strings.ToUpper(alias) {
t.Fatalf("expected alias %q, got %q", strings.ToUpper(alias), payload.Data.Alias)
}
if payload.Data.Type != string(utils.CustomerSupplierTypeBisnis) {
t.Fatalf("expected type %q, got %q", utils.CustomerSupplierTypeBisnis, payload.Data.Type)
}
if payload.Data.Category != string(utils.SupplierCategoryBOP) {
t.Fatalf("expected category %q, got %q", utils.SupplierCategoryBOP, payload.Data.Category)
}
if payload.Data.CreatedUser == nil || payload.Data.CreatedUser.Id != 1 {
t.Fatalf("expected created_user id 1, got %+v", payload.Data.CreatedUser)
}
})
const updatedAlias = "beta1"
t.Run("updating supplier fields succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), map[string]any{
"alias": updatedAlias,
"type": utils.CustomerSupplierTypeIndividual,
"category": utils.SupplierCategorySapronak,
"due_date": 45,
})
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating supplier, got %d: %s", resp.StatusCode, string(body))
}
var payload struct {
Data struct {
Alias string `json:"alias"`
Type string `json:"type"`
Category string `json:"category"`
} `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse update response: %v", err)
}
if payload.Data.Alias != strings.ToUpper(updatedAlias) {
t.Fatalf("expected alias %q, got %q", strings.ToUpper(updatedAlias), payload.Data.Alias)
}
if payload.Data.Type != string(utils.CustomerSupplierTypeIndividual) {
t.Fatalf("expected type %q, got %q", utils.CustomerSupplierTypeIndividual, payload.Data.Type)
}
if payload.Data.Category != string(utils.SupplierCategorySapronak) {
t.Fatalf("expected category %q, got %q", utils.SupplierCategorySapronak, payload.Data.Category)
}
supplier := fetchSupplier(t, db, supplierID)
if supplier.Alias != strings.ToUpper(updatedAlias) {
t.Fatalf("expected persisted alias %q, got %q", strings.ToUpper(updatedAlias), supplier.Alias)
}
if supplier.Type != string(utils.CustomerSupplierTypeIndividual) {
t.Fatalf("expected persisted type %q, got %q", utils.CustomerSupplierTypeIndividual, supplier.Type)
}
if supplier.Category != string(utils.SupplierCategorySapronak) {
t.Fatalf("expected persisted category %q, got %q", utils.SupplierCategorySapronak, supplier.Category)
}
if supplier.DueDate != 45 {
t.Fatalf("expected due date 45, got %d", supplier.DueDate)
}
})
t.Run("updating supplier with invalid type fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), map[string]any{
"type": "invalid-type",
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating with invalid type, got %d: %s", resp.StatusCode, string(body))
}
supplier := fetchSupplier(t, db, supplierID)
if supplier.Type != string(utils.CustomerSupplierTypeIndividual) {
t.Fatalf("expected type to remain %q after invalid update, got %q", utils.CustomerSupplierTypeIndividual, supplier.Type)
}
})
t.Run("updating supplier with invalid category fails", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), map[string]any{
"category": "invalid",
})
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("expected 400 when updating with invalid category, got %d: %s", resp.StatusCode, string(body))
}
supplier := fetchSupplier(t, db, supplierID)
if supplier.Category != string(utils.SupplierCategorySapronak) {
t.Fatalf("expected category to remain %q after invalid update, got %q", utils.SupplierCategorySapronak, supplier.Category)
}
})
t.Run("deleting supplier succeeds", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting supplier, got %d: %s", resp.StatusCode, string(body))
}
var supplier entities.Supplier
if err := db.First(&supplier, supplierID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("expected supplier to be deleted, got error %v", err)
}
})
t.Run("deleting non existent supplier returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when deleting missing supplier, got %d: %s", resp.StatusCode, string(body))
}
})
t.Run("getting deleted supplier returns 404", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), nil)
if resp.StatusCode != fiber.StatusNotFound {
t.Fatalf("expected 404 when fetching deleted supplier, got %d: %s", resp.StatusCode, string(body))
}
})
}
+8 -7
View File
@@ -29,6 +29,7 @@ func main() {
feat := os.Args[1] feat := os.Args[1]
parts := strings.Split(feat, "/") parts := strings.Split(feat, "/")
entity := parts[len(parts)-1] entity := parts[len(parts)-1]
pluralEntityKebab := toPlural(toKebab(entity))
d := Data{ d := Data{
FeatName: feat, FeatName: feat,
@@ -52,43 +53,43 @@ func main() {
}, },
{ {
TplPath: "tools/templates/validation.tmpl", TplPath: "tools/templates/validation.tmpl",
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "validations"), OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "validations"),
OutSuffix: ".validation.go", OutSuffix: ".validation.go",
TplName: "validation", TplName: "validation",
}, },
{ {
TplPath: "tools/templates/service.tmpl", TplPath: "tools/templates/service.tmpl",
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "services"), OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "services"),
OutSuffix: ".service.go", OutSuffix: ".service.go",
TplName: "service", TplName: "service",
}, },
{ {
TplPath: "tools/templates/controller.tmpl", TplPath: "tools/templates/controller.tmpl",
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "controllers"), OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "controllers"),
OutSuffix: ".controller.go", OutSuffix: ".controller.go",
TplName: "controller", TplName: "controller",
}, },
{ {
TplPath: "tools/templates/repository.tmpl", TplPath: "tools/templates/repository.tmpl",
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "repositories"), OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "repositories"),
OutSuffix: ".repository.go", OutSuffix: ".repository.go",
TplName: "repository", TplName: "repository",
}, },
{ {
TplPath: "tools/templates/dto.tmpl", TplPath: "tools/templates/dto.tmpl",
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "dto"), OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "dto"),
OutSuffix: ".dto.go", OutSuffix: ".dto.go",
TplName: "dto", TplName: "dto",
}, },
{ {
TplPath: "tools/templates/route.tmpl", TplPath: "tools/templates/route.tmpl",
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s"), OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab),
OutSuffix: "", OutSuffix: "",
TplName: "route", TplName: "route",
}, },
{ {
TplPath: "tools/templates/module.tmpl", TplPath: "tools/templates/module.tmpl",
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s"), OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab),
OutSuffix: "", OutSuffix: "",
TplName: "module", TplName: "module",
}, },
+6
View File
@@ -56,4 +56,10 @@ func To{{Pascal .Entity}}ListDTOs(e []entity.{{Pascal .Entity}}) []{{Pascal .Ent
} }
return result return result
} }
func To{{Pascal .Entity}}DetailDTO(e entity.{{Pascal .Entity}}) {{Pascal .Entity}}DetailDTO {
return {{Pascal .Entity}}DetailDTO{
{{Pascal .Entity}}ListDTO: To{{Pascal .Entity}}ListDTO(e),
}
}
{{end}} {{end}}

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