diff --git a/internal/database/migrations/20250925040409_create_master_tables.down.sql b/internal/database/migrations/20250925040409_create_master_tables.down.sql index 3f32606a..671f7579 100644 --- a/internal/database/migrations/20250925040409_create_master_tables.down.sql +++ b/internal/database/migrations/20250925040409_create_master_tables.down.sql @@ -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 INDEX IF EXISTS suppliers_name_unique; DROP TABLE IF EXISTS product_suppliers; @@ -35,4 +40,4 @@ DROP TABLE IF EXISTS fcrs; DROP TABLE IF EXISTS projects; DROP INDEX IF EXISTS users_id_user_unique; DROP INDEX IF EXISTS users_email_unique; -DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/internal/database/migrations/20250925040409_create_master_tables.up.sql b/internal/database/migrations/20250925040409_create_master_tables.up.sql index 6dcd914a..07e3005a 100644 --- a/internal/database/migrations/20250925040409_create_master_tables.up.sql +++ b/internal/database/migrations/20250925040409_create_master_tables.up.sql @@ -1,234 +1,337 @@ -- USERS CREATE TABLE users ( - id BIGSERIAL PRIMARY KEY, - id_user BIGINT NOT NULL, - name VARCHAR NOT NULL, - email VARCHAR NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - deleted_at TIMESTAMPTZ + id BIGSERIAL PRIMARY KEY, + id_user BIGINT NOT NULL, + name VARCHAR NOT NULL, + email VARCHAR NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ ); -CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL; -CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL; +CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) +WHERE + deleted_at IS NULL; + +CREATE UNIQUE INDEX users_email_unique ON users (email) +WHERE + deleted_at IS NULL; -- FLAGS CREATE TABLE flags ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - flagable_id BIGINT NOT NULL, - flagable_type VARCHAR(50) NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + flagable_id BIGINT NOT NULL, + flagable_type VARCHAR(50) NOT NULL, + created_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); -- PRODUCT CATEGORIES CREATE TABLE product_categories ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - code VARCHAR(10) NOT NULL, - 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 + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + code VARCHAR(10) NOT NULL, + 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 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 CREATE TABLE uoms ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - 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 + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + 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 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 CREATE TABLE banks ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - alias VARCHAR(5) NOT NULL, - owner VARCHAR, - account_number VARCHAR(50) NOT NULL, - 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 + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + alias VARCHAR(5) NOT NULL, + owner VARCHAR, + account_number VARCHAR(50) NOT NULL, + 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 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 CREATE TABLE areas ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - 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 + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + 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 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 CREATE TABLE locations ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - address TEXT NOT NULL, - area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - deleted_at TIMESTAMPTZ, - created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + address TEXT NOT NULL, + area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + created_by BIGINT 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 CREATE TABLE kandangs ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - location_id BIGINT NOT NULL REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE, - pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - deleted_at TIMESTAMPTZ, - created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE, + pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + created_by BIGINT 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 CREATE TABLE warehouses ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - type VARCHAR(50) NOT NULL, - area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE, - location_id BIGINT REFERENCES locations(id) ON DELETE SET NULL ON UPDATE CASCADE, - kandang_id BIGINT REFERENCES kandangs(id) ON DELETE SET NULL ON UPDATE CASCADE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - deleted_at TIMESTAMPTZ, - created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + type VARCHAR(50) NOT NULL, + area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE, + location_id BIGINT REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE, + kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + created_by BIGINT 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 CREATE TABLE customers ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, - type VARCHAR(50) NOT NULL, - address TEXT NOT NULL, - phone VARCHAR(20) NOT NULL, - email VARCHAR NOT NULL, - account_number VARCHAR(50) NOT NULL, - balance NUMERIC(15,3) DEFAULT 0, - 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 + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE, + type VARCHAR(50) NOT NULL, + address TEXT NOT NULL, + phone VARCHAR(20) NOT NULL, + email VARCHAR NOT NULL, + account_number VARCHAR(50) NOT NULL, + balance NUMERIC(15, 3) DEFAULT 0, + 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 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 CREATE TABLE nonstocks ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - deleted_at TIMESTAMPTZ, - created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + created_by BIGINT 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 CREATE TABLE fcrs ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - 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 + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + 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 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 ( - id BIGSERIAL PRIMARY KEY, - fcr_id BIGINT NOT NULL REFERENCES fcrs(id) ON DELETE CASCADE ON UPDATE CASCADE, - weight NUMERIC(15,3) NOT NULL, - fcr_number NUMERIC(15,3) NOT NULL, - mortality NUMERIC(15,3) NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - deleted_at TIMESTAMPTZ + id BIGSERIAL PRIMARY KEY, + fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE CASCADE ON UPDATE CASCADE, + weight NUMERIC(15, 3) NOT NULL, + fcr_number NUMERIC(15, 3) NOT NULL, + mortality NUMERIC(15, 3) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ ); -- SUPPLIERS CREATE TABLE suppliers ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR NOT NULL, - alias VARCHAR(5) NOT NULL, - pic VARCHAR NOT NULL, - type VARCHAR(50) NOT NULL, - category VARCHAR(20) NOT NULL, - hatchery VARCHAR, - phone VARCHAR(20) NOT NULL, - email VARCHAR NOT NULL, - address TEXT NOT NULL, - npwp VARCHAR(50), - account_number VARCHAR(50), - balance NUMERIC(15,3) DEFAULT 0, - due_date INT NOT NULL, - 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 + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + alias VARCHAR(5) NOT NULL, + pic VARCHAR NOT NULL, + type VARCHAR(50) NOT NULL, + category VARCHAR(20) NOT NULL, + hatchery VARCHAR, + phone VARCHAR(20) NOT NULL, + email VARCHAR NOT NULL, + address TEXT NOT NULL, + npwp VARCHAR(50), + account_number VARCHAR(50), + balance NUMERIC(15, 3) DEFAULT 0, + due_date INT NOT NULL, + 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 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) + 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 + 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 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) + 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 CREATE TABLE projects ( - id BIGSERIAL PRIMARY KEY, - 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 + id BIGSERIAL PRIMARY KEY, + 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 ); + +-- PRODUCT WAREHOUSES TABLE +CREATE TABLE product_warehouses ( + id BIGSERIAL PRIMARY KEY, + product_id BIGINT NOT NULL REFERENCES products (id), + warehouse_id BIGINT NOT NULL REFERENCES warehouses (id), + quantity INTEGER NOT NULL DEFAULT 0, + created_by BIGINT NOT NULL REFERENCES users (id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- INDEXES +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 UNIQUE INDEX idx_product_warehouses_unique ON product_warehouses (product_id, warehouse_id) +WHERE + deleted_at IS NULL; + +-- STOCK LOGS +CREATE TABLE stock_logs ( + id BIGSERIAL PRIMARY KEY, + transaction_type VARCHAR(20) NOT NULL, + quantity NUMERIC(15, 3) NOT NULL, + before_quantity NUMERIC(15, 3) NOT NULL, + after_quantity NUMERIC(15, 3) NOT NULL, + log_type VARCHAR(50) NOT NULL, + log_id BIGINT , + note TEXT, + 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_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- Create indexes for better performance +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_created_by_idx ON stock_logs (created_by); + +CREATE INDEX stock_logs_created_at_idx ON stock_logs (created_at); + +CREATE INDEX stock_logs_deleted_at_idx ON stock_logs (deleted_at); \ No newline at end of file diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 57c3687b..b321a784 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -74,6 +74,10 @@ func Run(db *gorm.DB) error { return err } + if err := seedProductWarehouse(tx, adminID); err != nil { + return err + } + fmt.Println("✅ Master data seeding completed") return nil }) @@ -674,6 +678,8 @@ func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers return nil } +// nanti saya isi + func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error { if len(flags) == 0 { return nil @@ -760,6 +766,39 @@ func seedBanks(tx *gorm.DB, createdBy uint) error { 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 { return &v } diff --git a/internal/entities/product_warehouse.go b/internal/entities/product_warehouse.go new file mode 100644 index 00000000..745dd298 --- /dev/null +++ b/internal/entities/product_warehouse.go @@ -0,0 +1,23 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type ProductWarehouse struct { + Id uint `gorm:"primaryKey;autoIncrement"` + ProductId uint `gorm:"not null"` + WarehouseId uint `gorm:"not null"` + Quantity float64 `gorm:"default:0"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + CreatedBy uint `gorm:"not null"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + // Relations + Product Product `gorm:"foreignKey:ProductId;references:Id"` + Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/entities/stock_log.go b/internal/entities/stock_log.go new file mode 100644 index 00000000..21e86bd4 --- /dev/null +++ b/internal/entities/stock_log.go @@ -0,0 +1,35 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +const ( + LogTypeAdjustment = "ADJUSTMENT" +) + +const ( + TransactionTypeIncrease = "INCREASE" + TransactionTypeDecrease = "DECREASE" +) + +type StockLog struct { + Id uint `gorm:"primaryKey;column:id"` + TransactionType string `gorm:"type:varchar(20);not null"` + Quantity float64 `gorm:"type:numeric(15,3);not null"` + BeforeQuantity float64 `gorm:"type:numeric(15,3);not null"` + AfterQuantity float64 `gorm:"type:numeric(15,3);not null"` + LogType string `gorm:"type:varchar(50);not null;index:stock_logs_flaggable_lookup,priority:1"` + LogId uint `gorm:"not null;index:stock_logs_flaggable_lookup,priority:2"` + Note string `gorm:"type:text"` + ProductWarehouseId uint `gorm:"not null;index"` + CreatedBy uint `gorm:"index"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + + ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"` + CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/inventory/adjustments/controllers/adjustment.controller.go b/internal/modules/inventory/adjustments/controllers/adjustment.controller.go new file mode 100644 index 00000000..d152f360 --- /dev/null +++ b/internal/modules/inventory/adjustments/controllers/adjustment.controller.go @@ -0,0 +1,80 @@ +package controller + +import ( + "math" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type AdjustmentController struct { + AdjustmentService service.AdjustmentService +} + +func NewAdjustmentController(adjustmentService service.AdjustmentService) *AdjustmentController { + return &AdjustmentController{ + AdjustmentService: adjustmentService, + } +} + +func (u *AdjustmentController) Adjustment(c *fiber.Ctx) error { + req := new(validation.Create) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + stockLog, err := u.AdjustmentService.Adjustment(c, req) + if err != nil { + return err + } + + adjustmentDTO := dto.ToAdjustmentDetailDTO(stockLog) + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create adjustment successfully", + Data: adjustmentDTO, + }) +} + +func (u *AdjustmentController) AdjustmentHistory(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + ProductID: c.QueryInt("product_id", 0), + WarehouseID: c.QueryInt("warehouse_id", 0), + TransactionType: c.Query("transaction_type", ""), + } + + result, totalResults, err := u.AdjustmentService.AdjustmentHistory(c, query) + if err != nil { + return err + } + + // Convert to DTOs + adjustmentDTOs := make([]dto.AdjustmentDetailDTO, len(result)) + for i, stockLog := range result { + adjustmentDTOs[i] = dto.ToAdjustmentDetailDTO(stockLog) + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.AdjustmentDetailDTO]{ + Code: fiber.StatusOK, + Status: "success", + 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, + }) +} diff --git a/internal/modules/inventory/adjustments/dto/adjustment.dto.go b/internal/modules/inventory/adjustments/dto/adjustment.dto.go new file mode 100644 index 00000000..72d58c2a --- /dev/null +++ b/internal/modules/inventory/adjustments/dto/adjustment.dto.go @@ -0,0 +1,131 @@ +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 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 { + Id uint `json:"id"` + 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 { + AdjustmentBaseDTO + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +type AdjustmentDetailDTO struct { + AdjustmentListDTO + UpdatedAt time.Time `json:"updated_at"` +} + +// === Mapper Functions === + +func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO { + if e == nil { + return nil + } + sku := "" + if e.Sku != nil { + sku = *e.Sku + } + return &ProductBaseDTO{ + Id: e.Id, + Name: e.Name, + SKU: sku, + } +} + +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 + if e.CreatedUser != nil { + createdUser = &userDTO.UserBaseDTO{ + Id: e.CreatedUser.Id, + IdUser: e.CreatedUser.IdUser, + Email: e.CreatedUser.Email, + Name: e.CreatedUser.Name, + } + } + + return AdjustmentListDTO{ + AdjustmentBaseDTO: ToAdjustmentBaseDTO(e), + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + } +} + +func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO { + return AdjustmentDetailDTO{ + AdjustmentListDTO: ToAdjustmentListDTO(e), + UpdatedAt: e.UpdatedAt, + } +} diff --git a/internal/modules/inventory/adjustments/module.go b/internal/modules/inventory/adjustments/module.go new file mode 100644 index 00000000..294cf9dc --- /dev/null +++ b/internal/modules/inventory/adjustments/module.go @@ -0,0 +1,29 @@ +package adjustments + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + 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" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type AdjustmentModule struct{} + +func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + stockLogsRepo := rStockLogs.NewStockLogRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + userRepo := rUser.NewUserRepository(db) + + adjustmentService := sAdjustment.NewAdjustmentService(stockLogsRepo, warehouseRepo, productWarehouseRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + AdjustmentRoutes(router, userService, adjustmentService) +} diff --git a/internal/modules/inventory/adjustments/route.go b/internal/modules/inventory/adjustments/route.go new file mode 100644 index 00000000..cb63defa --- /dev/null +++ b/internal/modules/inventory/adjustments/route.go @@ -0,0 +1,21 @@ +package adjustments + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers" + adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.AdjustmentService) { + ctrl := controller.NewAdjustmentController(s) + + route := v1.Group("/adjustments") + + // Standard CRUD routes following master data pattern + route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters + route.Post("/", ctrl.Adjustment) // Create adjustment + +} diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go new file mode 100644 index 00000000..252cf7a3 --- /dev/null +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -0,0 +1,195 @@ +package service + +import ( + "errors" + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + 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" + "gorm.io/gorm" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" +) + +type AdjustmentService interface { + Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.StockLog, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.StockLog, error) + AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) +} + +type adjustmentService struct { + Log *logrus.Logger + Validate *validator.Validate + StockLogsRepository stockLogsRepo.StockLogRepository + WarehouseRepo warehouseRepo.WarehouseRepository + ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository +} + +func NewAdjustmentService(stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService { + return &adjustmentService{ + Log: utils.Log, + Validate: validate, + StockLogsRepository: stockLogsRepo, + WarehouseRepo: warehouseRepo, + ProductWarehouseRepo: productWarehouseRepo, + } +} + +func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB { + return db. + Preload("ProductWarehouse"). + Preload("ProductWarehouse.Product"). + Preload("ProductWarehouse.Warehouse"). + Preload("CreatedUser") +} + +func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) { + 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 + } + + offset := (query.Page - 1) * query.Limit + + stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB { + + db = s.withRelations(db) + + db = db.Where("log_type = ?", entity.LogTypeAdjustment) + + if query.TransactionType != "" { + db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType)) + } + if query.ProductID > 0 { + db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id"). + Where("product_warehouses.product_id = ?", query.ProductID) + } + + if query.WarehouseID > 0 { + if query.ProductID > 0 { + + db = db.Where("product_warehouses.warehouse_id = ?", query.WarehouseID) + } else { + + db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id"). + Where("product_warehouses.warehouse_id = ?", query.WarehouseID) + } + } + + return db.Order("created_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get adjustments: %+v", err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history") + } + + result := make([]*entity.StockLog, len(stockLogs)) + for i, v := range stockLogs { + result[i] = &v + } + + return result, total, nil +} diff --git a/internal/modules/inventory/adjustments/validations/adjustment.validation.go b/internal/modules/inventory/adjustments/validations/adjustment.validation.go new file mode 100644 index 00000000..7d2385cc --- /dev/null +++ b/internal/modules/inventory/adjustments/validations/adjustment.validation.go @@ -0,0 +1,17 @@ +package validation + +type Create struct { + ProductID uint `json:"product_id" validate:"required"` + WarehouseID uint `json:"warehouse_id" validate:"required"` + TransactionType string `json:"transaction_type" validate:"required,oneof=increase decrease"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` + Note string `json:"note" validate:"omitempty,max=255"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,min=1"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100"` + 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"` +} diff --git a/internal/modules/inventory/module.go b/internal/modules/inventory/module.go new file mode 100644 index 00000000..4a60224a --- /dev/null +++ b/internal/modules/inventory/module.go @@ -0,0 +1,13 @@ +package inventory + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +type InventoryModule struct{} + +func (InventoryModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + RegisterRoutes(router, db, validate) +} diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go new file mode 100644 index 00000000..5e0ea423 --- /dev/null +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -0,0 +1,141 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type ProductWarehouseController struct { + ProductWarehouseService service.ProductWarehouseService +} + +func NewProductWarehouseController(productWarehouseService service.ProductWarehouseService) *ProductWarehouseController { + return &ProductWarehouseController{ + ProductWarehouseService: productWarehouseService, + } +} + +func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + ProductId: uint(c.QueryInt("product_id", 0)), + WarehouseId: uint(c.QueryInt("warehouse_id", 0)), + } + + result, totalResults, err := u.ProductWarehouseService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ProductWarehouseListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all productWarehouses successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToProductWarehouseListDTOs(result), + }) +} + +func (u *ProductWarehouseController) 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.ProductWarehouseService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get productWarehouse successfully", + Data: dto.ToProductWarehouseListDTO(*result), + }) +} + +func (u *ProductWarehouseController) 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.ProductWarehouseService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create productWarehouse successfully", + Data: dto.ToProductWarehouseListDTO(*result), + }) +} + +func (u *ProductWarehouseController) 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.ProductWarehouseService.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 productWarehouse successfully", + Data: dto.ToProductWarehouseListDTO(*result), + }) +} + +func (u *ProductWarehouseController) 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.ProductWarehouseService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete productWarehouse successfully", + }) +} diff --git a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go new file mode 100644 index 00000000..2260e834 --- /dev/null +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -0,0 +1,104 @@ +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 ProductWarehouseBaseDTO struct { + Id uint `json:"id"` + ProductId uint `json:"product_id"` + WarehouseId uint `json:"warehouse_id"` + Quantity float64 `json:"quantity"` +} + +type ProductWarehouseListDTO struct { + ProductWarehouseBaseDTO + Product *ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ProductWarehouseDetailDTO struct { + ProductWarehouseListDTO +} + +// Nested DTOs for relations +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"` +} + +// === Mapper Functions === + +func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDTO { + return ProductWarehouseBaseDTO{ + Id: e.Id, + ProductId: e.ProductId, // Field yang benar dari entity + WarehouseId: e.WarehouseId, // Field yang benar dari entity + Quantity: e.Quantity, + } +} + +func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO { + dto := ProductWarehouseListDTO{ + ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } + + // Map Product relation jika ada + if e.Product.Id != 0 { + product := ProductBaseDTO{ + Id: e.Product.Id, + Name: e.Product.Name, + } + if e.Product.Sku != nil { + product.Sku = *e.Product.Sku + } + dto.Product = &product + } + + // Map Warehouse relation jika ada + if e.Warehouse.Id != 0 { + warehouse := WarehouseBaseDTO{ + Id: e.Warehouse.Id, + Name: e.Warehouse.Name, + } + dto.Warehouse = &warehouse + } + + // Map CreatedUser relation jika ada + if e.CreatedUser.Id != 0 { + user := userDTO.ToUserBaseDTO(e.CreatedUser) + dto.CreatedUser = &user + } + + return dto +} + +func ToProductWarehouseListDTOs(e []entity.ProductWarehouse) []ProductWarehouseListDTO { + result := make([]ProductWarehouseListDTO, len(e)) + for i, r := range e { + result[i] = ToProductWarehouseListDTO(r) + } + return result +} + +func ToProductWarehouseDetailDTO(e entity.ProductWarehouse) ProductWarehouseDetailDTO { + return ProductWarehouseDetailDTO{ + ProductWarehouseListDTO: ToProductWarehouseListDTO(e), + } +} diff --git a/internal/modules/inventory/product-warehouses/module.go b/internal/modules/inventory/product-warehouses/module.go new file mode 100644 index 00000000..dfb72e8f --- /dev/null +++ b/internal/modules/inventory/product-warehouses/module.go @@ -0,0 +1,26 @@ +package productWarehouses + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + sProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type ProductWarehouseModule struct{} + +func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + userRepo := rUser.NewUserRepository(db) + + productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + ProductWarehouseRoutes(router, userService, productWarehouseService) +} + diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go new file mode 100644 index 00000000..0398a825 --- /dev/null +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -0,0 +1,62 @@ +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 ProductWarehouseRepository interface { + repository.BaseRepository[entity.ProductWarehouse] + ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) + IsProductExist(ctx context.Context, productId uint) (bool, error) + IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) + ExistsByID(ctx context.Context, id uint) (bool, error) + GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) +} + +type ProductWarehouseRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ProductWarehouse] + db *gorm.DB +} + +func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository { + return &ProductWarehouseRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db), + db: db, + } +} + +func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entity.ProductWarehouse{}). + Where("product_id = ? AND warehouse_id = ?", productId, warehouseId) + if excludeID != nil { + query = query.Where("id != ?", *excludeID) + } + if err := query.Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) { + return repository.Exists[entity.Product](ctx, r.db, productId) +} +func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) { + return repository.Exists[entity.Warehouse](ctx, r.db, warehouseId) +} + +func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) { + 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 +} diff --git a/internal/modules/inventory/product-warehouses/route.go b/internal/modules/inventory/product-warehouses/route.go new file mode 100644 index 00000000..b0cc9c65 --- /dev/null +++ b/internal/modules/inventory/product-warehouses/route.go @@ -0,0 +1,28 @@ +package productWarehouses + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/controllers" + productWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func ProductWarehouseRoutes(v1 fiber.Router, u user.UserService, s productWarehouse.ProductWarehouseService) { + ctrl := controller.NewProductWarehouseController(s) + + route := v1.Group("/product-warehouses") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go new file mode 100644 index 00000000..03c7c9a1 --- /dev/null +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -0,0 +1,203 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type ProductWarehouseService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductWarehouse, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductWarehouse, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type productWarehouseService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProductWarehouseRepository +} + +func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate) ProductWarehouseService { + return &productWarehouseService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("Product").Preload("Warehouse").Preload("CreatedUser") +} + +func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + + if params.ProductId != 0 { + db = db.Where("product_id = ?", params.ProductId) + } + + if params.WarehouseId != 0 { + db = db.Where("warehouse_id = ?", params.WarehouseId) + } + + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get productWarehouses: %+v", err) + return nil, 0, err + } + return productWarehouses, total, nil +} + +func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) { + productWarehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found") + } + if err != nil { + s.Log.Errorf("Failed get productWarehouse by id: %+v", err) + return nil, err + } + return productWarehouse, nil +} + +func (s *productWarehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProductWarehouse, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + isProductExist, err := s.Repository.IsProductExist(c.Context(), req.ProductId) + if err != nil { + s.Log.Errorf("Failed to check product existence: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product existence") + } + if !isProductExist { + return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found") + } + + isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), req.WarehouseId) + if err != nil { + s.Log.Errorf("Failed to check warehouse existence: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse existence") + } + if !isWarehouseExist { + return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not found") + } + + // chceking if productWarehouse with same product_id and warehouse_id already + exists, err := s.Repository.ProductWarehouseExists(c.Context(), req.ProductId, req.WarehouseId, nil) + if err != nil { + s.Log.Errorf("Failed to check productWarehouse existence: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check productWarehouse existence") + } + + if exists { + return nil, fiber.NewError(fiber.StatusConflict, "ProductWarehouse already exists") + } + + createBody := &entity.ProductWarehouse{ + ProductId: req.ProductId, + WarehouseId: req.WarehouseId, + Quantity: req.Quantity, + CreatedBy: 1, // TODO: Ganti dengan user ID dari context setelah middleware auth diimplementasi + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create productWarehouse: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s productWarehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductWarehouse, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + // validation Id exist + if exists, err := s.Repository.ExistsByID(c.Context(), id); err != nil { + s.Log.Errorf("Failed to check productWarehouse existence: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check productWarehouse existence") + } else if !exists { + return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found") + } + // validation productId and warehouseId exist + if req.ProductId != nil { + isProductExist, err := s.Repository.IsProductExist(c.Context(), *req.ProductId) + if err != nil { + s.Log.Errorf("Failed to check product existence: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product existence") + } + if !isProductExist { + return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found") + } + } + + if req.WarehouseId != nil { + isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), *req.WarehouseId) + if err != nil { + s.Log.Errorf("Failed to check warehouse existence: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse existence") + } + if !isWarehouseExist { + return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") + } + } + + updateBody := make(map[string]any) + + if req.ProductId != nil { + updateBody["product_id"] = *req.ProductId + } + if req.WarehouseId != nil { + updateBody["warehouse_id"] = *req.WarehouseId + } + if req.Quantity != nil { + updateBody["quantity"] = *req.Quantity + } + + 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, "ProductWarehouse not found") + } + s.Log.Errorf("Failed to update productWarehouse: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s productWarehouseService) 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, "ProductWarehouse not found") + } + s.Log.Errorf("Failed to delete productWarehouse: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go new file mode 100644 index 00000000..02648300 --- /dev/null +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -0,0 +1,20 @@ +package validation + +type Create struct { + ProductId uint `json:"product_id" validate:"required,number,min=1"` + WarehouseId uint `json:"warehouse_id" validate:"required,number,min=1"` + Quantity float64 `json:"quantity" validate:"required,number,min=0"` +} + +type Update struct { + ProductId *uint `json:"product_id,omitempty" validate:"omitempty,number,min=1"` + WarehouseId *uint `json:"warehouse_id,omitempty" validate:"omitempty,number,min=1"` + Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,number,min=0"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` + WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` +} diff --git a/internal/modules/inventory/route.go b/internal/modules/inventory/route.go new file mode 100644 index 00000000..f37e8cad --- /dev/null +++ b/internal/modules/inventory/route.go @@ -0,0 +1,28 @@ +package inventory + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/modules" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses" + adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments" + // MODULE IMPORTS +) + +func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + group := router.Group("/inventory") + + allModules := []modules.Module{ + productWarehouses.ProductWarehouseModule{}, + + adjustments.AdjustmentModule{}, + // MODULE REGISTRY + } + + for _, m := range allModules { + m.RegisterRoutes(group, db, validate) + } +} diff --git a/internal/modules/master/kandangs/controllers/kandang.controller.go b/internal/modules/master/kandangs/controllers/kandang.controller.go index 45ade39d..23d22334 100644 --- a/internal/modules/master/kandangs/controllers/kandang.controller.go +++ b/internal/modules/master/kandangs/controllers/kandang.controller.go @@ -24,9 +24,11 @@ func NewKandangController(kandangService service.KandangService) *KandangControl func (u *KandangController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + LocationId: c.QueryInt("location_id", 0), + PicId: c.QueryInt("pic_id", 0), } result, totalResults, err := u.KandangService.GetAll(c, query) diff --git a/internal/modules/master/kandangs/services/kandang.service.go b/internal/modules/master/kandangs/services/kandang.service.go index 59162860..d856f736 100644 --- a/internal/modules/master/kandangs/services/kandang.service.go +++ b/internal/modules/master/kandangs/services/kandang.service.go @@ -54,6 +54,12 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity if params.Search != "" { return db.Where("name LIKE ?", "%"+params.Search+"%") } + if params.LocationId != 0 { + db = db.Where("location_id = ?", params.LocationId) + } + if params.PicId != 0 { + db = db.Where("pic_id = ?", params.PicId) + } return db.Order("created_at DESC").Order("updated_at DESC") }) diff --git a/internal/modules/master/kandangs/validations/kandang.validation.go b/internal/modules/master/kandangs/validations/kandang.validation.go index d76a6982..8b986ca6 100644 --- a/internal/modules/master/kandangs/validations/kandang.validation.go +++ b/internal/modules/master/kandangs/validations/kandang.validation.go @@ -13,7 +13,9 @@ type Update struct { } 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"` + 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"` + LocationId int `query:"location_id" validate:"omitempty,number,gt=0"` + PicId int `query:"pic_id" validate:"omitempty,number,gt=0"` } diff --git a/internal/modules/master/locations/controllers/location.controller.go b/internal/modules/master/locations/controllers/location.controller.go index 37330928..8f8211d7 100644 --- a/internal/modules/master/locations/controllers/location.controller.go +++ b/internal/modules/master/locations/controllers/location.controller.go @@ -27,6 +27,7 @@ func (u *LocationController) GetAll(c *fiber.Ctx) error { Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), Search: c.Query("search", ""), + AreaId: c.QueryInt("area_id", 0), } result, totalResults, err := u.LocationService.GetAll(c, query) diff --git a/internal/modules/master/locations/services/location.service.go b/internal/modules/master/locations/services/location.service.go index 1aae174c..7b7599ea 100644 --- a/internal/modules/master/locations/services/location.service.go +++ b/internal/modules/master/locations/services/location.service.go @@ -54,6 +54,9 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit if params.Search != "" { db = db.Where("name LIKE ?", "%"+params.Search+"%") } + if params.AreaId != 0 { + db = db.Where("area_id = ?", params.AreaId) + } return db.Order("created_at DESC").Order("updated_at DESC") }) diff --git a/internal/modules/master/locations/validations/location.validation.go b/internal/modules/master/locations/validations/location.validation.go index a9af1eb8..029953c0 100644 --- a/internal/modules/master/locations/validations/location.validation.go +++ b/internal/modules/master/locations/validations/location.validation.go @@ -16,4 +16,5 @@ 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"` + AreaId int `query:"area_id" validate:"omitempty,number,gt=0"` } diff --git a/internal/modules/master/products/controllers/product.controller.go b/internal/modules/master/products/controllers/product.controller.go index 2406cd9c..ee2c95f8 100644 --- a/internal/modules/master/products/controllers/product.controller.go +++ b/internal/modules/master/products/controllers/product.controller.go @@ -24,9 +24,10 @@ func NewProductController(productService service.ProductService) *ProductControl 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", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + ProductCategoryID: c.QueryInt("product_category_id", 0), } result, totalResults, err := u.ProductService.GetAll(c, query) diff --git a/internal/modules/master/products/services/product.service.go b/internal/modules/master/products/services/product.service.go index 076cabf9..fb1fe00f 100644 --- a/internal/modules/master/products/services/product.service.go +++ b/internal/modules/master/products/services/product.service.go @@ -72,6 +72,9 @@ func (s productService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity if 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") }) diff --git a/internal/modules/master/products/validations/product.validation.go b/internal/modules/master/products/validations/product.validation.go index 07f4c005..70e23a74 100644 --- a/internal/modules/master/products/validations/product.validation.go +++ b/internal/modules/master/products/validations/product.validation.go @@ -29,7 +29,8 @@ type Update struct { } 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"` + 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"` + ProductCategoryID int `query:"product_category_id" validate:"omitempty,number,min=1"` } diff --git a/internal/modules/master/route.go b/internal/modules/master/route.go index 6c98db4f..88584c13 100644 --- a/internal/modules/master/route.go +++ b/internal/modules/master/route.go @@ -8,17 +8,17 @@ import ( "gorm.io/gorm" areas "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas" + banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks" customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers" fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs" kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs" locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations" nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks" productcategories "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories" + products "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products" suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers" uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms" warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses" - products "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products" - banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks" // MODULE IMPORTS ) diff --git a/internal/modules/master/warehouses/controllers/warehouse.controller.go b/internal/modules/master/warehouses/controllers/warehouse.controller.go index b1813ef2..b841d4ef 100644 --- a/internal/modules/master/warehouses/controllers/warehouse.controller.go +++ b/internal/modules/master/warehouses/controllers/warehouse.controller.go @@ -27,6 +27,7 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error { Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), Search: c.Query("search", ""), + AreaId: c.QueryInt("area_id", 0), } result, totalResults, err := u.WarehouseService.GetAll(c, query) diff --git a/internal/modules/master/warehouses/services/warehouse.service.go b/internal/modules/master/warehouses/services/warehouse.service.go index 3b45de5f..6cf45e0a 100644 --- a/internal/modules/master/warehouses/services/warehouse.service.go +++ b/internal/modules/master/warehouses/services/warehouse.service.go @@ -55,6 +55,9 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti if params.Search != "" { return db.Where("name LIKE ?", "%"+params.Search+"%") } + if params.AreaId != 0 { + db = db.Where("area_id = ?", params.AreaId) + } return db.Order("created_at DESC").Order("updated_at DESC") }) diff --git a/internal/modules/master/warehouses/validations/warehouse.validation.go b/internal/modules/master/warehouses/validations/warehouse.validation.go index ab16c02a..809ef0c4 100644 --- a/internal/modules/master/warehouses/validations/warehouse.validation.go +++ b/internal/modules/master/warehouses/validations/warehouse.validation.go @@ -20,4 +20,5 @@ 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"` + AreaId int `query:"area_id" validate:"omitempty,number,gt=0"` } diff --git a/internal/modules/shared/stock-logs/repositories/stock-logs.repository.go b/internal/modules/shared/stock-logs/repositories/stock-logs.repository.go new file mode 100644 index 00000000..c93db2b1 --- /dev/null +++ b/internal/modules/shared/stock-logs/repositories/stock-logs.repository.go @@ -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 +} diff --git a/internal/route/route.go b/internal/route/route.go index c4bfa4b0..82b48166 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -8,9 +8,10 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + 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" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" - constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants" // MODULE IMPORTS ) @@ -23,7 +24,8 @@ func Routes(app *fiber.App, db *gorm.DB) { allModules := []modules.Module{ users.UserModule{}, master.MasterModule{}, - constants.ConstantModule{}, + constants.ConstantModule{}, + inventory.InventoryModule{}, // MODULE REGISTRY }