feat(BE-47,48,49,50): implement inventory adjustment system

- Extend DB schema with product_warehouses and stock_logs tables
- Implement stock adjustment API (increase/decrease operations)
- Add comprehensive validation for all adjustment operations
- Implement audit log system for each adjustment with history tracking
- Include transaction handling, DTOs, seeders, and proper error handling
- Add adjustment history API with pagination and filtering

TODO: Integration testing pending
This commit is contained in:
aguhh18
2025-10-10 09:24:17 +07:00
parent a0bdc7b23c
commit 91b320d489
23 changed files with 719 additions and 502 deletions
@@ -1,4 +1,9 @@
DROP TABLE IF EXISTS stock_logs;
DROP INDEX IF EXISTS idx_product_warehouses_unique;
DROP INDEX IF EXISTS idx_product_warehouses_deleted_at;
DROP INDEX IF EXISTS idx_product_warehouses_warehouse_id;
DROP INDEX IF EXISTS idx_product_warehouses_product_id;
DROP TABLE IF EXISTS product_warehouses;
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 TABLE IF EXISTS product_suppliers;
@@ -35,10 +40,4 @@ DROP TABLE IF EXISTS fcrs;
DROP TABLE IF EXISTS projects; DROP TABLE IF EXISTS projects;
DROP INDEX IF EXISTS users_id_user_unique; DROP INDEX IF EXISTS users_id_user_unique;
DROP INDEX IF EXISTS users_email_unique; DROP INDEX IF EXISTS users_email_unique;
DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS users;
DROP INDEX IF EXISTS idx_product_warehouses_unique;
DROP INDEX IF EXISTS idx_product_warehouses_deleted_at;
DROP INDEX IF EXISTS idx_product_warehouses_warehouse_id;
DROP INDEX IF EXISTS idx_product_warehouses_product_id;
DROP TABLE IF EXISTS product_warehouses;
DROP TABLE IF EXISTS stock_logs;
@@ -1,276 +1,337 @@
-- USERS -- USERS
CREATE TABLE users ( CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
id_user BIGINT NOT NULL, id_user BIGINT NOT NULL,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
email VARCHAR NOT NULL, email VARCHAR 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
); );
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX users_id_user_unique ON users (id_user)
CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL; WHERE
deleted_at IS NULL;
CREATE UNIQUE INDEX users_email_unique ON users (email)
WHERE
deleted_at IS NULL;
-- FLAGS -- FLAGS
CREATE TABLE flags ( CREATE TABLE flags (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
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()
);
CREATE UNIQUE INDEX flags_unique_flagable ON flags (
name,
flagable_id,
flagable_type
); );
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id); 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(10) NOT NULL, code VARCHAR(10) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name)
WHERE
deleted_at IS NULL;
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code)
WHERE
deleted_at IS NULL;
-- UOM -- UOM
CREATE TABLE uoms ( CREATE TABLE uoms (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name)
WHERE
deleted_at IS NULL;
-- BANKS -- BANKS
CREATE TABLE banks ( CREATE TABLE banks (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
alias VARCHAR(5) NOT NULL, alias VARCHAR(5) NOT NULL,
owner VARCHAR, owner VARCHAR,
account_number VARCHAR(50) NOT NULL, account_number 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, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX banks_name_unique ON banks (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX banks_name_unique ON banks (name)
WHERE
deleted_at IS NULL;
-- AREAS -- AREAS
CREATE TABLE areas ( CREATE TABLE areas (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX areas_name_unique ON areas (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX areas_name_unique ON areas (name)
WHERE
deleted_at IS NULL;
-- LOCATIONS -- LOCATIONS
CREATE TABLE locations ( CREATE TABLE locations (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE, area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX locations_name_unique ON locations (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX locations_name_unique ON locations (name)
WHERE
deleted_at IS NULL;
-- KANDANG -- KANDANG
CREATE TABLE kandangs ( CREATE TABLE kandangs (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE, location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name)
WHERE
deleted_at IS NULL;
-- WAREHOUSES -- WAREHOUSES
CREATE TABLE warehouses ( CREATE TABLE warehouses (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE, area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
location_id BIGINT REFERENCES locations(id) ON DELETE SET NULL ON UPDATE CASCADE, location_id BIGINT REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE,
kandang_id BIGINT REFERENCES kandangs(id) ON DELETE SET NULL ON UPDATE CASCADE, kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name)
WHERE
deleted_at IS NULL;
-- CUSTOMERS -- CUSTOMERS
CREATE TABLE customers ( CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
phone VARCHAR(20) NOT NULL, phone VARCHAR(20) NOT NULL,
email VARCHAR NOT NULL, email VARCHAR NOT NULL,
account_number VARCHAR(50) NOT NULL, account_number VARCHAR(50) NOT NULL,
balance NUMERIC(15,3) DEFAULT 0, balance NUMERIC(15, 3) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX customers_name_unique ON customers (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX customers_name_unique ON customers (name)
WHERE
deleted_at IS NULL;
-- NONSTOCK -- NONSTOCK
CREATE TABLE nonstocks ( CREATE TABLE nonstocks (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE, uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name)
WHERE
deleted_at IS NULL;
-- FCR -- FCR
CREATE TABLE fcrs ( CREATE TABLE fcrs (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name)
WHERE
deleted_at IS NULL;
CREATE TABLE fcr_standards ( CREATE TABLE fcr_standards (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
fcr_id BIGINT NOT NULL REFERENCES fcrs(id) ON DELETE CASCADE ON UPDATE CASCADE, fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE CASCADE ON UPDATE CASCADE,
weight NUMERIC(15,3) NOT NULL, weight NUMERIC(15, 3) NOT NULL,
fcr_number NUMERIC(15,3) NOT NULL, fcr_number NUMERIC(15, 3) NOT NULL,
mortality NUMERIC(15,3) NOT NULL, mortality NUMERIC(15, 3) 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
); );
-- SUPPLIERS -- SUPPLIERS
CREATE TABLE suppliers ( CREATE TABLE suppliers (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
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, 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,
address TEXT NOT NULL, address TEXT NOT NULL,
npwp VARCHAR(50), npwp VARCHAR(50),
account_number VARCHAR(50), account_number VARCHAR(50),
balance NUMERIC(15,3) DEFAULT 0, balance NUMERIC(15, 3) DEFAULT 0,
due_date INT NOT NULL, due_date INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name)
WHERE
deleted_at IS NULL;
CREATE TABLE nonstock_suppliers ( CREATE TABLE nonstock_suppliers (
nonstock_id BIGINT NOT NULL REFERENCES nonstocks(id) ON DELETE CASCADE ON UPDATE CASCADE, 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, supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (nonstock_id, supplier_id) PRIMARY KEY (nonstock_id, supplier_id)
); );
-- PRODUCTS -- PRODUCTS
CREATE TABLE products ( CREATE TABLE products (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
brand VARCHAR NOT NULL, brand VARCHAR NOT NULL,
sku VARCHAR(100), sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE, 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_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_price NUMERIC(15,3) NOT NULL, product_price NUMERIC(15, 3) NOT NULL,
selling_price NUMERIC(15,3), selling_price NUMERIC(15, 3),
tax NUMERIC(15,3), tax NUMERIC(15, 3),
expiry_period INT, expiry_period INT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX products_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 ( CREATE TABLE product_suppliers (
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE ON UPDATE CASCADE, 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, supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (product_id, supplier_id) PRIMARY KEY (product_id, supplier_id)
); );
-- PROJECTS -- PROJECTS
CREATE TABLE projects ( CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
-- PRODUCT WAREHOUSES TABLE -- PRODUCT WAREHOUSES TABLE
CREATE TABLE product_warehouses ( CREATE TABLE product_warehouses (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id), product_id BIGINT NOT NULL REFERENCES products (id),
warehouse_id BIGINT NOT NULL REFERENCES warehouses(id), warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
quantity INTEGER NOT NULL DEFAULT 0, quantity INTEGER NOT NULL DEFAULT 0,
created_by BIGINT NOT NULL REFERENCES users(id), created_by BIGINT NOT NULL REFERENCES users (id),
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
); );
-- INDEXES -- INDEXES
CREATE INDEX idx_product_warehouses_product_id ON product_warehouses(product_id); CREATE INDEX idx_product_warehouses_product_id ON product_warehouses (product_id);
CREATE INDEX idx_product_warehouses_warehouse_id ON product_warehouses(warehouse_id);
CREATE INDEX idx_product_warehouses_deleted_at ON product_warehouses(deleted_at); CREATE INDEX idx_product_warehouses_warehouse_id ON product_warehouses (warehouse_id);
CREATE UNIQUE INDEX idx_product_warehouses_unique ON product_warehouses(product_id, warehouse_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_product_warehouses_deleted_at ON product_warehouses (deleted_at);
CREATE UNIQUE INDEX idx_product_warehouses_unique ON product_warehouses (product_id, warehouse_id)
WHERE
deleted_at IS NULL;
-- STOCK LOGS -- STOCK LOGS
CREATE TABLE stock_logs ( CREATE TABLE stock_logs (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
trancaction_type VARCHAR(20) NOT NULL, transaction_type VARCHAR(20) NOT NULL,
quantity NUMERIC(15,3) NOT NULL, quantity NUMERIC(15, 3) NOT NULL,
before_quantity NUMERIC(15,3) NOT NULL, before_quantity NUMERIC(15, 3) NOT NULL,
after_quantity NUMERIC(15,3) NOT NULL, after_quantity NUMERIC(15, 3) NOT NULL,
log_type VARCHAR(50) NOT NULL, log_type VARCHAR(50) NOT NULL,
log_id BIGINT NOT NULL, log_id BIGINT ,
note TEXT, note TEXT,
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses(id) ON DELETE CASCADE ON UPDATE CASCADE, product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_by BIGINT NOT NULL REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
-- Create indexes for better performance -- Create indexes for better performance
CREATE INDEX stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id); CREATE INDEX stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
CREATE INDEX stock_logs_log_type_log_id_idx ON stock_logs (log_type, log_id); CREATE INDEX stock_logs_log_type_log_id_idx ON stock_logs (log_type, log_id);
CREATE INDEX stock_logs_created_by_idx ON stock_logs (created_by); CREATE INDEX stock_logs_created_by_idx ON stock_logs (created_by);
CREATE INDEX stock_logs_created_at_idx ON stock_logs (created_at); CREATE INDEX stock_logs_created_at_idx ON stock_logs (created_at);
CREATE INDEX stock_logs_deleted_at_idx ON stock_logs (deleted_at); CREATE INDEX stock_logs_deleted_at_idx ON stock_logs (deleted_at);
+37 -4
View File
@@ -74,6 +74,10 @@ func Run(db *gorm.DB) error {
return err return err
} }
if err := seedProductWarehouse(tx, adminID); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed") fmt.Println("✅ Master data seeding completed")
return nil return nil
}) })
@@ -675,10 +679,6 @@ func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers
} }
// nanti saya isi // nanti saya isi
func seedProductWarehouse(tx *gorm.DB, createdBy uint, products map[string]uint, warehouses map[string]uint) error {
return nil
}
func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error { func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error {
if len(flags) == 0 { if len(flags) == 0 {
@@ -766,6 +766,39 @@ func seedBanks(tx *gorm.DB, createdBy uint) error {
return nil return nil
} }
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
ProductID uint
WarehouseID uint
Quantity float64
}{
{ProductID: 1, WarehouseID: 1, Quantity: 100},
{ProductID: 2, WarehouseID: 2, Quantity: 200},
{ProductID: 1, WarehouseID: 1, Quantity: 300},
}
for _, seed := range seeds {
var productWarehouse entity.ProductWarehouse
err := tx.Where("product_id = ? AND warehouse_id = ?", seed.ProductID, seed.WarehouseID).First(&productWarehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
productWarehouse = entity.ProductWarehouse{
ProductId: seed.ProductID,
WarehouseId: seed.WarehouseID,
Quantity: seed.Quantity,
CreatedBy: createdBy,
}
if err := tx.Create(&productWarehouse).Error; err != nil {
return err
}
} else if err != nil {
return err
}
}
return nil
}
func ptr[T any](v T) *T { func ptr[T any](v T) *T {
return &v return &v
} }
-18
View File
@@ -1,18 +0,0 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Adjustment struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+1 -1
View File
@@ -10,7 +10,7 @@ type ProductWarehouse struct {
Id uint `json:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" gorm:"primaryKey;autoIncrement"`
ProductId uint `json:"product_id" gorm:"not null"` ProductId uint `json:"product_id" gorm:"not null"`
WarehouseId uint `json:"warehouse_id" gorm:"not null"` WarehouseId uint `json:"warehouse_id" gorm:"not null"`
Quantity int `json:"quantity" gorm:"default:0"` Quantity float64 `json:"quantity" gorm:"default:0"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
CreatedBy uint `json:"created_by" gorm:"not null"` CreatedBy uint `json:"created_by" gorm:"not null"`
+22 -15
View File
@@ -6,23 +6,30 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
const (
LogTypeAdjustment = "ADJUSTMENT"
)
const (
TransactionTypeIncrease = "INCREASE"
TransactionTypeDecrease = "DECREASE"
)
type StockLog struct { type StockLog struct {
Id uint `json:"id" gorm:"primaryKey;"` Id uint `json:"id" gorm:"primaryKey;column:id"`
TransactionType string `json:"transaction_type" gorm:"type:varchar(20);not null"` TransactionType string `json:"transaction_type" gorm:"column:transaction_type;type:varchar(20);not null"`
Quantity float64 `json:"quantity" gorm:"type:numeric(15,3);not null"` Quantity float64 `json:"quantity" gorm:"column:quantity;type:numeric(15,3);not null"`
BeforeQuantity float64 `json:"before_quantity" gorm:"type:numeric(15,3);not null"` BeforeQuantity float64 `json:"before_quantity" gorm:"column:before_quantity;type:numeric(15,3);not null"`
AfterQuantity float64 `json:"after_quantity" gorm:"type:numeric(15,3);not null"` AfterQuantity float64 `json:"after_quantity" gorm:"column:after_quantity;type:numeric(15,3);not null"`
LogType string `json:"log_type" gorm:"type:varchar(50);not null"` LogType string `json:"log_type" gorm:"column:log_type;type:varchar(50);not null;index:stock_logs_flaggable_lookup,priority:1"`
LogId uint `json:"log_id" gorm:"not null"` LogId uint `json:"log_id" gorm:"column:log_id;not null;index:stock_logs_flaggable_lookup,priority:2"`
Note string `json:"note" gorm:"type:text"` Note string `json:"note" gorm:"column:note;type:text"`
ProductWarehouseId uint `json:"product_warehouse_id" gorm:"not null;index"` ProductWarehouseId uint `json:"product_warehouse_id" gorm:"column:product_warehouse_id;not null;index"`
CreatedBy uint `json:"created_by" gorm:"not null;index"` CreatedBy uint `json:"created_by" gorm:"column:created_by;not null;index"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` CreatedAt time.Time `json:"created_at" gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at;autoUpdateTime"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"column:deleted_at;index"`
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
} }
@@ -2,7 +2,6 @@ package controller
import ( import (
"math" "math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
@@ -22,119 +21,61 @@ func NewAdjustmentController(adjustmentService service.AdjustmentService) *Adjus
} }
} }
func (u *AdjustmentController) GetAll(c *fiber.Ctx) error { func (u *AdjustmentController) Adjustment(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.AdjustmentService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.AdjustmentListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all adjustments successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToAdjustmentListDTOs(result),
})
}
func (u *AdjustmentController) 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.AdjustmentService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get adjustment successfully",
Data: dto.ToAdjustmentListDTO(*result),
})
}
func (u *AdjustmentController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create) req := new(validation.Create)
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
result, err := u.AdjustmentService.CreateOne(c, req) stockLog, err := u.AdjustmentService.Adjustment(c, req)
if err != nil { if err != nil {
return err return err
} }
adjustmentDTO := dto.ToAdjustmentDetailDTO(stockLog)
return c.Status(fiber.StatusCreated). return c.Status(fiber.StatusCreated).
JSON(response.Success{ JSON(response.Success{
Code: fiber.StatusCreated, Code: fiber.StatusCreated,
Status: "success", Status: "success",
Message: "Create adjustment successfully", Message: "Create adjustment successfully",
Data: dto.ToAdjustmentListDTO(*result), Data: adjustmentDTO,
}) })
} }
func (u *AdjustmentController) UpdateOne(c *fiber.Ctx) error { func (u *AdjustmentController) AdjustmentHistory(c *fiber.Ctx) error {
req := new(validation.Update) query := &validation.Query{
param := c.Params("id") Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
id, err := strconv.Atoi(param) Search: c.Query("search", ""),
if err != nil { ProductID: c.QueryInt("product_id", 0),
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") WarehouseID: c.QueryInt("warehouse_id", 0),
TransactionType: c.Query("transaction_type", ""),
} }
if err := c.BodyParser(req); err != nil { result, totalResults, err := u.AdjustmentService.AdjustmentHistory(c, query)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.AdjustmentService.UpdateOne(c, req, uint(id))
if err != nil { if err != nil {
return err return err
} }
return c.Status(fiber.StatusOK). // Convert to DTOs
JSON(response.Success{ adjustmentDTOs := make([]dto.AdjustmentDetailDTO, len(result))
Code: fiber.StatusOK, for i, stockLog := range result {
Status: "success", adjustmentDTOs[i] = dto.ToAdjustmentDetailDTO(stockLog)
Message: "Update adjustment successfully",
Data: dto.ToAdjustmentListDTO(*result),
})
}
func (u *AdjustmentController) 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.AdjustmentService.DeleteOne(c, uint(id)); err != nil {
return err
} }
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.Common{ JSON(response.SuccessWithPaginate[dto.AdjustmentDetailDTO]{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Delete adjustment successfully", Message: "Get adjustment history successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: adjustmentDTOs,
}) })
} }
@@ -9,56 +9,123 @@ import (
// === DTO Structs === // === DTO Structs ===
type ProductBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
SKU string `json:"sku"`
}
type WarehouseBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductWarehouseDTO struct {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
WarehouseId uint `json:"warehouse_id"`
Quantity float64 `json:"quantity"`
Product *ProductBaseDTO `json:"product,omitempty"`
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
}
type AdjustmentBaseDTO struct { type AdjustmentBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` TransactionType string `json:"transaction_type"`
Quantity float64 `json:"quantity"`
BeforeQuantity float64 `json:"before_quantity"`
AfterQuantity float64 `json:"after_quantity"`
Note string `json:"note,omitempty"`
ProductWarehouseId uint `json:"product_warehouse_id"`
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
} }
type AdjustmentListDTO struct { type AdjustmentListDTO struct {
AdjustmentBaseDTO AdjustmentBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
type AdjustmentDetailDTO struct { type AdjustmentDetailDTO struct {
AdjustmentListDTO AdjustmentListDTO
UpdatedAt time.Time `json:"updated_at"`
} }
// === Mapper Functions === // === Mapper Functions ===
func ToAdjustmentBaseDTO(e entity.Adjustment) AdjustmentBaseDTO { func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO {
return AdjustmentBaseDTO{ if e == nil {
Id: e.Id, return nil
Name: e.Name, }
sku := ""
if e.Sku != nil {
sku = *e.Sku
}
return &ProductBaseDTO{
Id: e.Id,
Name: e.Name,
SKU: sku,
} }
} }
func ToAdjustmentListDTO(e entity.Adjustment) AdjustmentListDTO { func ToWarehouseBaseDTO(e *entity.Warehouse) *WarehouseBaseDTO {
if e == nil {
return nil
}
return &WarehouseBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
if e == nil {
return nil
}
return &ProductWarehouseDTO{
Id: e.Id,
ProductId: e.ProductId,
WarehouseId: e.WarehouseId,
Quantity: e.Quantity,
Product: ToProductBaseDTO(&e.Product),
Warehouse: ToWarehouseBaseDTO(&e.Warehouse),
}
}
func ToAdjustmentBaseDTO(e *entity.StockLog) AdjustmentBaseDTO {
return AdjustmentBaseDTO{
Id: e.Id,
TransactionType: e.TransactionType,
Quantity: e.Quantity,
BeforeQuantity: e.BeforeQuantity,
AfterQuantity: e.AfterQuantity,
Note: e.Note,
ProductWarehouseId: e.ProductWarehouseId,
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
}
}
func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
var createdUser *userDTO.UserBaseDTO var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 { if e.CreatedUser != nil {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser) createdUser = &userDTO.UserBaseDTO{
createdUser = &mapped Id: e.CreatedUser.Id,
IdUser: e.CreatedUser.IdUser,
Email: e.CreatedUser.Email,
Name: e.CreatedUser.Name,
}
} }
return AdjustmentListDTO{ return AdjustmentListDTO{
AdjustmentBaseDTO: ToAdjustmentBaseDTO(e), AdjustmentBaseDTO: ToAdjustmentBaseDTO(e),
CreatedAt: e.CreatedAt, CreatedUser: createdUser,
UpdatedAt: e.UpdatedAt, CreatedAt: e.CreatedAt,
CreatedUser: createdUser,
} }
} }
func ToAdjustmentListDTOs(e []entity.Adjustment) []AdjustmentListDTO { func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
result := make([]AdjustmentListDTO, len(e))
for i, r := range e {
result[i] = ToAdjustmentListDTO(r)
}
return result
}
func ToAdjustmentDetailDTO(e entity.Adjustment) AdjustmentDetailDTO {
return AdjustmentDetailDTO{ return AdjustmentDetailDTO{
AdjustmentListDTO: ToAdjustmentListDTO(e), AdjustmentListDTO: ToAdjustmentListDTO(e),
UpdatedAt: e.UpdatedAt,
} }
} }
@@ -5,8 +5,10 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
rAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories"
sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -15,12 +17,13 @@ import (
type AdjustmentModule struct{} type AdjustmentModule struct{}
func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
adjustmentRepo := rAdjustment.NewAdjustmentRepository(db) stockLogsRepo := rStockLogs.NewStockLogRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
adjustmentService := sAdjustment.NewAdjustmentService(adjustmentRepo, validate) adjustmentService := sAdjustment.NewAdjustmentService(stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
AdjustmentRoutes(router, userService, adjustmentService) AdjustmentRoutes(router, userService, adjustmentService)
} }
@@ -1,21 +0,0 @@
package repository
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type AdjustmentRepository interface {
repository.BaseRepository[entity.Adjustment]
}
type AdjustmentRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Adjustment]
}
func NewAdjustmentRepository(db *gorm.DB) AdjustmentRepository {
return &AdjustmentRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Adjustment](db),
}
}
@@ -14,15 +14,8 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme
route := v1.Group("/adjustments") route := v1.Group("/adjustments")
// route.Get("/", m.Auth(u), ctrl.GetAll) // Standard CRUD routes following master data pattern
// route.Post("/", m.Auth(u), ctrl.CreateOne) route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
// route.Get("/:id", m.Auth(u), ctrl.GetOne) route.Post("/", ctrl.Adjustment) // Create adjustment
// 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)
} }
@@ -2,128 +2,184 @@ package service
import ( import (
"errors" "errors"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm"
) )
type AdjustmentService interface { type AdjustmentService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Adjustment, int64, error) Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.StockLog, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Adjustment, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.StockLog, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Adjustment, error) AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Adjustment, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
} }
type adjustmentService struct { type adjustmentService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.AdjustmentRepository StockLogsRepository stockLogsRepo.StockLogRepository
WarehouseRepo warehouseRepo.WarehouseRepository
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
} }
func NewAdjustmentService(repo repository.AdjustmentRepository, validate *validator.Validate) AdjustmentService { func NewAdjustmentService(stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService {
return &adjustmentService{ return &adjustmentService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, StockLogsRepository: stockLogsRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
} }
} }
func (s adjustmentService) withRelations(db *gorm.DB) *gorm.DB { func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser") return db.
Preload("ProductWarehouse").
Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse").
Preload("CreatedUser")
} }
func (s adjustmentService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Adjustment, int64, error) { func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) {
if err := s.Validate.Struct(params); err != nil { stockLog, err := s.StockLogsRepository.GetByID(c.Context(), id, s.withRelations)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
}
s.Log.Errorf("Failed to get adjustment by id: %+v", err)
return nil, err
}
if stockLog.LogType != entity.LogTypeAdjustment {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
}
return stockLog, nil
}
func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*entity.StockLog, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
ctx := c.Context()
productWarehouseExists, err := s.ProductWarehouseRepo.ProductWarehouseExists(ctx, uint(req.ProductID), uint(req.WarehouseID), nil)
if err != nil {
return nil, err
}
if !productWarehouseExists {
return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse not found")
}
transactionType := strings.ToUpper(req.TransactionType)
if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
}
var createdLogId uint
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// Get product warehouse by product id and warehouse id (read operation, no transaction needed)
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil {
return err
}
if productWarehouse == nil {
return fiber.NewError(fiber.StatusBadRequest, "Product warehouse not found")
}
s.Log.Infof("Product Warehouse found: %+v", productWarehouse.Id)
afterQuantity := productWarehouse.Quantity
if transactionType == entity.TransactionTypeIncrease {
afterQuantity += req.Quantity
} else {
if productWarehouse.Quantity < req.Quantity {
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
}
afterQuantity -= req.Quantity
}
newLog := &entity.StockLog{
TransactionType: transactionType,
Quantity: req.Quantity,
BeforeQuantity: productWarehouse.Quantity,
AfterQuantity: afterQuantity,
LogType: entity.LogTypeAdjustment,
LogId: 0,
Note: req.Note,
ProductWarehouseId: productWarehouse.Id,
CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log: %+v", err)
return err
}
s.Log.Infof("Stock log created: %+v", newLog.Id)
productWarehouse.Quantity = afterQuantity
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return err
}
s.Log.Infof("Product warehouse quantity updated: %+v", productWarehouse.Id)
// Set createdLogId to get the log with relations after transaction
createdLogId = newLog.Id
return nil
})
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process adjustment transaction")
}
return s.GetOne(c, createdLogId)
}
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
if err := s.Validate.Struct(query); err != nil {
return nil, 0, err return nil, 0, err
} }
offset := (params.Page - 1) * params.Limit offset := (query.Page - 1) * query.Limit
stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB {
adjustments, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%") db = db.Where("log_type = ?", entity.LogTypeAdjustment)
if query.Search != "" {
db = db.Where("note ILIKE ?", "%"+query.Search+"%")
} }
return db.Order("created_at DESC").Order("updated_at DESC")
if query.TransactionType != "" {
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
}
return db.Order("created_at DESC")
}) })
if err != nil { if err != nil {
s.Log.Errorf("Failed to get adjustments: %+v", err) s.Log.Errorf("Failed to get adjustments: %+v", err)
return nil, 0, err return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history")
} }
return adjustments, total, nil
} // Convert to pointer slice
result := make([]*entity.StockLog, len(stockLogs))
func (s adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.Adjustment, error) { for i, v := range stockLogs {
adjustment, err := s.Repository.GetByID(c.Context(), id, s.withRelations) result[i] = &v
if errors.Is(err, gorm.ErrRecordNotFound) { }
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
} return result, total, nil
if err != nil {
s.Log.Errorf("Failed get adjustment by id: %+v", err)
return nil, err
}
return adjustment, nil
}
func (s *adjustmentService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Adjustment, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
createBody := &entity.Adjustment{
Name: req.Name,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create adjustment: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s adjustmentService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Adjustment, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
updateBody["name"] = *req.Name
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
}
s.Log.Errorf("Failed to update adjustment: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s adjustmentService) 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, "Adjustment not found")
}
s.Log.Errorf("Failed to delete adjustment: %+v", err)
return err
}
return nil
} }
@@ -1,15 +1,18 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3"` ProductID uint `json:"product_id" validate:"required"`
} WarehouseID uint `json:"warehouse_id" validate:"required"`
TransactionType string `json:"transaction_type" validate:"required,oneof=increase decrease"`
type Update struct { Quantity float64 `json:"quantity" validate:"required,gt=0"`
Name *string `json:"name,omitempty" validate:"omitempty"` Note string `json:"note" validate:"omitempty,max=255"`
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty"`
ProductID int `query:"product_id" validate:"omitempty,min=0"`
WarehouseID int `query:"warehouse_id" validate:"omitempty,min=0"`
TransactionType string `query:"transaction_type" validate:"omitempty,oneof=increase decrease"`
} }
@@ -26,7 +26,6 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
ProductId: uint(c.QueryInt("product_id", 0)), ProductId: uint(c.QueryInt("product_id", 0)),
WarehouseId: uint(c.QueryInt("warehouse_id", 0)), WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
} }
@@ -13,7 +13,7 @@ type ProductWarehouseBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
ProductId uint `json:"product_id"` ProductId uint `json:"product_id"`
WarehouseId uint `json:"warehouse_id"` WarehouseId uint `json:"warehouse_id"`
Quantity int `json:"quantity"` Quantity float64 `json:"quantity"`
} }
type ProductWarehouseListDTO struct { type ProductWarehouseListDTO struct {
@@ -14,6 +14,7 @@ type ProductWarehouseRepository interface {
IsProductExist(ctx context.Context, productId uint) (bool, error) IsProductExist(ctx context.Context, productId uint) (bool, error)
IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error)
ExistsByID(ctx context.Context, id uint) (bool, error) ExistsByID(ctx context.Context, id uint) (bool, error)
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
} }
type ProductWarehouseRepositoryImpl struct { type ProductWarehouseRepositoryImpl struct {
@@ -51,3 +52,11 @@ func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, w
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id) return repository.Exists[entity.ProductWarehouse](ctx, r.db, id)
} }
func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
if err := r.DB().WithContext(ctx).Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).First(&productWarehouse).Error; err != nil {
return nil, err
}
return &productWarehouse, nil
}
@@ -58,13 +58,6 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
db = db.Where("warehouse_id = ?", params.WarehouseId) db = db.Where("warehouse_id = ?", params.WarehouseId)
} }
// Search in related product or warehouse names
if params.Search != "" {
db = db.Joins("LEFT JOIN products ON products.id = product_warehouse.product_id").
Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouse.warehouse_id").
Where("products.name ILIKE ? OR warehouses.name ILIKE ?", "%"+params.Search+"%", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -1,21 +1,20 @@
package validation package validation
type Create struct { type Create struct {
ProductId uint `json:"product_id" validate:"required,number,min=1"` ProductId uint `json:"product_id" validate:"required,number,min=1"`
WarehouseId uint `json:"warehouse_id" validate:"required,number,min=1"` WarehouseId uint `json:"warehouse_id" validate:"required,number,min=1"`
Quantity int `json:"quantity" validate:"required,number,min=0"` Quantity float64 `json:"quantity" validate:"required,number,min=0"`
} }
type Update struct { type Update struct {
ProductId *uint `json:"product_id,omitempty" validate:"omitempty,number,min=1"` ProductId *uint `json:"product_id,omitempty" validate:"omitempty,number,min=1"`
WarehouseId *uint `json:"warehouse_id,omitempty" validate:"omitempty,number,min=1"` WarehouseId *uint `json:"warehouse_id,omitempty" validate:"omitempty,number,min=1"`
Quantity *int `json:"quantity,omitempty" validate:"omitempty,number,min=0"` Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,number,min=0"`
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"` ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
} }
@@ -24,9 +24,10 @@ func NewProductController(productService service.ProductService) *ProductControl
func (u *ProductController) GetAll(c *fiber.Ctx) error { func (u *ProductController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), Search: c.Query("search", ""),
ProductCategoryID: c.QueryInt("product_category_id", 0),
} }
result, totalResults, err := u.ProductService.GetAll(c, query) result, totalResults, err := u.ProductService.GetAll(c, query)
@@ -72,6 +72,9 @@ func (s productService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
if params.Search != "" { if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%") return db.Where("name LIKE ?", "%"+params.Search+"%")
} }
if params.ProductCategoryID != 0 {
return db.Where("product_category_id = ?", params.ProductCategoryID)
}
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -29,7 +29,8 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1"` Limit int `query:"limit" validate:"omitempty,number,min=1"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
ProductCategoryID int `query:"product_category_id" validate:"omitempty,number,min=1"`
} }
@@ -0,0 +1,88 @@
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 StockLogRepository interface {
repository.BaseRepository[entity.StockLog]
GetByFlaggable(ctx context.Context, logType string, logId uint) ([]*entity.StockLog, error)
GetByProductWarehouse(ctx context.Context, productWarehouseId uint, limit int) ([]*entity.StockLog, error)
GetByTransactionType(ctx context.Context, transactionType string, limit int) ([]*entity.StockLog, error)
}
type StockLogRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockLog]
}
func NewStockLogRepository(db *gorm.DB) StockLogRepository {
return &StockLogRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockLog](db),
}
}
func (r *StockLogRepositoryImpl) GetByFlaggable(ctx context.Context, logType string, logId uint) ([]*entity.StockLog, error) {
var stockLogs []*entity.StockLog
err := r.DB().WithContext(ctx).
Where("log_type = ? AND log_id = ?", logType, logId).
Preload("ProductWarehouse").
Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse").
Order("created_at DESC").
Find(&stockLogs).Error
if err != nil {
return nil, err
}
return stockLogs, nil
}
func (r *StockLogRepositoryImpl) GetByProductWarehouse(ctx context.Context, productWarehouseId uint, limit int) ([]*entity.StockLog, error) {
var stockLogs []*entity.StockLog
query := r.DB().WithContext(ctx).
Where("product_warehouse_id = ?", productWarehouseId).
Preload("ProductWarehouse").
Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse").
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
err := query.Find(&stockLogs).Error
if err != nil {
return nil, err
}
return stockLogs, nil
}
func (r *StockLogRepositoryImpl) GetByTransactionType(ctx context.Context, transactionType string, limit int) ([]*entity.StockLog, error) {
var stockLogs []*entity.StockLog
query := r.DB().WithContext(ctx).
Where("transaction_type = ?", transactionType).
Preload("ProductWarehouse").
Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse").
Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
err := query.Find(&stockLogs).Error
if err != nil {
return nil, err
}
return stockLogs, nil
}
+2 -2
View File
@@ -9,9 +9,9 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants" constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
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"
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -25,7 +25,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
users.UserModule{}, users.UserModule{},
master.MasterModule{}, master.MasterModule{},
constants.ConstantModule{}, constants.ConstantModule{},
inventory.InventoryModule{}, inventory.InventoryModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }