Merge branch 'development-after-rebase' into 'development'

chore(REBASE): Development with SSO

See merge request mbugroup/lti-api!40
This commit is contained in:
Adnan Zahir
2025-10-23 11:50:06 +07:00
143 changed files with 9043 additions and 322 deletions
@@ -0,0 +1,106 @@
package repository
import (
"context"
"errors"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ApprovalRepository interface {
BaseRepository[entity.Approval]
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
}
type approvalRepositoryImpl struct {
*BaseRepositoryImpl[entity.Approval]
}
func NewApprovalRepository(db *gorm.DB) ApprovalRepository {
return &approvalRepositoryImpl{
BaseRepositoryImpl: NewBaseRepository[entity.Approval](db),
}
}
func (r *approvalRepositoryImpl) FindByTarget(
ctx context.Context,
workflow string,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]entity.Approval, error) {
var approvals []entity.Approval
q := r.DB().WithContext(ctx).Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID)
if modifier != nil {
q = modifier(q)
}
if err := q.Order("action_at ASC").Find(&approvals).Error; err != nil {
return nil, err
}
return approvals, nil
}
func (r *approvalRepositoryImpl) LatestByTarget(
ctx context.Context,
workflow string,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) (*entity.Approval, error) {
var approval entity.Approval
q := r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
Order("action_at DESC")
if modifier != nil {
q = modifier(q)
}
if err := q.Limit(1).First(&approval).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &approval, nil
}
func (r *approvalRepositoryImpl) LatestByTargets(
ctx context.Context,
workflow string,
approvableIDs []uint,
modifier func(*gorm.DB) *gorm.DB,
) (map[uint]entity.Approval, error) {
if len(approvableIDs) == 0 {
return nil, nil
}
result := make(map[uint]entity.Approval, len(approvableIDs))
q := r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
Order("action_at DESC")
if modifier != nil {
q = modifier(q)
}
var approvals []entity.Approval
if err := q.Find(&approvals).Error; err != nil {
return nil, err
}
for _, approval := range approvals {
if _, exists := result[approval.ApprovableId]; exists {
continue
}
result[approval.ApprovableId] = approval
}
return result, nil
}
@@ -0,0 +1,234 @@
package service
import (
"context"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm"
)
type ApprovalService interface {
RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error
WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string
WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool)
CreateApproval(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, step approvalutils.ApprovalStep, action *entity.ApprovalAction, actorID uint, note *string) (*entity.Approval, error)
List(ctx context.Context, module string, approvableID *uint, page, limit int, search string) ([]entity.Approval, int64, error)
ListByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
LatestByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
LatestByTargets(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]*entity.Approval, error)
}
type approvalService struct {
repo commonRepo.ApprovalRepository
}
func NewApprovalService(repo commonRepo.ApprovalRepository) ApprovalService {
return &approvalService{repo: repo}
}
func (s *approvalService) RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error {
return approvalutils.RegisterWorkflowSteps(workflow, steps)
}
func (s *approvalService) WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string {
return approvalutils.WorkflowSteps(workflow)
}
func (s *approvalService) WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool) {
return approvalutils.ApprovalStepName(workflow, step)
}
func (s *approvalService) CreateApproval(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableID uint,
step approvalutils.ApprovalStep,
action *entity.ApprovalAction,
actorID uint,
note *string,
) (*entity.Approval, error) {
record, err := approvalutils.NewApproval(workflow, approvableID, step, action, actorID, note)
if err != nil {
return nil, err
}
if err := s.repo.CreateOne(ctx, record, nil); err != nil {
return nil, err
}
s.decorateApproval(workflow, record)
return record, nil
}
func (s *approvalService) List(
ctx context.Context,
module string,
approvableID *uint,
page, limit int,
search string,
) ([]entity.Approval, int64, error) {
module = strings.TrimSpace(strings.ToUpper(module))
search = strings.TrimSpace(search)
if limit <= 0 {
limit = 10
}
if page <= 0 {
page = 1
}
offset := (page - 1) * limit
records, total, err := s.repo.GetAll(
ctx,
offset,
limit,
func(db *gorm.DB) *gorm.DB {
query := db.
Where("approvable_type = ?", module).
Order("action_at DESC").
Preload("ActionUser")
if approvableID != nil {
query = query.Where("approvable_id = ?", *approvableID)
}
if search != "" {
like := "%" + strings.ToLower(search) + "%"
query = query.Where("(LOWER(step_name) LIKE ? OR LOWER(action) LIKE ? OR LOWER(notes) LIKE ?)", like, like, like)
}
return query
},
)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, 0, nil
}
return nil, 0, err
}
if len(records) == 0 {
return nil, total, nil
}
workflow := approvalutils.ApprovalWorkflowKey(module)
for i := range records {
s.decorateApproval(workflow, &records[i])
}
return records, total, nil
}
func (s *approvalService) ListByTarget(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]entity.Approval, error) {
records, err := s.repo.FindByTarget(ctx, workflow.String(), approvableID, modifier)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, nil
}
return nil, err
}
for i := range records {
s.decorateApproval(workflow, &records[i])
}
return records, nil
}
func (s *approvalService) LatestByTarget(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) (*entity.Approval, error) {
record, err := s.repo.LatestByTarget(ctx, workflow.String(), approvableID, modifier)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, nil
}
return nil, err
}
if record == nil {
return nil, nil
}
s.decorateApproval(workflow, record)
return record, nil
}
func (s *approvalService) LatestByTargets(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableIDs []uint,
modifier func(*gorm.DB) *gorm.DB,
) (map[uint]*entity.Approval, error) {
records, err := s.repo.LatestByTargets(ctx, workflow.String(), approvableIDs, modifier)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, nil
}
return nil, err
}
if len(records) == 0 {
return nil, nil
}
result := make(map[uint]*entity.Approval, len(records))
for approvableID, approval := range records {
approvalCopy := approval
s.decorateApproval(workflow, &approvalCopy)
result[approvableID] = &approvalCopy
}
return result, nil
}
func (s *approvalService) decorateApproval(workflow approvalutils.ApprovalWorkflowKey, approval *entity.Approval) {
if approval == nil {
return
}
currentName := strings.TrimSpace(approval.StepName)
if currentName == "" {
if name, ok := approvalutils.ApprovalStepName(workflow, approvalutils.ApprovalStep(approval.StepNumber)); ok {
approval.StepName = name
}
} else {
approval.StepName = currentName
}
}
func (s *approvalService) isApprovalTableMissing(err error) bool {
if err == nil {
return false
}
errMsg := strings.ToLower(err.Error())
if strings.Contains(errMsg, "no such table: approvals") {
return true
}
schemaIssues := []string{
`relation "approvals" does not exist`,
`column "step_name" does not exist`,
`column "step_number" does not exist`,
`column "action" does not exist`,
`column "status" does not exist`,
`column "step" does not exist`,
}
for _, issue := range schemaIssues {
if strings.Contains(errMsg, issue) {
return true
}
}
return false
}
@@ -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;
@@ -1,12 +1,12 @@
-- 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;
@@ -15,221 +15,319 @@ 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);
@@ -0,0 +1,4 @@
-- DROP TABLE: STOCK_TRANSFERS DAN SEQUENCE-NYA
DROP TABLE IF EXISTS stock_transfers CASCADE;
DROP SEQUENCE IF EXISTS stock_transfer_seq CASCADE;
@@ -0,0 +1,57 @@
-- ===============================================================
-- STOCK TRANSFERS (HEADER)
-- ===============================================================
CREATE SEQUENCE IF NOT EXISTS stock_transfer_seq START 1;
CREATE TABLE IF NOT EXISTS stock_transfers (
id BIGSERIAL PRIMARY KEY,
movement_number VARCHAR(50) UNIQUE NOT NULL,
from_warehouse_id BIGINT NOT NULL,
to_warehouse_id BIGINT NOT NULL,
area_id BIGINT,
reason TEXT,
transfer_date DATE NOT NULL,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_from_warehouse
FOREIGN KEY (from_warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_to_warehouse
FOREIGN KEY (to_warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'areas') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_area
FOREIGN KEY (area_id)
REFERENCES areas(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfers_from_warehouse_id ON stock_transfers(from_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfers_to_warehouse_id ON stock_transfers(to_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfers_transfer_date ON stock_transfers(transfer_date);
@@ -0,0 +1,2 @@
-- DROP TABLE: STOCK_TRANSFER_DETAILS
DROP TABLE IF EXISTS stock_transfer_details CASCADE;
@@ -0,0 +1,48 @@
-- ===============================================================
-- STOCK TRANSFER DETAILS (PRODUK)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_details (
id BIGSERIAL PRIMARY KEY,
stock_transfer_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0),
before_quantity NUMERIC(15, 3),
after_quantity NUMERIC(15, 3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- ===============================================================
-- FOREIGN KEYS (dengan pengecekan tabel agar anti gagal)
-- ===============================================================
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_transfer
FOREIGN KEY (stock_transfer_id)
REFERENCES stock_transfers(id)
ON DELETE CASCADE ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_product
FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
END $$;
-- ===============================================================
-- INDEXES
-- ===============================================================
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_transfer_id ON stock_transfer_details (stock_transfer_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_product_id ON stock_transfer_details (product_id);
@@ -0,0 +1,2 @@
-- DROP TABLE: STOCK_TRANSFER_DELIVERIES
DROP TABLE IF EXISTS stock_transfer_deliveries CASCADE;
@@ -0,0 +1,42 @@
-- ===============================================================
-- STOCK TRANSFER DELIVERIES (EKSPEDISI)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_deliveries (
id BIGSERIAL PRIMARY KEY,
stock_transfer_id BIGINT NOT NULL,
supplier_id BIGINT,
vehicle_plate VARCHAR(20),
driver_name VARCHAR(100),
document_number VARCHAR(50),
document_path TEXT,
shipping_cost_item NUMERIC(15,3),
shipping_cost_total NUMERIC(15,3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
ALTER TABLE stock_transfer_deliveries
ADD CONSTRAINT fk_stock_transfer_deliveries_transfer
FOREIGN KEY (stock_transfer_id)
REFERENCES stock_transfers(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
ALTER TABLE stock_transfer_deliveries
ADD CONSTRAINT fk_stock_transfer_deliveries_supplier
FOREIGN KEY (supplier_id)
REFERENCES suppliers(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_transfer_id ON stock_transfer_deliveries(stock_transfer_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_supplier_id ON stock_transfer_deliveries(supplier_id);
@@ -0,0 +1,2 @@
-- DROP PIVOT TABLE: STOCK_TRANSFER_DELIVERY_ITEMS
DROP TABLE IF EXISTS stock_transfer_delivery_items CASCADE;
@@ -0,0 +1,35 @@
-- ===============================================================
-- STOCK TRANSFER DELIVERY ITEMS (PIVOT)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_delivery_items (
id BIGSERIAL PRIMARY KEY,
stock_transfer_delivery_id BIGINT NOT NULL,
stock_transfer_detail_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0)
);
-- FOREIGN KEYS
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_deliveries') THEN
ALTER TABLE stock_transfer_delivery_items
ADD CONSTRAINT fk_delivery_items_delivery
FOREIGN KEY (stock_transfer_delivery_id)
REFERENCES stock_transfer_deliveries(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_details') THEN
ALTER TABLE stock_transfer_delivery_items
ADD CONSTRAINT fk_delivery_items_detail
FOREIGN KEY (stock_transfer_detail_id)
REFERENCES stock_transfer_details(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_delivery_id ON stock_transfer_delivery_items (stock_transfer_delivery_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_detail_id ON stock_transfer_delivery_items (stock_transfer_detail_id);
@@ -0,0 +1,2 @@
ALTER TABLE kandangs
DROP COLUMN IF EXISTS status;
@@ -0,0 +1,9 @@
ALTER TABLE kandangs
ADD COLUMN status VARCHAR(20);
UPDATE kandangs
SET status = 'NON_ACTIVE'
WHERE status IS NULL;
ALTER TABLE kandangs
ALTER COLUMN status SET NOT NULL;
@@ -0,0 +1,6 @@
ALTER TABLE kandangs
DROP COLUMN IF EXISTS project_flock_id;
DROP TABLE IF EXISTS project_flocks;
DROP TABLE IF EXISTS flocks;
@@ -0,0 +1,29 @@
CREATE TABLE flocks (
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 flocks_name_unique ON flocks (name)
WHERE
deleted_at IS NULL;
CREATE TABLE project_flocks (
id BIGSERIAL PRIMARY KEY,
flock_id BIGINT NOT NULL REFERENCES flocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE RESTRICT ON UPDATE CASCADE,
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
period 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
);
ALTER TABLE kandangs
ADD COLUMN project_flock_id BIGINT REFERENCES project_flocks (id) ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS approvals_approvable_lookup;
DROP TABLE IF EXISTS approvals;
@@ -0,0 +1,12 @@
CREATE TABLE approvals (
id BIGSERIAL PRIMARY KEY,
approvable_type VARCHAR(50) NOT NULL,
approvable_id BIGINT NOT NULL,
step SMALLINT NOT NULL,
status VARCHAR(20) NOT NULL,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
action_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE INDEX approvals_approvable_lookup ON approvals (approvable_type, approvable_id);
@@ -0,0 +1,18 @@
ALTER TABLE approvals
RENAME COLUMN action TO status;
UPDATE approvals
SET status = 'PENDING'
WHERE status IS NULL;
ALTER TABLE approvals
ALTER COLUMN status SET NOT NULL;
ALTER TABLE approvals
RENAME COLUMN step_number TO step;
ALTER TABLE approvals
DROP COLUMN step_name;
ALTER TABLE approvals
RENAME COLUMN action_at TO created_at;
@@ -0,0 +1,14 @@
ALTER TABLE approvals
RENAME COLUMN status TO action;
ALTER TABLE approvals
ALTER COLUMN action DROP NOT NULL;
ALTER TABLE approvals
RENAME COLUMN step TO step_number;
ALTER TABLE approvals
ADD COLUMN step_name VARCHAR NOT NULL;
ALTER TABLE approvals
RENAME COLUMN created_at TO action_at;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_flock_kandangs;
@@ -0,0 +1,17 @@
CREATE TABLE project_flock_kandangs (
id BIGSERIAL PRIMARY KEY,
project_flock_id BIGINT NOT NULL REFERENCES project_flocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
kandang_id BIGINT NOT NULL REFERENCES kandangs (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
detached_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_project_flock_kandangs_project ON project_flock_kandangs (project_flock_id);
CREATE INDEX idx_project_flock_kandangs_kandang ON project_flock_kandangs (kandang_id);
CREATE UNIQUE INDEX idx_project_flock_kandangs_active ON project_flock_kandangs (project_flock_id, kandang_id)
WHERE
detached_at IS NULL;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_chickins;
@@ -0,0 +1,36 @@
CREATE TABLE IF NOT EXISTS project_chickins (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
chick_in_date DATE NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
note TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_flock_populations;
@@ -0,0 +1,36 @@
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
initial_quantity NUMERIC(15, 3) NOT NULL,
current_quantity NUMERIC(15, 3) NOT NULL,
reserved_quantity NUMERIC(15, 3),
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
@@ -0,0 +1,25 @@
BEGIN;
-- Recreate legacy columns on project_flock_kandangs
DROP INDEX IF EXISTS idx_project_flock_kandangs_unique;
ALTER TABLE project_flock_kandangs
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
ADD COLUMN IF NOT EXISTS assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS detached_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_active
ON project_flock_kandangs (project_flock_id, kandang_id)
WHERE detached_at IS NULL;
-- Restore product_category_id reference and drop category column
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS product_category_id BIGINT REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS category;
COMMIT;
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
@@ -0,0 +1,43 @@
BEGIN;
-- Add category column to project_flocks and backfill existing rows
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS category VARCHAR(20);
UPDATE project_flocks
SET category = 'GROWING'
WHERE category IS NULL;
ALTER TABLE project_flocks
ALTER COLUMN category SET NOT NULL;
ALTER TABLE project_flocks
ALTER COLUMN category SET DEFAULT 'GROWING';
-- Drop legacy foreign key reference and column
ALTER TABLE project_flocks
DROP CONSTRAINT IF EXISTS project_flocks_product_category_id_fkey;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS product_category_id;
-- Simplify project_flock_kandangs structure
DROP INDEX IF EXISTS idx_project_flock_kandangs_active;
ALTER TABLE project_flock_kandangs
DROP COLUMN IF EXISTS created_by,
DROP COLUMN IF EXISTS assigned_at,
DROP COLUMN IF EXISTS detached_at,
DROP COLUMN IF EXISTS updated_at;
ALTER TABLE project_flock_kandangs
ALTER COLUMN created_at SET DEFAULT NOW();
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_unique
ON project_flock_kandangs (project_flock_id, kandang_id);
COMMIT;
CREATE UNIQUE INDEX project_flocks_flock_period_unique
ON project_flocks (flock_id, period)
WHERE deleted_at IS NULL;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_chickin_details;
@@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS project_chickin_details (
id BIGSERIAL PRIMARY KEY,
project_chickin_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_project_chickin_id
FOREIGN KEY (project_chickin_id)
REFERENCES project_chickins(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_product_warehouse_id
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_project_chickin_id ON project_chickin_details (project_chickin_id);
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_product_warehouse_id ON project_chickin_details (product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_created_by ON project_chickin_details (created_by);
+548 -28
View File
@@ -8,6 +8,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm"
)
@@ -35,7 +36,27 @@ func Run(db *gorm.DB) error {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users)
productCategories, err := seedProductCategories(tx, adminID)
if err != nil {
return err
}
flocks, err := seedFlocks(tx, adminID)
if err != nil {
return err
}
fcrs, err := seedFcr(tx, adminID)
if err != nil {
return err
}
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations)
if err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users, projectFlocks)
if err != nil {
return err
}
@@ -44,11 +65,6 @@ func Run(db *gorm.DB) error {
return err
}
productCategories, err := seedProductCategories(tx, adminID)
if err != nil {
return err
}
suppliers, err := seedSuppliers(tx, adminID)
if err != nil {
return err
@@ -58,10 +74,6 @@ func Run(db *gorm.DB) error {
return err
}
if err := seedFcr(tx, adminID); err != nil {
return err
}
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
return err
}
@@ -74,6 +86,17 @@ func Run(db *gorm.DB) error {
return err
}
if err := seedProductWarehouse(tx, adminID); err != nil {
return err
}
if err := seedTransferStock(tx, adminID); err != nil {
return err
}
if err := seedChickin(tx, adminID); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed")
return nil
})
@@ -190,16 +213,190 @@ func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[stri
return result, nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Flock Priangan", "Flock Banten"}
result := make(map[string]uint, len(names))
for _, name := range names {
var flock entity.Flock
err := tx.Where("name = ?", name).First(&flock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
flock = entity.Flock{
Name: name,
CreatedBy: createdBy,
}
if err := tx.Create(&flock).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.Flock{}).Where("id = ?", flock.Id).Updates(map[string]any{
"created_by": createdBy,
}).Error; err != nil {
return nil, err
}
}
result[name] = flock.Id
}
return result, nil
}
func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locations map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Key string
Flock string
Area string
Category utils.ProjectFlockCategory
Fcr string
Location string
PicKey string
Period int
}{
{"Singaparna 1", "Singaparna", "admin"},
{"Singaparna 2", "Singaparna", "admin"},
{"Cikaum 1", "Cikaum", "admin"},
{"Cikaum 2", "Cikaum", "admin"},
{
Key: "Singaparna Period 1",
Flock: "Flock Priangan",
Area: "Priangan",
Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer",
Location: "Singaparna",
Period: 1,
},
{
Key: "Cikaum Period 1",
Flock: "Flock Banten",
Area: "Banten",
Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer",
Location: "Cikaum",
Period: 1,
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
flockID, ok := flocks[seed.Flock]
if !ok {
return nil, fmt.Errorf("floc %s not seeded", seed.Flock)
}
areaID, ok := areas[seed.Area]
if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area)
}
fcrID, ok := fcrs[seed.Fcr]
if !ok {
return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr)
}
locationID, ok := locations[seed.Location]
if !ok {
return nil, fmt.Errorf("location %s not seeded", seed.Location)
}
var projectFlock entity.ProjectFlock
err := tx.Where("flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
flockID, areaID, seed.Category, fcrID, locationID, seed.Period).First(&projectFlock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
projectFlock = entity.ProjectFlock{
FlockId: flockID,
AreaId: areaID,
Category: string(seed.Category),
FcrId: fcrID,
LocationId: locationID,
Period: seed.Period,
CreatedBy: createdBy,
}
if err := tx.Create(&projectFlock).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
"flock_id": flockID,
"area_id": areaID,
"category": string(seed.Category),
"fcr_id": fcrID,
"location_id": locationID,
"period": seed.Period,
}).Error; err != nil {
return nil, err
}
}
if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil {
return nil, err
}
result[seed.Key] = projectFlock.Id
}
return result, nil
}
func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint) error {
if projectFlockID == 0 || actorID == 0 {
return nil
}
workflow := utils.ApprovalWorkflowProjectFlock.String()
steps := []struct {
step approvalutils.ApprovalStep
action entity.ApprovalAction
}{
{step: utils.ProjectFlockStepPengajuan, action: entity.ApprovalActionCreated},
{step: utils.ProjectFlockStepAktif, action: entity.ApprovalActionApproved},
}
for _, cfg := range steps {
var count int64
if err := tx.Model(&entity.Approval{}).
Where("approvable_type = ? AND approvable_id = ? AND step_number = ?", workflow, projectFlockID, uint16(cfg.step)).
Count(&count).Error; err != nil {
return err
}
if count > 0 {
continue
}
stepName, ok := utils.ProjectFlockApprovalSteps[cfg.step]
if !ok || strings.TrimSpace(stepName) == "" {
stepName = fmt.Sprintf("Step %d", cfg.step)
}
var actionPtr *entity.ApprovalAction
action := cfg.action
actionPtr = &action
record := entity.Approval{
ApprovableType: workflow,
ApprovableId: projectFlockID,
StepNumber: uint16(cfg.step),
StepName: stepName,
Action: actionPtr,
ActionBy: uintPtr(actorID),
}
if err := tx.Create(&record).Error; err != nil {
return err
}
}
return nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint, projectFlocks map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Status utils.KandangStatus
Location string
PicKey string
ProjectFlockKey *string
}{
{Name: "Singaparna 1", Status: utils.KandangStatusActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusActive, Location: "Cikaum", PicKey: "admin", ProjectFlockKey: strPtr("Cikaum Period 1")},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
}
result := make(map[string]uint, len(seeds))
@@ -214,20 +411,51 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
}
var projectFlockID *uint
if seed.ProjectFlockKey != nil {
pfID, ok := projectFlocks[*seed.ProjectFlockKey]
if !ok {
return nil, fmt.Errorf("project flock %s not seeded", *seed.ProjectFlockKey)
}
projectFlockID = uintPtr(pfID)
}
var kandang entity.Kandang
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
kandang = entity.Kandang{
Name: seed.Name,
LocationId: locID,
PicId: picID,
CreatedBy: createdBy,
Name: seed.Name,
Status: string(seed.Status),
LocationId: locID,
PicId: picID,
ProjectFlockId: projectFlockID,
CreatedBy: createdBy,
}
if err := tx.Create(&kandang).Error; err != nil {
return nil, err
}
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
updates := map[string]any{
"location_id": locID,
"pic_id": picID,
"status": string(seed.Status),
}
if projectFlockID != nil {
updates["project_flock_id"] = *projectFlockID
} else {
updates["project_flock_id"] = nil
}
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
return nil, err
}
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err
}
}
result[seed.Name] = kandang.Id
}
@@ -235,6 +463,38 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return result, nil
}
func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint) error {
if err := detachActivePivot(tx, kandangID); err != nil {
return err
}
if projectFlockID == nil {
return nil
}
return ensureActivePivot(tx, *projectFlockID, kandangID)
}
func detachActivePivot(tx *gorm.DB, kandangID uint) error {
return tx.Where("kandang_id = ?", kandangID).
Delete(&entity.ProjectFlockKandang{}).Error
}
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error {
var pivot entity.ProjectFlockKandang
err := tx.Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&pivot).Error
if err == nil {
return nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
newRecord := entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: kandangID,
}
return tx.Create(&newRecord).Error
}
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct {
Name string
@@ -426,7 +686,7 @@ func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
return nil
}
func seedFcr(tx *gorm.DB, createdBy uint) error {
func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct {
Name string
Standards []struct {
@@ -448,17 +708,20 @@ func seedFcr(tx *gorm.DB, createdBy uint) error {
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
var fcr entity.Fcr
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
if err := tx.Create(&fcr).Error; err != nil {
return err
return nil, err
}
} else if err != nil {
return err
return nil, err
}
result[seed.Name] = fcr.Id
for _, std := range seed.Standards {
var standard entity.FcrStandard
@@ -471,22 +734,22 @@ func seedFcr(tx *gorm.DB, createdBy uint) error {
Mortality: std.Mortality,
}
if err := tx.Create(&standard).Error; err != nil {
return err
return nil, err
}
} else if err != nil {
return err
return nil, err
} else {
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
"fcr_number": std.FcrNumber,
"mortality": std.Mortality,
}).Error; err != nil {
return err
return nil, err
}
}
}
}
return nil
return result, nil
}
func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
@@ -674,6 +937,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 +1025,261 @@ 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: 2, WarehouseID: 1, Quantity: 300},
{ProductID: 1, WarehouseID: 3, Quantity: 5000},
}
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 seedTransferStock(tx *gorm.DB, createdBy uint) error {
transfer := entity.StockTransfer{
FromWarehouseId: 1,
ToWarehouseId: 2,
Reason: "Seed transfer stock",
TransferDate: time.Now(),
MovementNumber: "SEED-TRF-00001",
CreatedBy: 1,
}
if err := tx.Create(&transfer).Error; err != nil {
return err
}
details := []entity.StockTransferDetail{
{
StockTransferId: transfer.Id,
ProductId: 1,
Quantity: 10,
},
{
StockTransferId: transfer.Id,
ProductId: 2,
Quantity: 5,
},
}
for i := range details {
if err := tx.Create(&details[i]).Error; err != nil {
return err
}
}
deliveries := []entity.StockTransferDelivery{
{
StockTransferId: transfer.Id,
SupplierId: 1,
VehiclePlate: "B 1234 XYZ",
DriverName: "Driver Seed",
DocumentPath: "seed.pdf",
ShippingCostItem: 1000,
ShippingCostTotal: 2000,
},
}
for i := range deliveries {
if err := tx.Create(&deliveries[i]).Error; err != nil {
return err
}
}
detailMap := make(map[uint64]uint64)
for _, d := range details {
detailMap[d.ProductId] = d.Id
}
deliveryItems := []entity.StockTransferDeliveryItem{
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[1],
Quantity: 50,
},
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[2],
Quantity: 30,
},
}
for i := range deliveryItems {
if err := tx.Create(&deliveryItems[i]).Error; err != nil {
return err
}
}
return nil
}
func seedChickin(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
ProjectFlockKandangId uint
ChickInDate string
Quantity float64
Note string
}{
{ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"},
{ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"},
}
for _, seed := range seeds {
chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate)
if err != nil {
return err
}
// Insert ProjectChickin jika belum ada
var chickin entity.ProjectChickin
err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate).
First(&chickin).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
chickin = entity.ProjectChickin{
ProjectFlockKandangId: seed.ProjectFlockKandangId,
ChickInDate: chickinDate,
Quantity: seed.Quantity,
Note: seed.Note,
CreatedBy: createdBy,
}
if err := tx.Create(&chickin).Error; err != nil {
return err
}
} else if err != nil {
return err
}
var population entity.ProjectFlockPopulation
err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
population = entity.ProjectFlockPopulation{
ProjectFlockKandangId: seed.ProjectFlockKandangId,
InitialQuantity: seed.Quantity,
CurrentQuantity: seed.Quantity,
ReservedQuantity: 0,
CreatedBy: createdBy,
}
if err := tx.Create(&population).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
// Update population quantities
if err := tx.Model(&entity.ProjectFlockPopulation{}).
Where("id = ?", population.Id).
Updates(map[string]any{
"initial_quantity": population.InitialQuantity + seed.Quantity,
"current_quantity": population.CurrentQuantity + seed.Quantity,
"reserved_quantity": 0,
}).Error; err != nil {
return err
}
}
var pfk entity.ProjectFlockKandang
if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// no pivot found; skip creating details
continue
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil {
// if warehouse not found, cannot create details
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
var productWarehouses []entity.ProductWarehouse
err = tx.Table("product_warehouses").
Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
if err != nil {
return err
}
// If no product warehouses found, keep existing chickin.Quantity and skip details
if len(productWarehouses) == 0 {
continue
}
// sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne)
totalQty := 0.0
for _, pw := range productWarehouses {
totalQty += pw.Quantity
}
if chickin.Quantity != totalQty {
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil {
return err
}
chickin.Quantity = totalQty
}
for _, pw := range productWarehouses {
// ensure detail exists or create it with full pw.Quantity
var detail entity.ProjectChickinDetail
err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
detail = entity.ProjectChickinDetail{
ProjectChickinId: chickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: createdBy,
}
if err := tx.Create(&detail).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if detail.Quantity != pw.Quantity {
if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil {
return err
}
}
}
// zero out pw quantity
if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil {
return err
}
}
}
return nil
}
func ptr[T any](v T) *T {
return &v
}
+22
View File
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectChickinDetail struct {
Id uint `gorm:"primaryKey"`
ProjectChickinId uint `gorm:"column:project_chickin_id;not null"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Quantity float64 `gorm:"type:numeric(15,3);not null"`
CreatedBy uint `gorm:"column:created_by;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+28
View File
@@ -0,0 +1,28 @@
package entities
import (
"time"
)
type ApprovalAction string
const (
ApprovalActionApproved ApprovalAction = "APPROVED"
ApprovalActionRejected ApprovalAction = "REJECTED"
ApprovalActionCreated ApprovalAction = "CREATED"
ApprovalActionUpdated ApprovalAction = "UPDATED"
)
type Approval struct {
Id uint `gorm:"primaryKey"`
ApprovableType string `gorm:"size:50;not null;index:approvals_approvable_lookup,priority:1"`
ApprovableId uint `gorm:"not null;index:approvals_approvable_lookup,priority:2"`
StepNumber uint16 `gorm:"not null"`
StepName string `gorm:"not null"`
Action *ApprovalAction `gorm:"type:VARCHAR(20)"`
Notes *string `gorm:"type:text"`
ActionAt time.Time `gorm:"autoCreateTime"`
ActionBy *uint `gorm:"index"`
ActionUser *User `gorm:"foreignKey:ActionBy;references:Id"`
}
+17
View File
@@ -0,0 +1,17 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Flock struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:flocks_name_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+14 -12
View File
@@ -7,16 +7,18 @@ import (
)
type Kandang struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"`
ProjectFlockId *uint `gorm:"column:project_flock_id"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
}
+23
View File
@@ -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"`
}
+24
View File
@@ -0,0 +1,24 @@
package entities
import (
"time"
"gorm.io/gorm"
)
const ()
type ProjectChickin struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null"`
ChickInDate time.Time `gorm:"not null"`
Quantity float64 `gorm:"not null"`
Note string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectFlockPopulation struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null"`
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectFlock struct {
Id uint `gorm:"primaryKey"`
FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
AreaId uint `gorm:"not null"`
Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"`
Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+12
View File
@@ -0,0 +1,12 @@
package entities
import "time"
type ProjectFlockKandang struct {
Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Recording 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"`
}
+23
View File
@@ -0,0 +1,23 @@
package entities
import "time"
// HEADER
type StockTransfer struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
MovementNumber string `gorm:"uniqueIndex;not null"`
FromWarehouseId uint64
ToWarehouseId uint64
TransferDate time.Time
Reason string
CreatedBy uint64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
FromWarehouse *Warehouse `gorm:"foreignKey:FromWarehouseId"`
ToWarehouse *Warehouse `gorm:"foreignKey:ToWarehouseId"`
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
}
+36
View File
@@ -0,0 +1,36 @@
package entities
import (
"time"
"gorm.io/gorm"
)
const (
LogTypeAdjustment = "ADJUSTMENT"
LogTypeTransfer = "TRANSFER"
)
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"`
}
@@ -0,0 +1,23 @@
package entities
import "time"
// DETAIL EKSPEDISI
type StockTransferDelivery struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64
SupplierId uint64
VehiclePlate string
DriverName string
DocumentNumber string
DocumentPath string
ShippingCostItem float64
ShippingCostTotal float64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
}
@@ -0,0 +1,12 @@
package entities
// PIVOT TABLE TRANSFER
type StockTransferDeliveryItem struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferDeliveryId uint64
StockTransferDetailId uint64
Quantity float64
// Relations
StockTransferDelivery *StockTransferDelivery `gorm:"foreignKey:StockTransferDeliveryId"`
StockTransferDetail *StockTransferDetail `gorm:"foreignKey:StockTransferDetailId"`
}
@@ -0,0 +1,18 @@
package entities
import "time"
// DETAIL PRODUK
type StockTransferDetail struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64
ProductId uint64
Quantity float64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Product *Product `gorm:"foreignKey:ProductId"`
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
}
+80 -91
View File
@@ -1,110 +1,99 @@
package middleware
import (
"strings"
// import (
// "strings"
"gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/sso"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
// "gitlab.com/mbugroup/lti-api.git/internal/config"
// service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
// "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
)
// "github.com/gofiber/fiber/v2"
// )
func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
// func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
// return func(c *fiber.Ctx) error {
// authHeader := c.Get("Authorization")
// token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
if token == "" {
cookieName := config.SSOAccessCookieName
if cookieName == "" {
cookieName = "access"
}
token = strings.TrimSpace(c.Cookies(cookieName))
}
// if token == "" {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
if token == "" {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)
// if err != nil {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
verification, err := sso.VerifyAccessToken(token)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// // Only end-user subjects are allowed by this middleware. Service tokens
// if verification.UserID == 0 {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
if len(config.SSOAllowedAudiences) > 0 {
allowed := make(map[string]struct{}, len(config.SSOAllowedAudiences))
for _, aud := range config.SSOAllowedAudiences {
aud = strings.TrimSpace(aud)
if aud != "" {
allowed[aud] = struct{}{}
}
}
audienceValid := false
for _, aud := range verification.Claims.Audience {
if _, ok := allowed[aud]; ok {
audienceValid = true
break
}
}
if !audienceValid {
return fiber.NewError(fiber.StatusUnauthorized, "invalid audience")
}
}
// // Fail-closed on revocation check errors for stricter security posture.
// if revoker := session.GetRevocationStore(); revoker != nil {
// if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
// revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
// if err != nil {
// utils.Log.WithError(err).Warn("failed to check token revocation")
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// if revoked {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// }
// }
if revoker := session.GetRevocationStore(); revoker != nil {
logoutAt, err := revoker.UserLogoutTime(c.Context(), verification.UserID)
if err != nil {
utils.Log.WithError(err).Warn("failed to load logout marker")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
if !logoutAt.IsZero() {
if verification.Claims.IssuedAt == nil || !verification.Claims.IssuedAt.Time.After(logoutAt) {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
}
// user, err := userService.GetBySSOUserID(c, verification.UserID)
// if err != nil || user == nil {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
if err != nil {
utils.Log.WithError(err).Warn("failed to check token revocation")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
if revoked {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
}
}
// if len(requiredRights) > 0 && verification.Claims != nil {
// if !hasAllScopes(verification.Claims.Scopes(), requiredRights) {
// return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
// }
// }
user, err := userService.GetBySSOUserID(c, verification.UserID)
if err != nil || user == nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// c.Locals("user", user)
c.Locals("user", user)
c.Locals("token_claims", verification.Claims)
// // if len(requiredRights) > 0 {
// // userRights, hasRights := config.RoleRights[user.Role]
// // if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID {
// // return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource")
// // }
// // }
// if len(requiredRights) > 0 {
// userRights, hasRights := config.RoleRights[user.Role]
// if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID {
// return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource")
// }
// }
return c.Next()
}
}
// func hasAllRights(userRights, requiredRights []string) bool {
// rightSet := make(map[string]struct{}, len(userRights))
// for _, right := range userRights {
// rightSet[right] = struct{}{}
// return c.Next()
// }
// }
// for _, right := range requiredRights {
// if _, exists := rightSet[right]; !exists {
// // bearerToken extracts a Bearer token from the Authorization header using
// // case-insensitive scheme matching and tolerant whitespace handling.
// func bearerToken(c *fiber.Ctx) string {
// parts := strings.Fields(c.Get("Authorization"))
// if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
// return strings.TrimSpace(parts[1])
// }
// return ""
// }
// func hasAllScopes(have, required []string) bool {
// if len(required) == 0 {
// return true
// }
// set := make(map[string]struct{}, len(have))
// for _, s := range have {
// s = strings.ToLower(strings.TrimSpace(s))
// if s != "" {
// set[s] = struct{}{}
// }
// }
// for _, r := range required {
// r = strings.ToLower(strings.TrimSpace(r))
// if r == "" {
// continue
// }
// if _, ok := set[r]; !ok {
// return false
// }
// }
@@ -0,0 +1,100 @@
package controller
import (
"math"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
)
type ApprovalController struct {
ApprovalService common.ApprovalService
}
func NewApprovalController(approvalService common.ApprovalService) *ApprovalController {
return &ApprovalController{
ApprovalService: approvalService,
}
}
func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
moduleName := strings.TrimSpace(c.Query("module_name", ""))
if moduleName == "" {
return fiber.NewError(fiber.StatusBadRequest, "`module_name` is required")
}
moduleIDParam := strings.TrimSpace(c.Query("module_id", ""))
var moduleID *uint
if moduleIDParam != "" {
value, err := strconv.ParseUint(moduleIDParam, 10, 64)
if err != nil || value == 0 {
return fiber.NewError(fiber.StatusBadRequest, "module_id must be a positive integer")
}
id := uint(value)
moduleID = &id
}
groupByStep := c.QueryBool("group_step_number", false)
page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 10)
search := strings.TrimSpace(c.Query("search", ""))
query := &validation.Query{
ModuleName: moduleName,
ModuleId: moduleID,
GroupByStep: groupByStep,
Page: page,
Limit: limit,
Search: search,
}
records, totalResults, err := u.ApprovalService.List(
c.Context(),
query.ModuleName,
query.ModuleId,
query.Page,
query.Limit,
query.Search,
)
if err != nil {
return err
}
if query.GroupByStep {
data := dto.ToApprovalGroupDTOs(records)
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ApprovalGroupDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get All approvals successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: data,
})
}
flat := dto.ToApprovalDTOs(records)
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ApprovalBaseDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get All approvals successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: flat,
})
}
@@ -0,0 +1,122 @@
package dto
import (
"sort"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
)
type ApprovalBaseDTO struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Action *string `json:"action"`
Notes *string `json:"notes"`
ActionBy userDTO.UserBaseDTO `json:"action_by"`
ActionAt time.Time `json:"action_at"`
}
type ApprovalGroupDTO struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Approvals []ApprovalBaseDTO `json:"approvals"`
}
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
dto := ApprovalBaseDTO{
Notes: e.Notes,
}
if e.StepNumber > 0 {
stepCopy := uint16(e.StepNumber)
dto.StepNumber = stepCopy
}
stepName := strings.TrimSpace(e.StepName)
if stepName == "" && e.ApprovableType != "" && e.StepNumber > 0 {
if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(e.ApprovableType), approvalutils.ApprovalStep(e.StepNumber)); ok {
stepName = label
}
}
dto.StepName = stepName
if e.Action != nil {
value := strings.TrimSpace(string(*e.Action))
if value != "" {
valueCopy := value
dto.Action = &valueCopy
}
}
if e.ActionUser != nil && e.ActionUser.Id != 0 {
user := userDTO.ToUserBaseDTO(*e.ActionUser)
dto.ActionBy = user
} else if e.ActionBy != nil && *e.ActionBy != 0 {
dto.ActionBy = userDTO.UserBaseDTO{
Id: *e.ActionBy,
IdUser: int64(*e.ActionBy),
}
}
if !e.ActionAt.IsZero() {
at := e.ActionAt
dto.ActionAt = at
}
return dto
}
func ToApprovalDTOs(items []entity.Approval) []ApprovalBaseDTO {
result := make([]ApprovalBaseDTO, len(items))
for i, item := range items {
result[i] = ToApprovalDTO(item)
}
return result
}
func ToApprovalGroupDTOs(items []entity.Approval) []ApprovalGroupDTO {
if len(items) == 0 {
return nil
}
type groupAccumulator struct {
StepName string
Approvals []ApprovalBaseDTO
}
groups := make(map[uint16]*groupAccumulator)
order := make([]uint16, 0)
for _, item := range items {
step := item.StepNumber
acc, exists := groups[step]
if !exists {
stepName := strings.TrimSpace(item.StepName)
if stepName == "" && item.ApprovableType != "" && item.StepNumber > 0 {
if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(item.ApprovableType), approvalutils.ApprovalStep(item.StepNumber)); ok {
stepName = label
}
}
acc = &groupAccumulator{StepName: stepName}
groups[step] = acc
order = append(order, step)
}
acc.Approvals = append(acc.Approvals, ToApprovalDTO(item))
}
sort.Slice(order, func(i, j int) bool { return order[i] < order[j] })
result := make([]ApprovalGroupDTO, len(order))
for i, step := range order {
acc := groups[step]
result[i] = ApprovalGroupDTO{
StepNumber: step,
StepName: acc.StepName,
Approvals: acc.Approvals,
}
}
return result
}
+25
View File
@@ -0,0 +1,25 @@
package approvals
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ApprovalModule struct{}
func (ApprovalModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
approvalRepo := commonRepo.NewApprovalRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
userService := sUser.NewUserService(userRepo, validate)
ApprovalRoutes(router, userService, approvalService)
}
+19
View File
@@ -0,0 +1,19 @@
package approvals
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"github.com/gofiber/fiber/v2"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
_ = u
ctrl := controller.NewApprovalController(s)
route := v1.Group("/approvals")
route.Get("/", ctrl.GetAll)
}
@@ -0,0 +1,10 @@
package validation
type Query struct {
ModuleName string `json:"module_name" validate:"required_strict"`
ModuleId *uint `json:"module_id,omitempty" validate:"omitempty,gt=0"`
GroupByStep bool `json:"group_by_step"`
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -1,9 +1,13 @@
package repository
import (
"sort"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm"
)
@@ -26,6 +30,50 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
for f := range utils.AllFlagTypes() {
flagList = append(flagList, string(f))
}
sort.Strings(flagList)
type approvalStepConstant struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
}
workflowConstants := approvalutils.WorkflowConstants()
workflowKeys := make([]string, 0, len(workflowConstants))
for key := range workflowConstants {
workflowKeys = append(workflowKeys, key)
}
sort.Strings(workflowKeys)
approvalWorkflows := make([]map[string]interface{}, 0, len(workflowKeys))
for _, key := range workflowKeys {
stepMap := workflowConstants[key]
if len(stepMap) == 0 {
continue
}
stepList := make([]approvalStepConstant, 0, len(stepMap))
for stepStr, label := range stepMap {
stepNum, err := strconv.ParseUint(stepStr, 10, 16)
if err != nil || stepNum == 0 {
continue
}
stepList = append(stepList, approvalStepConstant{
StepNumber: uint16(stepNum),
StepName: label,
})
}
if len(stepList) == 0 {
continue
}
sort.Slice(stepList, func(i, j int) bool {
return stepList[i].StepNumber < stepList[j].StepNumber
})
approvalWorkflows = append(approvalWorkflows, map[string]interface{}{
"key": key,
"steps": stepList,
})
}
return map[string]interface{}{
"flags": flagList,
@@ -42,5 +90,6 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"BISNIS",
"INDIVIDUAL",
},
"approval_workflows": approvalWorkflows,
}
}
@@ -0,0 +1,103 @@
package controller
import (
"math"
"strconv"
"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: uint(c.QueryInt("product_id", 0)),
WarehouseID: uint(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,
})
}
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")
}
stockLog, err := u.AdjustmentService.GetOne(c, uint(id))
if err != nil {
return err
}
// Use DTO for response
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get adjustment successfully",
Data: dto.ToAdjustmentDetailDTO(stockLog),
})
}
@@ -0,0 +1,141 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
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"`
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
}
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
}
var category *productCategoryDTO.ProductCategoryBaseDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
category = &mapped
}
return &ProductBaseDTO{
Id: e.Id,
Name: e.Name,
SKU: sku,
ProductCategory: category,
}
}
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,
}
}
@@ -0,0 +1,31 @@
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"
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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)
productRepo := rproduct.NewProductRepository(db)
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
AdjustmentRoutes(router, userService, adjustmentService)
}
@@ -0,0 +1,22 @@
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
route.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,235 @@
package service
import (
"errors"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
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"
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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
ProductRepo productRepo.ProductRepository
}
func NewAdjustmentService(productRepo productRepo.ProductRepository, 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,
ProductRepo: productRepo,
}
}
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, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db).Preload("ProductWarehouse.Product.ProductCategory")
})
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()
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists},
common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists},
); err != nil {
return nil, err
}
if req.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
}
transactionType := strings.ToUpper(req.TransactionType)
if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
}
var createdLogId uint
isProductWarehouseExist, err := s.ProductWarehouseRepo.ProductWarehouseExistByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to check product warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
}
if !isProductWarehouseExist {
newPW := &entity.ProductWarehouse{
ProductId: uint(req.ProductID),
WarehouseId: uint(req.WarehouseID),
Quantity: 0,
CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
s.Log.Errorf("Failed to create product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
}
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
}
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to get product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
}
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
}
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
}
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
isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
}
if query.WarehouseID > 0 && !isWarehousesExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
isProductsExist, err := s.ProductRepo.IdExists(c.Context(), uint(query.ProductID))
if err != nil {
s.Log.Errorf("Failed to check product existence: %+v", err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product")
}
if query.ProductID > 0 && !isProductsExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
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
}
@@ -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 uint `query:"product_id" validate:"omitempty,min=0"`
WarehouseID uint `query:"warehouse_id" validate:"omitempty,min=0"`
TransactionType string `query:"transaction_type" validate:"omitempty,oneof=increase decrease"`
}
+13
View File
@@ -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)
}
@@ -0,0 +1,75 @@
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),
})
}
@@ -0,0 +1,178 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
// === 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 *UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UserBaseDTO struct {
Id uint `json:"id"`
Username string `json:"username"`
}
type ProductWarehouseDetailDTO struct {
ProductWarehouseListDTO
}
// Nested DTOs for relations
type ProductBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Sku string `json:"sku"`
Flags []string `json:"flags"`
}
type WarehouseBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Kandang *KandangBaseDTO `json:"kandang,omitempty"`
Location *LocationBaseDTO `json:"location,omitempty"`
Area *AreaBaseDTO `json:"area,omitempty"`
}
type KandangBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaBaseDTO 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
}
if len(e.Product.Flags) > 0 {
for _, f := range e.Product.Flags {
product.Flags = append(product.Flags, f.Name)
}
}
dto.Product = &product
}
// Map Warehouse relation jika ada
if e.Warehouse.Id != 0 {
warehouse := WarehouseBaseDTO{
Id: e.Warehouse.Id,
Name: e.Warehouse.Name,
}
// Map Kandang jika ada
if e.Warehouse.Kandang != nil && e.Warehouse.Kandang.Id != 0 {
warehouse.Kandang = &KandangBaseDTO{
Id: e.Warehouse.Kandang.Id,
Name: e.Warehouse.Kandang.Name,
}
}
// Map Location jika ada
if e.Warehouse.Location != nil && e.Warehouse.Location.Id != 0 {
warehouse.Location = &LocationBaseDTO{
Id: e.Warehouse.Location.Id,
Name: e.Warehouse.Location.Name,
}
}
if &e.Warehouse.Area != nil && e.Warehouse.Area.Id != 0 {
warehouse.Area = &AreaBaseDTO{
Id: e.Warehouse.Area.Id,
Name: e.Warehouse.Area.Name,
}
}
dto.Warehouse = &warehouse
}
// Map CreatedUser relation jika ada
if e.CreatedUser.Id != 0 {
user := UserBaseDTO{
Id: e.CreatedUser.Id,
Username: e.CreatedUser.Name,
}
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),
}
}
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
return KandangBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToLocationBaseDTO(e entity.Location) LocationBaseDTO {
return LocationBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToAreaBaseDTO(e entity.Area) AreaBaseDTO {
return AreaBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
@@ -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)
}
@@ -0,0 +1,74 @@
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)
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, 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) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
var count int64
if err := r.db.WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
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
}
@@ -0,0 +1,26 @@
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.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,85 @@
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)
}
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.Flags").
Preload("Product").
Preload("Warehouse").
Preload("Warehouse.Location").
Preload("Warehouse.Area").
Preload("Warehouse.Kandang").
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
}
@@ -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"`
}
+30
View File
@@ -0,0 +1,30 @@
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"
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
// MODULE IMPORTS
)
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
group := router.Group("/inventory")
allModules := []modules.Module{
productWarehouses.ProductWarehouseModule{},
adjustments.AdjustmentModule{},
transfers.TransferModule{},
// MODULE REGISTRY
}
for _, m := range allModules {
m.RegisterRoutes(group, db, validate)
}
}
@@ -0,0 +1,103 @@
package controller
import (
"encoding/json"
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransferController struct {
TransferService service.TransferService
}
func NewTransferController(transferService service.TransferService) *TransferController {
return &TransferController{
TransferService: transferService,
}
}
func (u *TransferController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.TransferService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transfers successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransferListDTOs(result),
})
}
func (u *TransferController) 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.TransferService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transfer successfully",
Data: dto.ToTransferListDTO(*result),
})
}
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
data := c.FormValue("data")
var req validation.TransferRequest
if err := json.Unmarshal([]byte(data), &req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
// ambil file
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
_ = form.File["documents"]
// todo: tunggu ada aws baru proses
result, err := u.TransferService.CreateOne(c, &req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create transfer successfully",
Data: dto.ToTransferListDTO(*result),
})
}
@@ -0,0 +1,245 @@
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 TransferBaseDTO struct {
Id uint64 `json:"id"`
TransferReason string `json:"transfer_reason"`
TransferDate string `json:"transfer_date"`
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
}
// Only id and name for warehouse simple view
type WarehouseSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type SupplierSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type WarehouseDetailDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Location *LocationDTO `json:"location"`
Area *AreaDTO `json:"area"`
}
type TransferListDTO struct {
TransferBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Details []TransferDetailItemDTO `json:"details"`
Deliveries []TransferDeliveryDTO `json:"deliveries"`
}
type TransferDetailDTO struct {
TransferListDTO
Details []TransferDetailItemDTO `json:"details"`
Deliveries []TransferDeliveryDTO `json:"deliveries"`
}
// Detail produk
type TransferDetailItemDTO struct {
Id uint64 `json:"id"`
Proudct ProductSimpleDTO `json:"product"`
Quantity float64 `json:"quantity"`
}
// Delivery ekspedisi
type TransferDeliveryDTO struct {
Id uint64 `json:"id"`
Supplier SupplierSimpleDTO `json:"supplier"`
VehiclePlate string `json:"vehicle_plate"`
DriverName string `json:"driver_name"`
DocumentNumber string `json:"document_number"`
DocumentPath string `json:"document_path"`
ShippingCostItem float64 `json:"shipping_cost_item"`
ShippingCostTotal float64 `json:"shipping_cost_total"`
Items []TransferDeliveryItemDTO `json:"items"`
}
type TransferDeliveryItemDTO struct {
Id uint64 `json:"id"`
StockTransferDetailId uint64 `json:"stock_transfer_detail_id"`
Quantity float64 `json:"quantity"`
}
// === Mapper Functions ===
func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
var sourceWarehouse *WarehouseDetailDTO
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
}
var destinationWarehouse *WarehouseDetailDTO
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
}
return TransferBaseDTO{
Id: e.Id,
TransferReason: e.Reason,
TransferDate: e.CreatedAt.Format("2006-01-02"),
SourceWarehouse: sourceWarehouse,
DestinationWarehouse: destinationWarehouse,
}
}
func toAreaDTO(a *entity.Area) *AreaDTO {
if a == nil {
return nil
}
return &AreaDTO{
Id: a.Id,
Name: a.Name,
}
}
func toLocationDTO(l *entity.Location) *LocationDTO {
if l == nil {
return nil
}
return &LocationDTO{
Id: l.Id,
Name: l.Name,
}
}
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
if w == nil {
return nil
}
return &WarehouseDetailDTO{
Id: w.Id,
Name: w.Name,
Location: toLocationDTO(w.Location),
Area: toAreaDTO(&w.Area), // Ambil area langsung dari warehouse (area_id)
}
}
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser != nil {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
// Map details
var details []TransferDetailItemDTO
for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{
Id: d.Id,
Proudct: ProductSimpleDTO{
Id: d.Product.Id,
Name: d.Product.Name,
},
Quantity: d.Quantity,
})
}
// Map deliveries
var deliveries []TransferDeliveryDTO
for _, del := range e.Deliveries {
// Map delivery items
var items []TransferDeliveryItemDTO
for _, item := range del.Items {
items = append(items, TransferDeliveryItemDTO{
Id: item.Id,
StockTransferDetailId: item.StockTransferDetailId,
Quantity: item.Quantity,
})
}
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
Supplier: SupplierSimpleDTO{
Id: del.Supplier.Id,
Name: del.Supplier.Name,
},
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
Items: items,
})
}
return TransferListDTO{
TransferBaseDTO: ToTransferBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Details: details,
Deliveries: deliveries,
}
}
func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
result := make([]TransferListDTO, len(e))
for i, r := range e {
result[i] = ToTransferListDTO(r)
}
return result
}
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
// Map details
var details []TransferDetailItemDTO
for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{
Id: d.Id,
Proudct: ProductSimpleDTO{
Id: d.Product.Id,
Name: d.Product.Name,
},
Quantity: d.Quantity,
})
}
// Map deliveries
var deliveries []TransferDeliveryDTO
for _, del := range e.Deliveries {
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
Supplier: SupplierSimpleDTO{
Id: del.Supplier.Id,
Name: del.Supplier.Name,
},
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
})
}
return TransferDetailDTO{
TransferListDTO: ToTransferListDTO(e),
Details: details,
Deliveries: deliveries,
}
}
@@ -0,0 +1,33 @@
package transfers
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"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type TransferModule struct{}
func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
stockTransferRepo := rStockTransfer.NewStockTransferRepository(db)
stockTransferDetailRepo := rStockTransfer.NewStockTransferDetailRepository(db)
stockTransferDeliveryRepo := rStockTransfer.NewStockTransferDeliveryRepository(db)
StockTransferDeliveryItemRepo := rStockTransfer.NewStockTransferDeliveryItemRepository(db)
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo)
userService := sUser.NewUserService(userRepo, validate)
TransferRoutes(router, userService, transferService)
}
@@ -0,0 +1,34 @@
package repositories
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferRepository interface {
repository.BaseRepository[entity.StockTransfer]
// get sequence for movement number
GetNextMovementNumber(ctx context.Context) (int64, error)
}
type StockTransferRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransfer]
}
func NewStockTransferRepository(db *gorm.DB) StockTransferRepository {
return &StockTransferRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransfer](db),
}
}
func (r *StockTransferRepositoryImpl) GetNextMovementNumber(ctx context.Context) (int64, error) {
var seq int64
err := r.DB().WithContext(ctx).Raw("SELECT nextval('stock_transfer_seq')").Scan(&seq).Error
if err != nil {
return 0, err
}
return seq, nil
}
@@ -0,0 +1,22 @@
package repositories
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferDeliveryRepository interface {
repository.BaseRepository[entity.StockTransferDelivery]
// Tambahkan custom method jika perlu
}
type StockTransferDeliveryRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransferDelivery]
}
func NewStockTransferDeliveryRepository(db *gorm.DB) StockTransferDeliveryRepository {
return &StockTransferDeliveryRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDelivery](db),
}
}
@@ -0,0 +1,22 @@
package repositories
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferDeliveryItemRepository interface {
repository.BaseRepository[entity.StockTransferDeliveryItem]
// Tambahkan custom method jika perlu
}
type StockTransferDeliveryItemRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransferDeliveryItem]
}
func NewStockTransferDeliveryItemRepository(db *gorm.DB) StockTransferDeliveryItemRepository {
return &StockTransferDeliveryItemRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDeliveryItem](db),
}
}
@@ -0,0 +1,29 @@
// Find all details by StockTransferId
package repositories
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferDetailRepository interface {
repository.BaseRepository[entity.StockTransferDetail]
FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error
}
type StockTransferDetailRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransferDetail]
}
func NewStockTransferDetailRepository(db *gorm.DB) StockTransferDetailRepository {
return &StockTransferDetailRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDetail](db),
}
}
func (r *StockTransferDetailRepositoryImpl) FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error {
return r.DB().WithContext(ctx).Where("stock_transfer_id = ?", transferId).Find(out).Error
}
@@ -0,0 +1,27 @@
package transfers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/controllers"
transfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferService) {
ctrl := controller.NewTransferController(s)
route := v1.Group("/transfers")
// 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)
}
@@ -0,0 +1,351 @@
package service
import (
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"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 TransferService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error)
}
type transferService struct {
Log *logrus.Logger
Validate *validator.Validate
StockTransferRepo rStockTransfer.StockTransferRepository
StockTransferDetailRepo rStockTransfer.StockTransferDetailRepository
StockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository
StockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository
StockLogsRepository rStockLogs.StockLogRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
SupplierRepo rSupplier.SupplierRepository
}
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository) TransferService {
return &transferService{
Log: utils.Log,
Validate: validate,
StockTransferRepo: stockTransferRepo,
StockTransferDetailRepo: stockTransferDetailRepo,
StockTransferDeliveryRepo: stockTransferDeliveryRepo,
StockTransferDeliveryItemRepo: stockTransferDeliveryItemRepo,
StockLogsRepository: stockLogsRepo,
ProductWarehouseRepo: productWarehouseRepo,
SupplierRepo: supplierRepo,
}
}
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("FromWarehouse").
Preload("FromWarehouse.Location").
Preload("FromWarehouse.Area").
Preload("ToWarehouse").
Preload("ToWarehouse.Location").
Preload("ToWarehouse.Area").
Preload("Details").
Preload("Details.Product").
Preload("Deliveries.Items").
Preload("Deliveries.Supplier")
}
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("movement_number LIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
return nil, 0, err
}
s.Log.Infof("Retrieved %d transfers", len(transfers))
return transfers, total, nil
}
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
var transfer entity.StockTransfer
// gunakan repo secara langsung
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db)
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
s.Log.Errorf("Failed to get transfer by ID: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
}
s.Log.Infof("Retrieved transfer: %+v", transfer)
return transferPtr, nil
}
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
// Validasi stok di gudang asal harus exist dan mencukupi
for _, product := range req.Products {
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
}
if sourcePW.Quantity < product.ProductQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
}
}
// validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid
deliveryQtyMap := make(map[uint]float64)
for _, delivery := range req.Deliveries {
for _, prod := range delivery.Products {
deliveryQtyMap[prod.ProductID] += prod.ProductQty
}
}
// Cek: qty delivery tidak boleh melebihi qty di root
for _, product := range req.Products {
if deliveryQtyMap[product.ProductID] > product.ProductQty {
return nil, fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("Total qty delivery untuk produk %d (%v) melebihi qty transfer (%v)", product.ProductID, deliveryQtyMap[product.ProductID], product.ProductQty))
}
}
// cek suplier id caegory BOP cek by id
for _, delivery := range req.Deliveries {
supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
}
if supplier.Category != "BOP" {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
}
}
// Generate movement number
// Format: PND-MBU-00001
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
if err != nil {
s.Log.Errorf("Failed to get next movement number: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
}
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
transferDate, _ := utils.ParseDateString(req.TransferDate)
entityTransfer := &entity.StockTransfer{
FromWarehouseId: uint64(req.SourceWarehouseID),
ToWarehouseId: uint64(req.DestinationWarehouseID),
Reason: req.TransferReason,
TransferDate: transferDate,
MovementNumber: movementNumber,
CreatedBy: 1, //todo: get from token
}
// Save the transfer entity to the database
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
// Insert header
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer: %+v", err)
return err
}
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
// insert ke details
var details []*entity.StockTransferDetail
for _, product := range req.Products {
details = append(details, &entity.StockTransferDetail{
StockTransferId: entityTransfer.Id,
ProductId: uint64(product.ProductID),
Quantity: product.ProductQty,
})
}
if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer details: %+v", err)
return err
}
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
// Tambahkan proses insert delivery
var deliveries []*entity.StockTransferDelivery
for _, delivery := range req.Deliveries {
deliveries = append(deliveries, &entity.StockTransferDelivery{
StockTransferId: entityTransfer.Id,
SupplierId: uint64(delivery.SupplierID),
VehiclePlate: delivery.VehiclePlate,
DriverName: delivery.DriverName,
DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf", // todo: tunggu ada aws baru proses
ShippingCostItem: delivery.DeliveryCostPerItem,
ShippingCostTotal: delivery.DeliveryCost,
})
}
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
return err
}
// tambahkan insert ke delivery items sebagai pivot
detailMap := make(map[uint64]uint64)
for _, d := range details {
detailMap[d.ProductId] = d.Id
}
var deliveryItems []*entity.StockTransferDeliveryItem
for i, delivery := range deliveries {
item := req.Deliveries[i]
for _, prod := range item.Products {
detailID, ok := detailMap[uint64(prod.ProductID)]
if !ok {
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
}
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
StockTransferDeliveryId: delivery.Id,
StockTransferDetailId: detailID,
Quantity: prod.ProductQty,
})
}
}
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer delivery items: %+v", err)
return err
}
s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id)
// Proses pengurangan stok di gudang asal dan penambahan stok di gudang tujuan
for _, product := range req.Products {
// Kurangi stok di gudang asal
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
if err != nil {
s.Log.Errorf("Failed to get source product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
}
if sourcePW.Quantity < product.ProductQty {
s.Log.Errorf("Insufficient stock in source warehouse for product ID: %+v", product.ProductID)
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock in source warehouse for product ID: %d", product.ProductID))
}
sourcePW.Quantity -= product.ProductQty
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), sourcePW.Id, sourcePW, nil); err != nil {
s.Log.Errorf("Failed to update source product warehouse: %+v", err)
return err
}
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
// create stock log for decrease (source)
beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
decreaseLog := &entity.StockLog{
TransactionType: entity.TransactionTypeDecrease,
Quantity: product.ProductQty,
BeforeQuantity: beforeQty,
AfterQuantity: sourcePW.Quantity,
LogType: entity.LogTypeTransfer,
LogId: uint(entityTransfer.Id),
Note: "",
ProductWarehouseId: sourcePW.Id,
CreatedBy: 1,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log decrease: %+v", err)
return err
}
// Tambah stok di gudang tujuan
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to get destination product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
}
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
// Jika belum ada record untuk produk di gudang tujuan, buat baru
destPW = &entity.ProductWarehouse{
ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0,
CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create destination product warehouse")
}
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
}
// Update stok di gudang tujuan
destPW.Quantity += product.ProductQty
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
s.Log.Errorf("Failed to update destination product warehouse: %+v", err)
return err
}
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
// create stock log for increase (destination)
beforeDestQty := destPW.Quantity - product.ProductQty
increaseLog := &entity.StockLog{
TransactionType: entity.TransactionTypeIncrease,
Quantity: product.ProductQty,
BeforeQuantity: beforeDestQty,
AfterQuantity: destPW.Quantity,
LogType: entity.LogTypeTransfer,
LogId: uint(entityTransfer.Id),
Note: "",
ProductWarehouseId: destPW.Id,
CreatedBy: 1,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log increase: %+v", err)
return err
}
}
return nil
})
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
}
// Ambil data lengkap hasil create dengan GetOne (agar preload relasi sama dengan GetOne)
result, err := s.GetOne(c, uint(entityTransfer.Id))
if err != nil {
return nil, err
}
return result, nil
}
@@ -0,0 +1,40 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
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"`
}
type TransferProduct struct {
ProductID uint `json:"product_id" validate:"required"`
ProductQty float64 `json:"product_qty" validate:"required,gt=0"`
}
type TransferDeliveryProduct struct {
ProductID uint `json:"product_id" validate:"required"`
ProductQty float64 `json:"product_qty" validate:"required,gt=0"`
}
type TransferDelivery struct {
DeliveryCost float64 `json:"delivery_cost" validate:"required"`
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
DocumentIndex int `json:"document_index" validate:"min=0"`
DriverName string `json:"driver_name" validate:"required"`
VehiclePlate string `json:"vehicle_plate" validate:"required"`
SupplierID uint `json:"supplier_id" validate:"required"`
Products []TransferDeliveryProduct `json:"products" validate:"required,dive"`
}
type TransferRequest struct {
TransferReason string `json:"transfer_reason" validate:"required"`
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
SourceWarehouseID uint `json:"source_warehouse_id" validate:"required"`
DestinationWarehouseID uint `json:"destination_warehouse_id" validate:"required"`
Products []TransferProduct `json:"products" validate:"required,dive"`
Deliveries []TransferDelivery `json:"deliveries" validate:"required,dive"`
}
@@ -0,0 +1,140 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type FlockController struct {
FlockService service.FlockService
}
func NewFlockController(flockService service.FlockService) *FlockController {
return &FlockController{
FlockService: flockService,
}
}
func (u *FlockController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.FlockService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.FlockListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all flocks successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToFlockListDTOs(result),
})
}
func (u *FlockController) 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.FlockService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get flock successfully",
Data: dto.ToFlockListDTO(*result),
})
}
func (u *FlockController) 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.FlockService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create flock successfully",
Data: dto.ToFlockListDTO(*result),
})
}
func (u *FlockController) 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.FlockService.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 flock successfully",
Data: dto.ToFlockListDTO(*result),
})
}
func (u *FlockController) 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.FlockService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete flock successfully",
})
}
@@ -0,0 +1,64 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type FlockBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type FlockListDTO struct {
FlockBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type FlockDetailDTO struct {
FlockListDTO
}
// === Mapper Functions ===
func ToFlockBaseDTO(e entity.Flock) FlockBaseDTO {
return FlockBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToFlockListDTO(e entity.Flock) FlockListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped
}
return FlockListDTO{
FlockBaseDTO: ToFlockBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToFlockListDTOs(e []entity.Flock) []FlockListDTO {
result := make([]FlockListDTO, len(e))
for i, r := range e {
result[i] = ToFlockListDTO(r)
}
return result
}
func ToFlockDetailDTO(e entity.Flock) FlockDetailDTO {
return FlockDetailDTO{
FlockListDTO: ToFlockListDTO(e),
}
}
+25
View File
@@ -0,0 +1,25 @@
package flocks
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
sFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type FlockModule struct{}
func (FlockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
flockRepo := rFlock.NewFlockRepository(db)
userRepo := rUser.NewUserRepository(db)
flockService := sFlock.NewFlockService(flockRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
FlockRoutes(router, userService, flockService)
}
@@ -0,0 +1,30 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type FlockRepository interface {
repository.BaseRepository[entity.Flock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type FlockRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Flock]
db *gorm.DB
}
func NewFlockRepository(db *gorm.DB) FlockRepository {
return &FlockRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Flock](db),
db: db,
}
}
func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID)
}
+28
View File
@@ -0,0 +1,28 @@
package flocks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/controllers"
flock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func FlockRoutes(v1 fiber.Router, u user.UserService, s flock.FlockService) {
ctrl := controller.NewFlockController(s)
route := v1.Group("/flocks")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,159 @@
package service
import (
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/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 FlockService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Flock, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Flock, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Flock, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Flock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type flockService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.FlockRepository
}
func NewFlockService(repo repository.FlockRepository, validate *validator.Validate) FlockService {
return &flockService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s flockService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s flockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Flock, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
flocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get flocks: %+v", err)
return nil, 0, err
}
return flocks, total, nil
}
func (s flockService) GetOne(c *fiber.Ctx, id uint) (*entity.Flock, error) {
flock, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Flock not found")
}
if err != nil {
s.Log.Errorf("Failed get flock by id: %+v", err)
return nil, err
}
return flock, nil
}
func (s *flockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Flock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
name := strings.TrimSpace(req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name is required")
}
exists, err := s.Repository.NameExists(c.Context(), name, nil)
if err != nil {
s.Log.Errorf("Failed to check flock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check flock name")
}
if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Flock with name %s already exists", name))
}
createBody := &entity.Flock{
Name: name,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create flock: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s flockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Flock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name cannot be empty")
}
exists, err := s.Repository.NameExists(c.Context(), name, &id)
if err != nil {
s.Log.Errorf("Failed to check flock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check flock name")
}
if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Flock with name %s already exists", name))
}
updateBody["name"] = 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, "Flock not found")
}
s.Log.Errorf("Failed to update flock: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s flockService) 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, "Flock not found")
}
s.Log.Errorf("Failed to delete flock: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,15 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -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)
@@ -13,6 +13,7 @@ import (
type KandangBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Pic *userDTO.UserBaseDTO `json:"pic"`
}
@@ -46,6 +47,7 @@ func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
return KandangBaseDTO{
Id: e.Id,
Name: e.Name,
Status: e.Status,
Location: location,
Pic: pic,
}
@@ -5,6 +5,7 @@ import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
@@ -13,6 +14,10 @@ type KandangRepository interface {
LocationExists(ctx context.Context, areaId uint) (bool, error)
PicExists(ctx context.Context, areaId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
ProjectFlockExists(ctx context.Context, projectFlockID uint) (bool, error)
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error)
HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error)
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
}
type KandangRepositoryImpl struct {
@@ -38,3 +43,49 @@ func (r *KandangRepositoryImpl) PicExists(ctx context.Context, picId uint) (bool
func (r *KandangRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Kandang](ctx, r.db, name, excludeID)
}
func (r *KandangRepositoryImpl) ProjectFlockExists(ctx context.Context, projectFlockID uint) (bool, error) {
var count int64
if err := r.db.WithContext(ctx).
Model(&entity.ProjectFlock{}).
Where("id = ?", projectFlockID).
Where("deleted_at IS NULL").
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) {
var count int64
q := r.db.WithContext(ctx).
Model(&entity.Kandang{}).
Where("project_flock_id = ?", projectFlockID).
Where("status = ?", utils.KandangStatusActive).
Where("deleted_at IS NULL")
if excludeID != nil {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) {
kandang := new(entity.Kandang)
err := r.db.WithContext(ctx).
Where("project_flock_id = ?", projectFlockID).
First(kandang).Error
if err != nil {
return nil, err
}
return kandang, nil
}
func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error {
return r.db.WithContext(ctx).
Model(&entity.Kandang{}).
Where("project_flock_id = ?", projectFlockID).
Update("status", string(status)).Error
}
@@ -3,6 +3,7 @@ package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -54,6 +55,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")
})
@@ -95,12 +102,44 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err
}
status := strings.ToUpper(strings.TrimSpace(req.Status))
if status == "" {
status = string(utils.KandangStatusNonActive)
}
if !utils.IsValidKandangStatus(status) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status")
}
var projectFlockID *uint
if req.ProjectFlockId != nil {
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
s.Log.Errorf("Failed to check project flock existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check project flock")
} else if !exists {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId))
}
if status == string(utils.KandangStatusActive) {
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *req.ProjectFlockId, nil); err != nil {
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *req.ProjectFlockId, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock")
} else if active {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang")
}
}
idCopy := *req.ProjectFlockId
projectFlockID = &idCopy
}
//TODO: created by dummy
createBody := &entity.Kandang{
Name: req.Name,
LocationId: req.LocationId,
PicId: req.PicId,
CreatedBy: 1,
Name: req.Name,
LocationId: req.LocationId,
Status: status,
PicId: req.PicId,
ProjectFlockId: projectFlockID,
CreatedBy: 1,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -116,6 +155,15 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
return nil, err
}
existing, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
if err != nil {
s.Log.Errorf("Failed to fetch kandang %d before update: %+v", id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang")
}
updateBody := make(map[string]any)
if req.Name != nil {
@@ -143,6 +191,38 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["pic_id"] = *req.PicId
}
finalStatus := strings.ToUpper(existing.Status)
if req.Status != nil {
status := strings.ToUpper(*req.Status)
if !utils.IsValidKandangStatus(status) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status")
}
updateBody["status"] = status
finalStatus = status
}
projectFlockIDToUse := existing.ProjectFlockId
if req.ProjectFlockId != nil {
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
s.Log.Errorf("Failed to check project flock existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check project flock")
} else if !exists {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId))
}
idCopy := *req.ProjectFlockId
projectFlockIDToUse = &idCopy
updateBody["project_flock_id"] = idCopy
}
if projectFlockIDToUse != nil && finalStatus == string(utils.KandangStatusActive) {
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *projectFlockIDToUse, &id); err != nil {
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *projectFlockIDToUse, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock")
} else if active {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang")
}
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
@@ -1,19 +1,25 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
Name string `json:"name" validate:"required_strict,min=3"`
Status string `json:"status,omitempty" validate:"omitempty,min=3"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
Name *string `json:"name,omitempty" validate:"omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
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"`
}
@@ -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)
@@ -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")
})
@@ -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"`
}
@@ -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)
@@ -13,6 +13,7 @@ type ProductRepository interface {
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error)
UomExists(ctx context.Context, uomID uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error)
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
@@ -194,3 +195,7 @@ func (r *ProductRepositoryImpl) GetFlags(ctx context.Context, productID uint) ([
}
return flags, nil
}
func (r *ProductRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.DB(), id)
}
@@ -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")
})

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