mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development-after-rebase' into 'development'
chore(REBASE): Development with SSO See merge request mbugroup/lti-api!40
This commit is contained in:
@@ -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);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- DROP PIVOT TABLE: STOCK_TRANSFER_DELIVERY_ITEMS
|
||||
DROP TABLE IF EXISTS stock_transfer_delivery_items CASCADE;
|
||||
+35
@@ -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);
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS project_flock_populations;
|
||||
+36
@@ -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);
|
||||
+25
@@ -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;
|
||||
+43
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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:"-"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+75
@@ -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)
|
||||
}
|
||||
|
||||
+74
@@ -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
|
||||
}
|
||||
+20
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+22
@@ -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),
|
||||
}
|
||||
}
|
||||
+22
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user