mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 07:15:43 +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 TABLE IF EXISTS fcr_standards;
|
||||||
DROP INDEX IF EXISTS suppliers_name_unique;
|
DROP INDEX IF EXISTS suppliers_name_unique;
|
||||||
DROP TABLE IF EXISTS product_suppliers;
|
DROP TABLE IF EXISTS product_suppliers;
|
||||||
@@ -35,4 +40,4 @@ DROP TABLE IF EXISTS fcrs;
|
|||||||
DROP TABLE IF EXISTS projects;
|
DROP TABLE IF EXISTS projects;
|
||||||
DROP INDEX IF EXISTS users_id_user_unique;
|
DROP INDEX IF EXISTS users_id_user_unique;
|
||||||
DROP INDEX IF EXISTS users_email_unique;
|
DROP INDEX IF EXISTS users_email_unique;
|
||||||
DROP TABLE IF EXISTS users;
|
DROP TABLE IF EXISTS users;
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
-- USERS
|
-- USERS
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
id_user BIGINT NOT NULL,
|
id_user BIGINT NOT NULL,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
email VARCHAR NOT NULL,
|
email VARCHAR NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
|
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
|
||||||
@@ -15,221 +15,319 @@ CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL
|
|||||||
|
|
||||||
-- FLAGS
|
-- FLAGS
|
||||||
CREATE TABLE flags (
|
CREATE TABLE flags (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
flagable_id BIGINT NOT NULL,
|
flagable_id BIGINT NOT NULL,
|
||||||
flagable_type VARCHAR(50) NOT NULL,
|
flagable_type VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX flags_unique_flagable ON flags (
|
||||||
|
name,
|
||||||
|
flagable_id,
|
||||||
|
flagable_type
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
|
|
||||||
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
||||||
|
|
||||||
-- PRODUCT CATEGORIES
|
-- PRODUCT CATEGORIES
|
||||||
CREATE TABLE product_categories (
|
CREATE TABLE product_categories (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
code VARCHAR(10) NOT NULL,
|
code VARCHAR(10) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name) WHERE deleted_at IS NULL;
|
|
||||||
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code) WHERE deleted_at IS NULL;
|
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- UOM
|
-- UOM
|
||||||
CREATE TABLE uoms (
|
CREATE TABLE uoms (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- BANKS
|
-- BANKS
|
||||||
CREATE TABLE banks (
|
CREATE TABLE banks (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
alias VARCHAR(5) NOT NULL,
|
alias VARCHAR(5) NOT NULL,
|
||||||
owner VARCHAR,
|
owner VARCHAR,
|
||||||
account_number VARCHAR(50) NOT NULL,
|
account_number VARCHAR(50) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX banks_name_unique ON banks (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX banks_name_unique ON banks (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- AREAS
|
-- AREAS
|
||||||
CREATE TABLE areas (
|
CREATE TABLE areas (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX areas_name_unique ON areas (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX areas_name_unique ON areas (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- LOCATIONS
|
-- LOCATIONS
|
||||||
CREATE TABLE locations (
|
CREATE TABLE locations (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX locations_name_unique ON locations (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX locations_name_unique ON locations (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- KANDANG
|
-- KANDANG
|
||||||
CREATE TABLE kandangs (
|
CREATE TABLE kandangs (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
location_id BIGINT NOT NULL REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- WAREHOUSES
|
-- WAREHOUSES
|
||||||
CREATE TABLE warehouses (
|
CREATE TABLE warehouses (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
location_id BIGINT REFERENCES locations(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
location_id BIGINT REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
kandang_id BIGINT REFERENCES kandangs(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- CUSTOMERS
|
-- CUSTOMERS
|
||||||
CREATE TABLE customers (
|
CREATE TABLE customers (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
phone VARCHAR(20) NOT NULL,
|
phone VARCHAR(20) NOT NULL,
|
||||||
email VARCHAR NOT NULL,
|
email VARCHAR NOT NULL,
|
||||||
account_number VARCHAR(50) NOT NULL,
|
account_number VARCHAR(50) NOT NULL,
|
||||||
balance NUMERIC(15,3) DEFAULT 0,
|
balance NUMERIC(15, 3) DEFAULT 0,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX customers_name_unique ON customers (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX customers_name_unique ON customers (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- NONSTOCK
|
-- NONSTOCK
|
||||||
CREATE TABLE nonstocks (
|
CREATE TABLE nonstocks (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
-- FCR
|
-- FCR
|
||||||
CREATE TABLE fcrs (
|
CREATE TABLE fcrs (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
CREATE TABLE fcr_standards (
|
CREATE TABLE fcr_standards (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
fcr_id BIGINT NOT NULL REFERENCES fcrs(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
weight NUMERIC(15,3) NOT NULL,
|
weight NUMERIC(15, 3) NOT NULL,
|
||||||
fcr_number NUMERIC(15,3) NOT NULL,
|
fcr_number NUMERIC(15, 3) NOT NULL,
|
||||||
mortality NUMERIC(15,3) NOT NULL,
|
mortality NUMERIC(15, 3) NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
-- SUPPLIERS
|
-- SUPPLIERS
|
||||||
CREATE TABLE suppliers (
|
CREATE TABLE suppliers (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
alias VARCHAR(5) NOT NULL,
|
alias VARCHAR(5) NOT NULL,
|
||||||
pic VARCHAR NOT NULL,
|
pic VARCHAR NOT NULL,
|
||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
category VARCHAR(20) NOT NULL,
|
category VARCHAR(20) NOT NULL,
|
||||||
hatchery VARCHAR,
|
hatchery VARCHAR,
|
||||||
phone VARCHAR(20) NOT NULL,
|
phone VARCHAR(20) NOT NULL,
|
||||||
email VARCHAR NOT NULL,
|
email VARCHAR NOT NULL,
|
||||||
address TEXT NOT NULL,
|
address TEXT NOT NULL,
|
||||||
npwp VARCHAR(50),
|
npwp VARCHAR(50),
|
||||||
account_number VARCHAR(50),
|
account_number VARCHAR(50),
|
||||||
balance NUMERIC(15,3) DEFAULT 0,
|
balance NUMERIC(15, 3) DEFAULT 0,
|
||||||
due_date INT NOT NULL,
|
due_date INT NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL;
|
|
||||||
|
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
CREATE TABLE nonstock_suppliers (
|
CREATE TABLE nonstock_suppliers (
|
||||||
nonstock_id BIGINT NOT NULL REFERENCES nonstocks(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
nonstock_id BIGINT NOT NULL REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
PRIMARY KEY (nonstock_id, supplier_id)
|
PRIMARY KEY (nonstock_id, supplier_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- PRODUCTS
|
-- PRODUCTS
|
||||||
CREATE TABLE products (
|
CREATE TABLE products (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
brand VARCHAR NOT NULL,
|
brand VARCHAR NOT NULL,
|
||||||
sku VARCHAR(100),
|
sku VARCHAR(100),
|
||||||
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
product_category_id BIGINT NOT NULL REFERENCES product_categories(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
product_price NUMERIC(15,3) NOT NULL,
|
product_price NUMERIC(15, 3) NOT NULL,
|
||||||
selling_price NUMERIC(15,3),
|
selling_price NUMERIC(15, 3),
|
||||||
tax NUMERIC(15,3),
|
tax NUMERIC(15, 3),
|
||||||
expiry_period INT,
|
expiry_period INT,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
|
|
||||||
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
|
CREATE UNIQUE INDEX products_name_unique ON products (name)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX products_sku_unique ON products (sku)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
CREATE TABLE product_suppliers (
|
CREATE TABLE product_suppliers (
|
||||||
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
product_id BIGINT NOT NULL REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
PRIMARY KEY (product_id, supplier_id)
|
PRIMARY KEY (product_id, supplier_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- PROJECTS
|
-- PROJECTS
|
||||||
CREATE TABLE projects (
|
CREATE TABLE projects (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- PRODUCT WAREHOUSES TABLE
|
||||||
|
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"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -35,7 +36,27 @@ func Run(db *gorm.DB) error {
|
|||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -44,11 +65,6 @@ func Run(db *gorm.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
productCategories, err := seedProductCategories(tx, adminID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
suppliers, err := seedSuppliers(tx, adminID)
|
suppliers, err := seedSuppliers(tx, adminID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -58,10 +74,6 @@ func Run(db *gorm.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := seedFcr(tx, adminID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
|
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -74,6 +86,17 @@ func Run(db *gorm.DB) error {
|
|||||||
return err
|
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")
|
fmt.Println("✅ Master data seeding completed")
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -190,16 +213,190 @@ func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[stri
|
|||||||
return result, nil
|
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 {
|
seeds := []struct {
|
||||||
Name string
|
Key string
|
||||||
|
Flock string
|
||||||
|
Area string
|
||||||
|
Category utils.ProjectFlockCategory
|
||||||
|
Fcr string
|
||||||
Location string
|
Location string
|
||||||
PicKey string
|
Period int
|
||||||
}{
|
}{
|
||||||
{"Singaparna 1", "Singaparna", "admin"},
|
{
|
||||||
{"Singaparna 2", "Singaparna", "admin"},
|
Key: "Singaparna Period 1",
|
||||||
{"Cikaum 1", "Cikaum", "admin"},
|
Flock: "Flock Priangan",
|
||||||
{"Cikaum 2", "Cikaum", "admin"},
|
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))
|
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)
|
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
|
var kandang entity.Kandang
|
||||||
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
|
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
kandang = entity.Kandang{
|
kandang = entity.Kandang{
|
||||||
Name: seed.Name,
|
Name: seed.Name,
|
||||||
LocationId: locID,
|
Status: string(seed.Status),
|
||||||
PicId: picID,
|
LocationId: locID,
|
||||||
CreatedBy: createdBy,
|
PicId: picID,
|
||||||
|
ProjectFlockId: projectFlockID,
|
||||||
|
CreatedBy: createdBy,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&kandang).Error; err != nil {
|
if err := tx.Create(&kandang).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
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
|
result[seed.Name] = kandang.Id
|
||||||
}
|
}
|
||||||
@@ -235,6 +463,38 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
return result, nil
|
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 {
|
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
|
||||||
seeds := []struct {
|
seeds := []struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -426,7 +686,7 @@ func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedFcr(tx *gorm.DB, createdBy uint) error {
|
func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||||
seeds := []struct {
|
seeds := []struct {
|
||||||
Name string
|
Name string
|
||||||
Standards []struct {
|
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 {
|
for _, seed := range seeds {
|
||||||
var fcr entity.Fcr
|
var fcr entity.Fcr
|
||||||
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
|
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
|
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
|
||||||
if err := tx.Create(&fcr).Error; err != nil {
|
if err := tx.Create(&fcr).Error; err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
result[seed.Name] = fcr.Id
|
||||||
|
|
||||||
for _, std := range seed.Standards {
|
for _, std := range seed.Standards {
|
||||||
var standard entity.FcrStandard
|
var standard entity.FcrStandard
|
||||||
@@ -471,22 +734,22 @@ func seedFcr(tx *gorm.DB, createdBy uint) error {
|
|||||||
Mortality: std.Mortality,
|
Mortality: std.Mortality,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&standard).Error; err != nil {
|
if err := tx.Create(&standard).Error; err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
|
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
|
||||||
"fcr_number": std.FcrNumber,
|
"fcr_number": std.FcrNumber,
|
||||||
"mortality": std.Mortality,
|
"mortality": std.Mortality,
|
||||||
}).Error; err != nil {
|
}).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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nanti saya isi
|
||||||
|
|
||||||
func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error {
|
func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error {
|
||||||
if len(flags) == 0 {
|
if len(flags) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -760,6 +1025,261 @@ func seedBanks(tx *gorm.DB, createdBy uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
||||||
|
|
||||||
|
seeds := []struct {
|
||||||
|
ProductID uint
|
||||||
|
WarehouseID uint
|
||||||
|
Quantity float64
|
||||||
|
}{
|
||||||
|
{ProductID: 1, WarehouseID: 1, Quantity: 100},
|
||||||
|
{ProductID: 2, WarehouseID: 2, Quantity: 200},
|
||||||
|
{ProductID: 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 {
|
func ptr[T any](v T) *T {
|
||||||
return &v
|
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 {
|
type Kandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
||||||
LocationId uint `gorm:"not null"`
|
Status string `gorm:"type:varchar(50);not null"`
|
||||||
PicId uint `gorm:"not null"`
|
LocationId uint `gorm:"not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
PicId uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
ProjectFlockId *uint `gorm:"column:project_flock_id"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Pic User `gorm:"foreignKey:PicId;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
|
package middleware
|
||||||
|
|
||||||
import (
|
// import (
|
||||||
"strings"
|
// "strings"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
// "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"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
// "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
|
||||||
"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 {
|
// func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
// return func(c *fiber.Ctx) error {
|
||||||
authHeader := c.Get("Authorization")
|
// authHeader := c.Get("Authorization")
|
||||||
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
// token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
||||||
|
|
||||||
if token == "" {
|
// if token == "" {
|
||||||
cookieName := config.SSOAccessCookieName
|
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
if cookieName == "" {
|
// }
|
||||||
cookieName = "access"
|
|
||||||
}
|
|
||||||
token = strings.TrimSpace(c.Cookies(cookieName))
|
|
||||||
}
|
|
||||||
|
|
||||||
if token == "" {
|
// userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
// if err != nil {
|
||||||
}
|
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
// }
|
||||||
|
|
||||||
verification, err := sso.VerifyAccessToken(token)
|
// // Only end-user subjects are allowed by this middleware. Service tokens
|
||||||
if err != nil {
|
// if verification.UserID == 0 {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if len(config.SSOAllowedAudiences) > 0 {
|
// // Fail-closed on revocation check errors for stricter security posture.
|
||||||
allowed := make(map[string]struct{}, len(config.SSOAllowedAudiences))
|
// if revoker := session.GetRevocationStore(); revoker != nil {
|
||||||
for _, aud := range config.SSOAllowedAudiences {
|
// if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
|
||||||
aud = strings.TrimSpace(aud)
|
// revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
|
||||||
if aud != "" {
|
// if err != nil {
|
||||||
allowed[aud] = struct{}{}
|
// utils.Log.WithError(err).Warn("failed to check token revocation")
|
||||||
}
|
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
}
|
// }
|
||||||
audienceValid := false
|
// if revoked {
|
||||||
for _, aud := range verification.Claims.Audience {
|
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
if _, ok := allowed[aud]; ok {
|
// }
|
||||||
audienceValid = true
|
// }
|
||||||
break
|
// }
|
||||||
}
|
|
||||||
}
|
|
||||||
if !audienceValid {
|
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "invalid audience")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if revoker := session.GetRevocationStore(); revoker != nil {
|
// user, err := userService.GetBySSOUserID(c, verification.UserID)
|
||||||
logoutAt, err := revoker.UserLogoutTime(c.Context(), verification.UserID)
|
// if err != nil || user == nil {
|
||||||
if err != nil {
|
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
|
// if len(requiredRights) > 0 && verification.Claims != nil {
|
||||||
revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
|
// if !hasAllScopes(verification.Claims.Scopes(), requiredRights) {
|
||||||
if err != nil {
|
// return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := userService.GetBySSOUserID(c, verification.UserID)
|
// c.Locals("user", user)
|
||||||
if err != nil || user == nil {
|
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Locals("user", user)
|
// // if len(requiredRights) > 0 {
|
||||||
c.Locals("token_claims", verification.Claims)
|
// // 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 {
|
// return c.Next()
|
||||||
// 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{}{}
|
|
||||||
// }
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// for _, right := range requiredRights {
|
// // bearerToken extracts a Bearer token from the Authorization header using
|
||||||
// if _, exists := rightSet[right]; !exists {
|
// // 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
|
// 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
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,6 +30,50 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
|
|||||||
for f := range utils.AllFlagTypes() {
|
for f := range utils.AllFlagTypes() {
|
||||||
flagList = append(flagList, string(f))
|
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{}{
|
return map[string]interface{}{
|
||||||
"flags": flagList,
|
"flags": flagList,
|
||||||
@@ -42,5 +90,6 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
|
|||||||
"BISNIS",
|
"BISNIS",
|
||||||
"INDIVIDUAL",
|
"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 {
|
func (u *KandangController) GetAll(c *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
|
LocationId: c.QueryInt("location_id", 0),
|
||||||
|
PicId: c.QueryInt("pic_id", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.KandangService.GetAll(c, query)
|
result, totalResults, err := u.KandangService.GetAll(c, query)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
type KandangBaseDTO struct {
|
type KandangBaseDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
Location *locationDTO.LocationBaseDTO `json:"location"`
|
Location *locationDTO.LocationBaseDTO `json:"location"`
|
||||||
Pic *userDTO.UserBaseDTO `json:"pic"`
|
Pic *userDTO.UserBaseDTO `json:"pic"`
|
||||||
}
|
}
|
||||||
@@ -46,6 +47,7 @@ func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
|
|||||||
return KandangBaseDTO{
|
return KandangBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
|
Status: e.Status,
|
||||||
Location: location,
|
Location: location,
|
||||||
Pic: pic,
|
Pic: pic,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +14,10 @@ type KandangRepository interface {
|
|||||||
LocationExists(ctx context.Context, areaId uint) (bool, error)
|
LocationExists(ctx context.Context, areaId uint) (bool, error)
|
||||||
PicExists(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)
|
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 {
|
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) {
|
func (r *KandangRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||||
return repository.ExistsByName[entity.Kandang](ctx, r.db, name, excludeID)
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
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 != "" {
|
if params.Search != "" {
|
||||||
return db.Where("name LIKE ?", "%"+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")
|
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
|
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
|
//TODO: created by dummy
|
||||||
createBody := &entity.Kandang{
|
createBody := &entity.Kandang{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
LocationId: req.LocationId,
|
LocationId: req.LocationId,
|
||||||
PicId: req.PicId,
|
Status: status,
|
||||||
CreatedBy: 1,
|
PicId: req.PicId,
|
||||||
|
ProjectFlockId: projectFlockID,
|
||||||
|
CreatedBy: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
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
|
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)
|
updateBody := make(map[string]any)
|
||||||
|
|
||||||
if req.Name != nil {
|
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
|
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 {
|
if len(updateBody) == 0 {
|
||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `json:"name" validate:"required_strict,min=3"`
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
Status string `json:"status,omitempty" validate:"omitempty,min=3"`
|
||||||
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
|
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 {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
|
||||||
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
|
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
|
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
|
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
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),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
|
AreaId: c.QueryInt("area_id", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.LocationService.GetAll(c, query)
|
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 != "" {
|
if params.Search != "" {
|
||||||
db = db.Where("name LIKE ?", "%"+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")
|
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"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
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 {
|
func (u *ProductController) GetAll(c *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
|
ProductCategoryID: c.QueryInt("product_category_id", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.ProductService.GetAll(c, query)
|
result, totalResults, err := u.ProductService.GetAll(c, query)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type ProductRepository interface {
|
|||||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||||
SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error)
|
SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error)
|
||||||
UomExists(ctx context.Context, uomID 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)
|
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
|
||||||
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
|
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
|
||||||
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) 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
|
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 != "" {
|
if params.Search != "" {
|
||||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
|
if params.ProductCategoryID != 0 {
|
||||||
|
return db.Where("product_category_id = ?", params.ProductCategoryID)
|
||||||
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user