diff --git a/internal/common/repository/common.approval.repository..go b/internal/common/repository/common.approval.repository..go new file mode 100644 index 00000000..7f1c27ae --- /dev/null +++ b/internal/common/repository/common.approval.repository..go @@ -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 +} diff --git a/internal/common/repository/repository.go b/internal/common/repository/common.base.repository.go similarity index 100% rename from internal/common/repository/repository.go rename to internal/common/repository/common.base.repository.go diff --git a/internal/common/repository/helpers.go b/internal/common/repository/common.exists.repository.go similarity index 100% rename from internal/common/repository/helpers.go rename to internal/common/repository/common.exists.repository.go diff --git a/internal/common/service/common.approval.service.go b/internal/common/service/common.approval.service.go new file mode 100644 index 00000000..569a7cc6 --- /dev/null +++ b/internal/common/service/common.approval.service.go @@ -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 +} diff --git a/internal/common/service/relation.go b/internal/common/service/common.relation.service.go similarity index 100% rename from internal/common/service/relation.go rename to internal/common/service/common.relation.service.go diff --git a/internal/common/validation/custom_validation.go b/internal/common/validation/common.custom.validation.go similarity index 100% rename from internal/common/validation/custom_validation.go rename to internal/common/validation/common.custom.validation.go diff --git a/internal/database/migrations/20251015162158_create_approvals_table.down.sql b/internal/database/migrations/20251015162158_create_approvals_table.down.sql new file mode 100644 index 00000000..0ad38d2b --- /dev/null +++ b/internal/database/migrations/20251015162158_create_approvals_table.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS approvals_approvable_lookup; +DROP TABLE IF EXISTS approvals; diff --git a/internal/database/migrations/20251015162158_create_approvals_table.up.sql b/internal/database/migrations/20251015162158_create_approvals_table.up.sql new file mode 100644 index 00000000..50154f33 --- /dev/null +++ b/internal/database/migrations/20251015162158_create_approvals_table.up.sql @@ -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); diff --git a/internal/database/migrations/20251017071755_rename_approval_status_to_action.down.sql b/internal/database/migrations/20251017071755_rename_approval_status_to_action.down.sql new file mode 100644 index 00000000..cca2f08b --- /dev/null +++ b/internal/database/migrations/20251017071755_rename_approval_status_to_action.down.sql @@ -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; diff --git a/internal/database/migrations/20251017071755_rename_approval_status_to_action.up.sql b/internal/database/migrations/20251017071755_rename_approval_status_to_action.up.sql new file mode 100644 index 00000000..4d27cd27 --- /dev/null +++ b/internal/database/migrations/20251017071755_rename_approval_status_to_action.up.sql @@ -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; diff --git a/internal/database/migrations/20251018120649_create_project_chick_ins_table.down.sql b/internal/database/migrations/20251018120649_create_project_chick_ins_table.down.sql new file mode 100644 index 00000000..bb8f8a2d --- /dev/null +++ b/internal/database/migrations/20251018120649_create_project_chick_ins_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS project_chickins; \ No newline at end of file diff --git a/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql b/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql new file mode 100644 index 00000000..25d3476d --- /dev/null +++ b/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql @@ -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); \ No newline at end of file diff --git a/internal/database/migrations/20251020022357_create_project_flock_populations_table.down.sql b/internal/database/migrations/20251020022357_create_project_flock_populations_table.down.sql new file mode 100644 index 00000000..8fa11576 --- /dev/null +++ b/internal/database/migrations/20251020022357_create_project_flock_populations_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS project_flock_populations; \ No newline at end of file diff --git a/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql b/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql new file mode 100644 index 00000000..82b3e9a7 --- /dev/null +++ b/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql @@ -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); \ No newline at end of file diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index f718cde9..32c3b310 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -8,6 +8,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "gorm.io/gorm" ) @@ -92,6 +93,9 @@ func Run(db *gorm.DB) error { if err := seedTransferStock(tx, adminID); err != nil { return err } + if err := seedChickin(tx, adminID); err != nil { + return err + } fmt.Println("✅ Master data seeding completed") return nil @@ -319,12 +323,68 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio 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 @@ -334,9 +394,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users 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", 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.KandangStatusPengajuan, Location: "Cikaum", PicKey: "admin"}, + {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"}, } result := make(map[string]uint, len(seeds)) @@ -975,6 +1035,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error { {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 { @@ -999,8 +1060,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error { } func seedTransferStock(tx *gorm.DB, createdBy uint) error { - // Seeder Transfer Stock - // 1. Insert StockTransfer (header) + transfer := entity.StockTransfer{ FromWarehouseId: 1, ToWarehouseId: 2, @@ -1013,7 +1073,6 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error { return err } - // 2. Insert StockTransferDetail (detail) details := []entity.StockTransferDetail{ { StockTransferId: transfer.Id, @@ -1032,7 +1091,6 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error { } } - // 3. Insert StockTransferDelivery (delivery) deliveries := []entity.StockTransferDelivery{ { StockTransferId: transfer.Id, @@ -1076,6 +1134,75 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error { 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 + } + + // Update/Insert ProjectFlockPopulation + 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 + } + } + } + + return nil +} + func ptr[T any](v T) *T { return &v } diff --git a/internal/entities/approval.go b/internal/entities/approval.go new file mode 100644 index 00000000..87dc7b0a --- /dev/null +++ b/internal/entities/approval.go @@ -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"` +} diff --git a/internal/entities/audit_log.go b/internal/entities/audit_log.go new file mode 100644 index 00000000..3b770125 --- /dev/null +++ b/internal/entities/audit_log.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" +) + +type AuditLog struct { + Id uint `gorm:"primaryKey"` + TableName string `gorm:"size:100;not null"` + RecordId uint `gorm:"not null"` + Action string `gorm:"size:30;not null"` + BeforeData string `gorm:"type:jsonb"` + AfterData string `gorm:"type:jsonb"` + ChangedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + + User *User `gorm:"foreignKey:ChangedBy;references:Id"` +} diff --git a/internal/entities/project_chickin.go b/internal/entities/project_chickin.go new file mode 100644 index 00000000..95a658c8 --- /dev/null +++ b/internal/entities/project_chickin.go @@ -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"` +} diff --git a/internal/entities/project_flock_population.go b/internal/entities/project_flock_population.go new file mode 100644 index 00000000..184ace65 --- /dev/null +++ b/internal/entities/project_flock_population.go @@ -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"` +} diff --git a/internal/entities/projectfloc.go b/internal/entities/projectfloc.go deleted file mode 100644 index 47362d42..00000000 --- a/internal/entities/projectfloc.go +++ /dev/null @@ -1,28 +0,0 @@ -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"` -} diff --git a/internal/entities/projectflock.go b/internal/entities/projectflock.go new file mode 100644 index 00000000..c840892f --- /dev/null +++ b/internal/entities/projectflock.go @@ -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:"-"` +} diff --git a/internal/entities/stock_availabilites.go b/internal/entities/stock_availabilites.go new file mode 100644 index 00000000..ec24d36b --- /dev/null +++ b/internal/entities/stock_availabilites.go @@ -0,0 +1,26 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +const ( + EntityTypeProjectFlockKandang = "PROJECT_FLOCK_KANDANG" +) + +type StockAvailability struct { + Id uint `gorm:"primaryKey"` + EntityType string `gorm:"size:50;not null"` + EntityId uint `gorm:"not null"` + ProductId uint `gorm:"not null"` + Quantity float64 `gorm:"not null;default:0"` + ReservedQuantity float64 `gorm:"not null;default:0"` + Unit string `gorm:"size:20"` + LastUpdated time.Time `gorm:"autoUpdateTime"` + CreatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Product *Product `gorm:"foreignKey:ProductId;references:Id"` +} diff --git a/internal/entities/stock_log.go b/internal/entities/stock_log.go index 21e86bd4..6546e790 100644 --- a/internal/entities/stock_log.go +++ b/internal/entities/stock_log.go @@ -8,6 +8,7 @@ import ( const ( LogTypeAdjustment = "ADJUSTMENT" + LogTypeTransfer = "TRANSFER" ) const ( diff --git a/internal/modules/approvals/controllers/approval.controller.go b/internal/modules/approvals/controllers/approval.controller.go new file mode 100644 index 00000000..fd0baa6e --- /dev/null +++ b/internal/modules/approvals/controllers/approval.controller.go @@ -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, + }) +} diff --git a/internal/modules/approvals/dto/approval.dto.go b/internal/modules/approvals/dto/approval.dto.go new file mode 100644 index 00000000..085c367c --- /dev/null +++ b/internal/modules/approvals/dto/approval.dto.go @@ -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 +} diff --git a/internal/modules/approvals/module.go b/internal/modules/approvals/module.go new file mode 100644 index 00000000..8cf52f73 --- /dev/null +++ b/internal/modules/approvals/module.go @@ -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) +} diff --git a/internal/modules/approvals/route.go b/internal/modules/approvals/route.go new file mode 100644 index 00000000..b7d66abd --- /dev/null +++ b/internal/modules/approvals/route.go @@ -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) +} diff --git a/internal/modules/approvals/validations/approval.validation.go b/internal/modules/approvals/validations/approval.validation.go new file mode 100644 index 00000000..7338550e --- /dev/null +++ b/internal/modules/approvals/validations/approval.validation.go @@ -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"` +} diff --git a/internal/modules/constants/repositories/constant.repository.go b/internal/modules/constants/repositories/constant.repository.go index 7b85ce20..4b44d553 100644 --- a/internal/modules/constants/repositories/constant.repository.go +++ b/internal/modules/constants/repositories/constant.repository.go @@ -1,9 +1,13 @@ package repository import ( + "sort" + "strconv" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" utils "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "gorm.io/gorm" ) @@ -26,6 +30,50 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} { for f := range utils.AllFlagTypes() { flagList = append(flagList, string(f)) } + sort.Strings(flagList) + + type approvalStepConstant struct { + StepNumber uint16 `json:"step_number"` + StepName string `json:"step_name"` + } + + workflowConstants := approvalutils.WorkflowConstants() + workflowKeys := make([]string, 0, len(workflowConstants)) + for key := range workflowConstants { + workflowKeys = append(workflowKeys, key) + } + sort.Strings(workflowKeys) + + approvalWorkflows := make([]map[string]interface{}, 0, len(workflowKeys)) + for _, key := range workflowKeys { + stepMap := workflowConstants[key] + if len(stepMap) == 0 { + continue + } + + stepList := make([]approvalStepConstant, 0, len(stepMap)) + for stepStr, label := range stepMap { + stepNum, err := strconv.ParseUint(stepStr, 10, 16) + if err != nil || stepNum == 0 { + continue + } + stepList = append(stepList, approvalStepConstant{ + StepNumber: uint16(stepNum), + StepName: label, + }) + } + if len(stepList) == 0 { + continue + } + sort.Slice(stepList, func(i, j int) bool { + return stepList[i].StepNumber < stepList[j].StepNumber + }) + + approvalWorkflows = append(approvalWorkflows, map[string]interface{}{ + "key": key, + "steps": stepList, + }) + } return map[string]interface{}{ "flags": flagList, @@ -42,5 +90,6 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} { "BISNIS", "INDIVIDUAL", }, + "approval_workflows": approvalWorkflows, } } diff --git a/internal/modules/inventory/adjustments/controllers/adjustment.controller.go b/internal/modules/inventory/adjustments/controllers/adjustment.controller.go index dc3df0a9..617a1b5f 100644 --- a/internal/modules/inventory/adjustments/controllers/adjustment.controller.go +++ b/internal/modules/inventory/adjustments/controllers/adjustment.controller.go @@ -49,8 +49,8 @@ func (u *AdjustmentController) AdjustmentHistory(c *fiber.Ctx) error { query := &validation.Query{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), - ProductID: c.QueryInt("product_id", 0), - WarehouseID: c.QueryInt("warehouse_id", 0), + ProductID: uint(c.QueryInt("product_id", 0)), + WarehouseID: uint(c.QueryInt("warehouse_id", 0)), TransactionType: c.Query("transaction_type", ""), } diff --git a/internal/modules/inventory/adjustments/module.go b/internal/modules/inventory/adjustments/module.go index cfe01118..b3e12676 100644 --- a/internal/modules/inventory/adjustments/module.go +++ b/internal/modules/inventory/adjustments/module.go @@ -9,7 +9,7 @@ import ( 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/stock-logs/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" diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 929a5c8a..7a2d06bc 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -4,12 +4,14 @@ 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/stock-logs/repositories" + stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" @@ -77,22 +79,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } ctx := c.Context() - isProductExist, err := s.ProductRepo.IdExists(c.Context(), uint(req.ProductID)) - if err != nil { - s.Log.Errorf("Failed to check product existence: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product") - } - if !isProductExist { - return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found") - } - - isWarehouseExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(req.WarehouseID)) - if err != nil { - s.Log.Errorf("Failed to check warehouse existence: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse") - } - if !isWarehouseExist { - return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not found") + 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 { @@ -118,6 +109,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e 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") @@ -126,7 +118,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } 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) @@ -159,14 +150,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e s.Log.Errorf("Failed to create stock log: %+v", err) return err } - s.Log.Infof("Stock log created: %+v", newLog.Id) productWarehouse.Quantity = afterQuantity if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil { s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) return err } - s.Log.Infof("Product warehouse quantity updated: %+v", productWarehouse.Id) createdLogId = newLog.Id return nil @@ -184,9 +173,26 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu 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) diff --git a/internal/modules/inventory/adjustments/validations/adjustment.validation.go b/internal/modules/inventory/adjustments/validations/adjustment.validation.go index 7d2385cc..2e7259f2 100644 --- a/internal/modules/inventory/adjustments/validations/adjustment.validation.go +++ b/internal/modules/inventory/adjustments/validations/adjustment.validation.go @@ -11,7 +11,7 @@ type Create struct { type Query struct { Page int `query:"page" validate:"omitempty,min=1"` Limit int `query:"limit" validate:"omitempty,min=1,max=100"` - ProductID int `query:"product_id" validate:"omitempty,min=0"` - WarehouseID int `query:"warehouse_id" validate:"omitempty,min=0"` + 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"` } diff --git a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go index fdebb519..8c9f3846 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -4,7 +4,6 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs === @@ -18,11 +17,16 @@ type ProductWarehouseBaseDTO struct { type ProductWarehouseListDTO struct { ProductWarehouseBaseDTO - Product *ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + 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 { @@ -38,6 +42,24 @@ type ProductBaseDTO struct { } 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"` } @@ -69,7 +91,6 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT if e.Product.Sku != nil { product.Sku = *e.Product.Sku } - // Map flags from Product relation if len(e.Product.Flags) > 0 { for _, f := range e.Product.Flags { product.Flags = append(product.Flags, f.Name) @@ -84,12 +105,37 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT 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 := userDTO.ToUserBaseDTO(e.CreatedUser) + user := UserBaseDTO{ + Id: e.CreatedUser.Id, + Username: e.CreatedUser.Name, + } dto.CreatedUser = &user } @@ -109,3 +155,24 @@ func ToProductWarehouseDetailDTO(e entity.ProductWarehouse) ProductWarehouseDeta 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, + } +} diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 9afe5707..4fad5dc5 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -34,7 +34,14 @@ func NewProductWarehouseService(repo repository.ProductWarehouseRepository, vali } func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("Product.Flags").Preload("Product").Preload("Warehouse").Preload("CreatedUser") + 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) { diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index 217e5038..cb85af94 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -23,6 +23,11 @@ type WarehouseSimpleDTO struct { 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"` @@ -33,6 +38,11 @@ type LocationDTO struct { 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"` @@ -57,17 +67,15 @@ type TransferDetailDTO struct { // Detail produk type TransferDetailItemDTO struct { - Id uint64 `json:"id"` - ProductId uint64 `json:"product_id"` - Quantity float64 `json:"quantity"` - BeforeQuantity float64 `json:"before_quantity"` - AfterQuantity float64 `json:"after_quantity"` + Id uint64 `json:"id"` + Proudct ProductSimpleDTO `json:"product"` + Quantity float64 `json:"quantity"` } // Delivery ekspedisi type TransferDeliveryDTO struct { Id uint64 `json:"id"` - SupplierId uint64 `json:"supplier_id"` + Supplier SupplierSimpleDTO `json:"supplier"` VehiclePlate string `json:"vehicle_plate"` DriverName string `json:"driver_name"` DocumentNumber string `json:"document_number"` @@ -146,9 +154,12 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { var details []TransferDetailItemDTO for _, d := range e.Details { details = append(details, TransferDetailItemDTO{ - Id: d.Id, - ProductId: d.ProductId, - Quantity: d.Quantity, + Id: d.Id, + Proudct: ProductSimpleDTO{ + Id: d.Product.Id, + Name: d.Product.Name, + }, + Quantity: d.Quantity, }) } // Map deliveries @@ -164,8 +175,11 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { }) } deliveries = append(deliveries, TransferDeliveryDTO{ - Id: del.Id, - SupplierId: del.SupplierId, + Id: del.Id, + Supplier: SupplierSimpleDTO{ + Id: del.Supplier.Id, + Name: del.Supplier.Name, + }, VehiclePlate: del.VehiclePlate, DriverName: del.DriverName, DocumentNumber: del.DocumentNumber, @@ -198,17 +212,23 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { var details []TransferDetailItemDTO for _, d := range e.Details { details = append(details, TransferDetailItemDTO{ - Id: d.Id, - ProductId: d.ProductId, - Quantity: d.Quantity, + 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, - SupplierId: del.SupplierId, + Id: del.Id, + Supplier: SupplierSimpleDTO{ + Id: del.Supplier.Id, + Name: del.Supplier.Name, + }, VehiclePlate: del.VehiclePlate, DriverName: del.DriverName, DocumentNumber: del.DocumentNumber, diff --git a/internal/modules/inventory/transfers/module.go b/internal/modules/inventory/transfers/module.go index 21f0ec89..734f0f03 100644 --- a/internal/modules/inventory/transfers/module.go +++ b/internal/modules/inventory/transfers/module.go @@ -9,7 +9,7 @@ import ( 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/stock-logs/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" ) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 7f18d257..90642f6c 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -10,7 +10,7 @@ import ( 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/stock-logs/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" @@ -60,7 +60,9 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB { Preload("ToWarehouse.Location"). Preload("ToWarehouse.Area"). Preload("Details"). - Preload("Deliveries.Items") + Preload("Details.Product"). + Preload("Deliveries.Items"). + Preload("Deliveries.Supplier") } func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) { @@ -208,7 +210,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques SupplierId: uint64(delivery.SupplierID), VehiclePlate: delivery.VehiclePlate, DriverName: delivery.DriverName, - DocumentPath: "dummy duls", // todo: tunggu ada aws baru proses + 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, }) diff --git a/internal/modules/master/kandangs/repositories/kandang.repository.go b/internal/modules/master/kandangs/repositories/kandang.repository.go index b253fade..22546339 100644 --- a/internal/modules/master/kandangs/repositories/kandang.repository.go +++ b/internal/modules/master/kandangs/repositories/kandang.repository.go @@ -15,7 +15,9 @@ type KandangRepository interface { PicExists(ctx context.Context, areaId uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) ProjectFlockExists(ctx context.Context, projectFlockID uint) (bool, error) + GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) + UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error } type KandangRepositoryImpl struct { @@ -69,3 +71,21 @@ func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Cont } 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 +} diff --git a/internal/modules/master/warehouses/repositories/warehouse.repository.go b/internal/modules/master/warehouses/repositories/warehouse.repository.go index 5c791e01..956c30ef 100644 --- a/internal/modules/master/warehouses/repositories/warehouse.repository.go +++ b/internal/modules/master/warehouses/repositories/warehouse.repository.go @@ -15,6 +15,7 @@ type WarehouseRepository interface { KandangExists(ctx context.Context, kandangId uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error) + GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) } type WarehouseRepositoryImpl struct { @@ -47,3 +48,15 @@ func (r *WarehouseRepositoryImpl) NameExists(ctx context.Context, name string, e func (r *WarehouseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { return repository.Exists[entity.Warehouse](ctx, r.db, id) } + +func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) { + var warehouse entity.Warehouse + err := r.db.WithContext(ctx). + Where("kandang_id = ?", kandangId). + Where("deleted_at IS NULL"). + First(&warehouse).Error + if err != nil { + return nil, err + } + return &warehouse, nil +} diff --git a/internal/modules/production/chickins/controllers/chickin.controller.go b/internal/modules/production/chickins/controllers/chickin.controller.go new file mode 100644 index 00000000..fadcbc3e --- /dev/null +++ b/internal/modules/production/chickins/controllers/chickin.controller.go @@ -0,0 +1,161 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type ChickinController struct { + ChickinService service.ChickinService +} + +func NewChickinController(chickinService service.ChickinService) *ChickinController { + return &ChickinController{ + ChickinService: chickinService, + } +} + +func (u *ChickinController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)), + } + + result, totalResults, err := u.ChickinService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all chickins successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToChickinListDTOs(result), + }) +} + +func (u *ChickinController) 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.ChickinService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get chickin successfully", + Data: dto.ToChickinListDTO(*result), + }) +} + +func (u *ChickinController) 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.ChickinService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create chickin successfully", + Data: dto.ToChickinListDTO(*result), + }) +} + +func (u *ChickinController) 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.ChickinService.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 chickin successfully", + Data: dto.ToChickinListDTO(*result), + }) +} + +func (u *ChickinController) 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.ChickinService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete chickin successfully", + }) +} + +func (u *ChickinController) Approve(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.ChickinService.Approve(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Approve chickin successfully", + Data: nil, + }) +} diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go new file mode 100644 index 00000000..193257b6 --- /dev/null +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -0,0 +1,205 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + areaBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" + fcrBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" + flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" + kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" + locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" + userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs (ordered) === + +type ChickinBaseDTO struct { + Id uint `json:"id"` + ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` + ChickInDate time.Time `json:"chick_in_date"` + Quantity float64 `json:"quantity"` + Note string `json:"note"` +} + +type ProjectFlockDTO struct { + Id uint `json:"id"` + Period int `json:"period"` + Category string `json:"category"` + Flock *flockBaseDTO.FlockBaseDTO `json:"flock"` + Area *areaBaseDTO.AreaBaseDTO `json:"area"` + Fcr *fcrBaseDTO.FcrBaseDTO `json:"fcr"` + Location *locationBaseDTO.LocationBaseDTO `json:"location"` +} + +type ProjectFlockKandangDTO struct { + Id uint `json:"id"` + ProjectFlock *ProjectFlockDTO `json:"project_flock"` + Kandang *kandangBaseDTO.KandangBaseDTO `json:"kandang"` +} + +// gunakan base DTO dari package users + +type ChickinSimpleDTO struct { + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + Quantity float64 `json:"quantity"` + Note string `json:"note"` + CreatedBy uint `json:"created_by"` +} + +type ChickinListDTO struct { + ChickinBaseDTO + ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` + CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ChickinDetailDTO struct { + ChickinListDTO +} + +// === Mapper Functions (ordered) === + +func ToFlockDTO(e entity.Flock) flockBaseDTO.FlockBaseDTO { + return flockBaseDTO.ToFlockBaseDTO(e) +} + +func ToKandangDTO(e entity.Kandang) kandangBaseDTO.KandangBaseDTO { + return kandangBaseDTO.ToKandangBaseDTO(e) +} +func ToAreaDTO(e entity.Area) areaBaseDTO.AreaBaseDTO { + return areaBaseDTO.ToAreaBaseDTO(e) +} + +func ToFcrDTO(e entity.Fcr) fcrBaseDTO.FcrBaseDTO { + return fcrBaseDTO.ToFcrBaseDTO(e) +} + +func ToLocationDTO(e entity.Location) locationBaseDTO.LocationBaseDTO { + return locationBaseDTO.ToLocationBaseDTO(e) +} + +func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO { + return userBaseDTO.ToUserBaseDTO(e) +} + +func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { + var flock *flockBaseDTO.FlockBaseDTO + if e.Flock.Id != 0 { + mapped := flockBaseDTO.ToFlockBaseDTO(e.Flock) + flock = &mapped + } + var area *areaBaseDTO.AreaBaseDTO + if e.Area.Id != 0 { + mapped := areaBaseDTO.ToAreaBaseDTO(e.Area) + area = &mapped + } + var fcr *fcrBaseDTO.FcrBaseDTO + if e.Fcr.Id != 0 { + mapped := fcrBaseDTO.ToFcrBaseDTO(e.Fcr) + fcr = &mapped + } + var location *locationBaseDTO.LocationBaseDTO + if e.Location.Id != 0 { + mapped := locationBaseDTO.ToLocationBaseDTO(e.Location) + location = &mapped + } + return ProjectFlockDTO{ + Id: e.Id, + Period: e.Period, + Category: e.Category, + Flock: flock, + Area: area, + Fcr: fcr, + Location: location, + } +} + +func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { + var pf *ProjectFlockDTO + if e.ProjectFlock.Id != 0 { + mapped := ToProjectFlockDTO(e.ProjectFlock) + pf = &mapped + } + var kandang *kandangBaseDTO.KandangBaseDTO + if e.Kandang.Id != 0 { + mapped := kandangBaseDTO.ToKandangBaseDTO(e.Kandang) + kandang = &mapped + } + return ProjectFlockKandangDTO{ + Id: e.Id, + ProjectFlock: pf, + Kandang: kandang, + } +} + +func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { + var pfk *ProjectFlockKandangDTO + if e.ProjectFlockKandang.Id != 0 { + mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + pfk = &mapped + } + return ChickinBaseDTO{ + Id: e.Id, + ProjectFlockKandang: pfk, + ChickInDate: e.ChickInDate, + Quantity: e.Quantity, + Note: e.Note, + } +} + +func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { + return ChickinSimpleDTO{ + Id: e.Id, + ProjectFlockKandangId: e.ProjectFlockKandangId, + ChickInDate: e.ChickInDate, + Quantity: e.Quantity, + Note: e.Note, + CreatedBy: e.CreatedBy, + } +} + +func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { + var createdUser *userBaseDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + var pfk *ProjectFlockKandangDTO + if e.ProjectFlockKandang.Id != 0 { + mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + pfk = &mapped + } + return ChickinListDTO{ + ChickinBaseDTO: ToChickinBaseDTO(e), + ProjectFlockKandang: pfk, + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } +} + +func ToChickinListDTOs(e []entity.ProjectChickin) []ChickinListDTO { + result := make([]ChickinListDTO, len(e)) + for i, r := range e { + result[i] = ToChickinListDTO(r) + } + return result +} + +func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO { + result := make([]ChickinSimpleDTO, len(e)) + for i, r := range e { + result[i] = ToChickinSimpleDTO(r) + } + return result +} + +func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO { + return ChickinDetailDTO{ + ChickinListDTO: ToChickinListDTO(e), + } +} diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go new file mode 100644 index 00000000..116e2fbb --- /dev/null +++ b/internal/modules/production/chickins/module.go @@ -0,0 +1,39 @@ +package chickins + +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" + rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" + sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" + rAuditLog "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" + + rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type ChickinModule struct{} + +func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + chickinRepo := rChickin.NewChickinRepository(db) + kandangRepo := rKandang.NewKandangRepository(db) + auditlogrepo := rAuditLog.NewAuditLogRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) + projectflockkandangrepo := rProjectFlock.NewProjectFlockKandangRepository(db) + projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db) + projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + + userRepo := rUser.NewUserRepository(db) + + chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, auditlogrepo, projectflockkandangrepo, projectflockpopulationrepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + ChickinRoutes(router, userService, chickinService) +} diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go new file mode 100644 index 00000000..64e2e4b4 --- /dev/null +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -0,0 +1,36 @@ +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 ProjectChickinRepository interface { + repository.BaseRepository[entity.ProjectChickin] + GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) +} + +type ChickinRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ProjectChickin] +} + +func NewChickinRepository(db *gorm.DB) ProjectChickinRepository { + return &ChickinRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db), + } +} + +func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) { + var chickin entity.ProjectChickin + err := r.DB().WithContext(ctx). + Where("project_floc_id = ?", projectFlockID). + Where("deleted_at IS NULL"). + First(&chickin).Error + if err != nil { + return nil, err + } + return &chickin, nil +} diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go new file mode 100644 index 00000000..5fa5237a --- /dev/null +++ b/internal/modules/production/chickins/route.go @@ -0,0 +1,29 @@ +package chickins + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/controllers" + chickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService) { + ctrl := controller.NewChickinController(s) + + route := v1.Group("/chickins") + + // 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) + route.Post("/:id/approve", ctrl.Approve) +} diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go new file mode 100644 index 00000000..43105374 --- /dev/null +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -0,0 +1,370 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" + rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" + rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + AuditLogRepo "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 ChickinService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) + DeleteOne(ctx *fiber.Ctx, id uint) error + Approve(ctx *fiber.Ctx, id uint) error +} + +type chickinService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProjectChickinRepository + KandangRepo KandangRepo.KandangRepository + WarehouseRepo rWarehouse.WarehouseRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + ProjectFlockRepo rProjectFlock.ProjectflockRepository + AuditLogRepo AuditLogRepo.AuditLogRepository + ProjectflockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository + ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository +} + +func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, auditLogRepo AuditLogRepo.AuditLogRepository, projectflockkandangRepo rProjectFlockKandang.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, validate *validator.Validate) ChickinService { + return &chickinService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + KandangRepo: kandangRepo, + WarehouseRepo: warehouseRepo, + ProductWarehouseRepo: productWarehouseRepo, + ProjectFlockRepo: projectFlockRepo, + AuditLogRepo: auditLogRepo, + ProjectflockKandangRepo: projectflockkandangRepo, + ProjectflockPopulationRepo: projectflockpopulationRepo, + } +} + +func (s chickinService) withRelations(db *gorm.DB) *gorm.DB { + return db. + Preload("CreatedUser"). + Preload("ProjectFlockKandang.Kandang"). + Preload("ProjectFlockKandang.Kandang.Location"). + Preload("ProjectFlockKandang.Kandang.Location.Area"). + Preload("ProjectFlockKandang.Kandang.Pic"). + Preload("ProjectFlockKandang.ProjectFlock"). + Preload("ProjectFlockKandang.ProjectFlock.Flock"). + Preload("ProjectFlockKandang.ProjectFlock.Area"). + Preload("ProjectFlockKandang.ProjectFlock.Fcr"). + Preload("ProjectFlockKandang.ProjectFlock.Location"). + Preload("ProjectFlockKandang.ProjectFlock.Location.Area") + +} + +func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.ProjectFlockKandangId != 0 { + return db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId) + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + if err != nil { + s.Log.Errorf("Failed to get chickins: %+v", err) + return nil, 0, err + } + return chickins, total, nil +} + +func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, error) { + + chickin, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + if err != nil { + s.Log.Errorf("Failed get chickin by id: %+v", err) + return nil, err + } + return chickin, nil +} + +func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), 1) + if err != nil { + s.Log.Errorf("Failed to get projectflock kandang: %+v", err) + return nil, err + } + + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId) + if err != nil { + s.Log.Errorf("Failed to get warehouse: %+v", err) + return nil, err + } + + projectFlock, err := s.ProjectFlockRepo.GetByID( + c.Context(), + projectflockkandang.ProjectFlockId, + func(db *gorm.DB) *gorm.DB { + return db + }, + ) + if err != nil { + s.Log.Errorf("Failed to get project flock: %+v", err) + return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") + } + var productWarehouses []entity.ProductWarehouse + err = s.ProductWarehouseRepo.DB(). + WithContext(c.Context()). + 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 = ?", projectFlock.Category, warehouse.Id). + Order("created_at DESC"). + Find(&productWarehouses).Error + if err != nil { + s.Log.Errorf("Failed to get product warehouses: %+v", err) + return nil, err + } + if len(productWarehouses) == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") + } + + // Jumlahkan semua quantity DOC + totalQuantity := 0.0 + for _, pw := range productWarehouses { + totalQuantity += pw.Quantity + } + + if totalQuantity < 1 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses") + } + + // Buat satu chickin dengan total quantity + chickinDate, err := utils.ParseDateString(req.ChickInDate) + if err != nil { + s.Log.Errorf("Failed to parse chickin date: %+v", err) + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format") + } + newChickin := &entity.ProjectChickin{ + ProjectFlockKandangId: projectflockkandang.ProjectFlockId, + ChickInDate: chickinDate, + Quantity: totalQuantity, + Note: "", + CreatedBy: 1, //todo: ganti dengan user login + } + err = s.Repository.CreateOne(c.Context(), newChickin, nil) + if err != nil { + s.Log.Errorf("Failed to create chickin: %+v", err) + return nil, err + } + + // Update semua product warehouse: set quantity jadi 0 + for _, pw := range productWarehouses { + err = s.ProductWarehouseRepo.PatchOne(c.Context(), pw.Id, map[string]any{ + "quantity": 0, + }, nil) + if err != nil { + s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) + return nil, err + } + } + + existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to get project flock population: %+v", err) + return nil, err + } + if existingPopulation != nil { + + err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), existingPopulation.Id, map[string]any{ + "reserved_quantity": newChickin.Quantity + existingPopulation.ReservedQuantity, + }, nil) + if err != nil { + s.Log.Errorf("Failed to update project flock population: %+v", err) + return nil, err + } + } else { + newPopulation := &entity.ProjectFlockPopulation{ + ProjectFlockKandangId: req.ProjectFlockKandangId, + InitialQuantity: 0, + CurrentQuantity: 0, + ReservedQuantity: newChickin.Quantity, + CreatedBy: 1, // todo: ganti dengan user login + } + err = s.ProjectflockPopulationRepo.CreateOne(c.Context(), newPopulation, nil) + if err != nil { + s.Log.Errorf("Failed to create project flock population: %+v", err) + return nil, err + } + } + + return s.GetOne(c, newChickin.Id) +} + +func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.ChickInDate != "" { + updateBody["chick_in_date"] = req.ChickInDate + } + 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, "Chickin not found") + } + s.Log.Errorf("Failed to update chickin: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { + // todo: cek apakah chickin sudah di approve atau belum + + chickin, err := s.Repository.GetByID(c.Context(), id, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + if err != nil { + s.Log.Errorf("Failed get chickin by id: %+v", err) + return err + } + + population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) + if err != nil { + s.Log.Errorf("Failed to get project flock population: %+v", err) + return err + } + + err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{ + "reserved_quantity": population.ReservedQuantity - chickin.Quantity, + }, nil) + if err != nil { + s.Log.Errorf("Failed to update project flock population: %+v", err) + return err + } + + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + s.Log.Errorf("Failed to delete chickin: %+v", err) + return err + } + + projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), population.ProjectFlockKandangId) + if err != nil { + s.Log.Errorf("Failed to get projectflock kandang: %+v", err) + return err + } + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId) + if err != nil { + s.Log.Errorf("Failed to get warehouse: %+v", err) + return err + } + + projectFlock, err := s.ProjectFlockRepo.GetByID( + c.Context(), + projectflockkandang.ProjectFlockId, + func(db *gorm.DB) *gorm.DB { + return db + }, + ) + + if err != nil { + s.Log.Errorf("Failed to get project flock: %+v", err) + return fiber.NewError(fiber.StatusNotFound, "Project Flock not found") + } + var productWarehouse entity.ProductWarehouse + err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). + 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 = ?", projectFlock.Category, warehouse.Id). + Order("created_at DESC"). + First(&productWarehouse).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") + } + s.Log.Errorf("Failed to get product warehouse: %+v", err) + return err + } + + updatedQuantity := productWarehouse.Quantity + chickin.Quantity + err = s.ProductWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{ + "quantity": updatedQuantity, + }, nil) + if err != nil { + s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) + return err + } + + return nil +} + +func (s *chickinService) Approve(c *fiber.Ctx, id uint) error { + + // todo: ini contoh akhir jika sudah approved + + chickin, err := s.Repository.GetByID( + c.Context(), + id, + nil, + ) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + if err != nil { + s.Log.Errorf("Failed get chickin by id: %+v", err) + return err + } + + population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) + if err != nil { + s.Log.Errorf("Failed to get project flock population: %+v", err) + return err + } + + err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{ + "reserved_quantity": population.ReservedQuantity - chickin.Quantity, + "initial_quantity": population.InitialQuantity + chickin.Quantity, + "current_quantity": population.CurrentQuantity + chickin.Quantity, + }, nil) + if err != nil { + s.Log.Errorf("Failed to update project flock population: %+v", err) + return err + } + + return nil +} diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go new file mode 100644 index 00000000..c122c100 --- /dev/null +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -0,0 +1,16 @@ +package validation + +type Create struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` + ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` +} + +type Update struct { + ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` +} diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index a1f2e263..31d0b9f0 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -190,6 +190,33 @@ func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error { }) } +func (u *ProjectflockController) Approval(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + req := new(validation.Approve) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.ProjectflockService.Approval(c, uint(id), req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Submit projectflock approval successfully", + Data: dto.ToProjectFlockListDTO(*result), + }) +} + func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { param := c.Params("flock_id") diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index fcf3d50c..e639e968 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -4,73 +4,41 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" ) type ProjectFlockBaseDTO struct { - Id uint `json:"id"` - Period int `json:"period"` - Category string `json:"category"` - Flock *flockDTO.FlockBaseDTO `json:"flock"` - Area *areaDTO.AreaBaseDTO `json:"area"` - Fcr *fcrDTO.FcrBaseDTO `json:"fcr"` - Location *locationDTO.LocationBaseDTO `json:"location"` -} - -func ToProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { - var flock *flockDTO.FlockBaseDTO - if e.Flock.Id != 0 { - mapped := flockDTO.ToFlockBaseDTO(e.Flock) - flock = &mapped - } - - var area *areaDTO.AreaBaseDTO - if e.Area.Id != 0 { - mapped := areaDTO.ToAreaBaseDTO(e.Area) - area = &mapped - } - - var fcr *fcrDTO.FcrBaseDTO - if e.Fcr.Id != 0 { - mapped := fcrDTO.ToFcrBaseDTO(e.Fcr) - fcr = &mapped - } - - var location *locationDTO.LocationBaseDTO - if e.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(e.Location) - location = &mapped - } - - return ProjectFlockBaseDTO{ - Id: e.Id, - Period: e.Period, - Category: e.Category, - Flock: flock, - Area: area, - Fcr: fcr, - Location: location, - } + Id uint `json:"id"` + Period int `json:"period"` } type ProjectFlockListDTO struct { ProjectFlockBaseDTO - Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` + Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` + Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` + Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval approvalDTO.ApprovalBaseDTO `json:"approval"` } type ProjectFlockDetailDTO struct { ProjectFlockListDTO } -type FlockPeriodSummaryDTO struct { +type FlockPeriodDTO struct { Flock flockDTO.FlockBaseDTO `json:"flock"` NextPeriod int `json:"next_period"` } @@ -90,12 +58,20 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { } } + latestApproval := defaultProjectFlockLatestApproval(e) + if e.LatestApproval != nil { + snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval) + latestApproval = snapshot + } + return ProjectFlockListDTO{ - ProjectFlockBaseDTO: ToProjectFlockBaseDTO(e), + ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), Kandangs: kandangSummaries, + Category: e.Category, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, + Approval: latestApproval, } } @@ -113,9 +89,55 @@ func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO { } } -func ToFlockPeriodSummaryDTO(flock entity.Flock, next int) FlockPeriodSummaryDTO { - return FlockPeriodSummaryDTO{ - Flock: flockDTO.ToFlockBaseDTO(flock), +func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.ApprovalBaseDTO { + result := approvalDTO.ApprovalBaseDTO{} + + step := utils.ProjectFlockStepPengajuan + if step > 0 { + result.StepNumber = uint16(step) + if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, step); ok { + result.StepName = label + } else if label, ok := utils.ProjectFlockApprovalSteps[step]; ok { + result.StepName = label + } + } + if result.StepName == "" { + result.StepName = "Pengajuan" + } + + if !e.CreatedAt.IsZero() { + result.ActionAt = e.CreatedAt + } + + if e.CreatedUser.Id != 0 { + result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) + } else if e.CreatedBy != 0 { + result.ActionBy = userDTO.UserBaseDTO{ + Id: e.CreatedBy, + IdUser: int64(e.CreatedBy), + } + } + + return result +} + +func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { + return ProjectFlockBaseDTO{ + Id: e.Id, + Period: e.Period, + } +} + +func ToFlockSummaryDTO(e entity.Flock) flockDTO.FlockBaseDTO { + return flockDTO.FlockBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToFlockPeriodSummaryDTO(flock entity.Flock, next int) FlockPeriodDTO { + return FlockPeriodDTO{ + Flock: ToFlockSummaryDTO(flock), NextPeriod: next, } } diff --git a/internal/modules/production/project_flocks/module.go b/internal/modules/production/project_flocks/module.go index 5f1afbe3..994eb4a4 100644 --- a/internal/modules/production/project_flocks/module.go +++ b/internal/modules/production/project_flocks/module.go @@ -1,14 +1,20 @@ package project_flocks import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" "gorm.io/gorm" rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" + utils "gitlab.com/mbugroup/lti-api.git/internal/utils" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -23,7 +29,13 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db) userRepo := rUser.NewUserRepository(db) - projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, validate) + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlock, utils.ProjectFlockApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err)) + } + + projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, approvalService, validate) userService := sUser.NewUserService(userRepo, validate) ProjectflockRoutes(router, userService, projectflockService) diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go new file mode 100644 index 00000000..cb4b0d5f --- /dev/null +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -0,0 +1,35 @@ +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 ProjectFlockPopulationRepository interface { + repository.BaseRepository[entity.ProjectFlockPopulation] + GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) +} + +type projectFlockPopulationRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ProjectFlockPopulation] +} + +func NewProjectFlockPopulationRepository(db *gorm.DB) ProjectFlockPopulationRepository { + return &projectFlockPopulationRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockPopulation](db), + } +} + +func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) { + var record entity.ProjectFlockPopulation + err := r.DB().WithContext(ctx). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + First(&record).Error + if err != nil { + return nil, err + } + return &record, nil +} diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index b5ce21a4..a5ceaf7f 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -8,6 +8,7 @@ import ( ) type ProjectFlockKandangRepository interface { + GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) @@ -59,3 +60,16 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB { return r.db } + +func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) { + record := new(entity.ProjectFlockKandang) + if err := r.db.WithContext(ctx). + Preload("ProjectFlock"). + Preload("ProjectFlock.Flock"). + Preload("Kandang"). + Preload("CreatedUser"). + First(record, id).Error; err != nil { + return nil, err + } + return record, nil +} diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index e5dbb48a..7282c020 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -25,5 +25,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + route.Post("/:id/approvals", ctrl.Approval) route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary) } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 21941826..f6ebf6e7 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -7,13 +7,14 @@ import ( "strings" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" - common "gitlab.com/mbugroup/lti-api.git/internal/common/service" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" - "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" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -28,15 +29,18 @@ type ProjectflockService interface { UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) + Approval(ctx *fiber.Ctx, id uint, req *validation.Approve) (*entity.ProjectFlock, error) } type projectflockService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.ProjectflockRepository - FlockRepo flockRepository.FlockRepository - KandangRepo kandangRepository.KandangRepository - PivotRepo repository.ProjectFlockKandangRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProjectflockRepository + FlockRepo flockRepository.FlockRepository + KandangRepo kandangRepository.KandangRepository + PivotRepo repository.ProjectFlockKandangRepository + ApprovalSvc commonSvc.ApprovalService + approvalWorkflow approvalutils.ApprovalWorkflowKey } type FlockPeriodSummary struct { @@ -49,15 +53,18 @@ func NewProjectflockService( flockRepo flockRepository.FlockRepository, kandangRepo kandangRepository.KandangRepository, pivotRepo repository.ProjectFlockKandangRepository, + approvalSvc commonSvc.ApprovalService, validate *validator.Validate, ) ProjectflockService { return &projectflockService{ - Log: utils.Log, - Validate: validate, - Repository: repo, - FlockRepo: flockRepo, - KandangRepo: kandangRepo, - PivotRepo: pivotRepo, + Log: utils.Log, + Validate: validate, + Repository: repo, + FlockRepo: flockRepo, + KandangRepo: kandangRepo, + PivotRepo: pivotRepo, + ApprovalSvc: approvalSvc, + approvalWorkflow: utils.ApprovalWorkflowProjectFlock, } } @@ -68,7 +75,7 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { Preload("Area"). Preload("Fcr"). Preload("Location"). - Preload("Kandangs") + Preload("Kandangs.Location") } func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { @@ -154,6 +161,27 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e s.Log.Errorf("Failed to get projectflocks: %+v", err) return nil, 0, err } + + if s.ApprovalSvc != nil && len(projectflocks) > 0 { + ids := make([]uint, len(projectflocks)) + for i, item := range projectflocks { + ids[i] = item.Id + } + + latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err) + } else if len(latestMap) > 0 { + for i := range projectflocks { + if approval, ok := latestMap[projectflocks[i].Id]; ok { + projectflocks[i].LatestApproval = approval + } + } + } + } + return projectflocks, total, nil } @@ -166,6 +194,23 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock s.Log.Errorf("Failed get projectflock by id: %+v", err) return nil, err } + + if s.ApprovalSvc != nil { + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) + } else if len(approvals) > 0 { + if projectflock.LatestApproval == nil { + latest := approvals[len(approvals)-1] + projectflock.LatestApproval = &latest + } + } else { + projectflock.LatestApproval = nil + } + } + return projectflock, nil } @@ -174,8 +219,8 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, err } - category, ok := utils.NormalizeProjectFlockCategory(req.Category) - if !ok { + cat := strings.ToUpper(req.Category) + if !utils.IsValidProjectFlockCategory(cat) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") } @@ -183,11 +228,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") } - if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, - common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, - common.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, - common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())}, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, + commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, + commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, + commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())}, ); err != nil { return nil, err } @@ -209,31 +254,48 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* } } - tx := s.Repository.DB().Begin() - if tx.Error != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") - } - - projectRepo := repository.NewProjectflockRepository(tx) - nextPeriod, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId) - if err != nil { - tx.Rollback() - s.Log.Errorf("Failed to determine next period for flock %d: %+v", req.FlockId, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine next period") - } - createBody := &entity.ProjectFlock{ FlockId: req.FlockId, AreaId: req.AreaId, - Category: string(category), + Category: cat, FcrId: req.FcrId, LocationId: req.LocationId, - Period: nextPeriod, CreatedBy: 1, } - if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { - tx.Rollback() + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + projectRepo := repository.NewProjectflockRepository(dbTransaction) + + period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId) + if err != nil { + return err + } + createBody.Period = period + + if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { + return err + } + + if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs); err != nil { + return err + } + + actorID := uint(1) //TODO: Change From Auth + action := entity.ApprovalActionCreated + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + _, err = approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlock, + createBody.Id, + utils.ProjectFlockStepPengajuan, + &action, + actorID, + nil, + ) + return err + }) + + if err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists") } @@ -241,17 +303,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, err } - if err := s.attachKandangs(c.Context(), tx, createBody.Id, kandangIDs); err != nil { - tx.Rollback() - s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", createBody.Id, err) - return nil, err - } - - if err := tx.Commit().Error; err != nil { - tx.Rollback() - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") - } - return s.GetOne(c, createBody.Id) } @@ -268,13 +319,14 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - updateBody := make(map[string]any) - var relationChecks []common.RelationCheck + hasBodyChanges := false + var relationChecks []commonSvc.RelationCheck if req.FlockId != nil { updateBody["flock_id"] = *req.FlockId - relationChecks = append(relationChecks, common.RelationCheck{ + hasBodyChanges = true + relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Flock", ID: req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB()), @@ -282,22 +334,25 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if req.AreaId != nil { updateBody["area_id"] = *req.AreaId - relationChecks = append(relationChecks, common.RelationCheck{ + hasBodyChanges = true + relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Area", ID: req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB()), }) } if req.Category != nil { - if normalized, ok := utils.NormalizeProjectFlockCategory(*req.Category); ok { - updateBody["category"] = string(normalized) - } else { + cat := strings.ToUpper(*req.Category) + if !utils.IsValidProjectFlockCategory(cat) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") } + + updateBody["category"] = cat } if req.FcrId != nil { updateBody["fcr_id"] = *req.FcrId - relationChecks = append(relationChecks, common.RelationCheck{ + hasBodyChanges = true + relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "FCR", ID: req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()), @@ -305,24 +360,24 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if req.LocationId != nil { updateBody["location_id"] = *req.LocationId - relationChecks = append(relationChecks, common.RelationCheck{ + hasBodyChanges = true + relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Location", ID: req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB()), }) } - if req.Period != nil { - updateBody["period"] = *req.Period - } if len(relationChecks) > 0 { - if err := common.EnsureRelations(c.Context(), relationChecks...); err != nil { + if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil { return nil, err } } var newKandangIDs []uint + hasKandangChanges := false if req.KandangIds != nil { + hasKandangChanges = true if len(req.KandangIds) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty") } @@ -344,67 +399,178 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } } - tx := s.Repository.DB().Begin() - if tx.Error != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") + hasChanges := hasBodyChanges || hasKandangChanges + if !hasChanges { + return s.GetOne(c, id) } - projectRepo := repository.NewProjectflockRepository(tx) - if len(updateBody) > 0 { - if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { - tx.Rollback() - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + projectRepo := repository.NewProjectflockRepository(dbTransaction) + + if len(updateBody) > 0 { + if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { + return err + } + } else { + if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil { + return err } - s.Log.Errorf("Failed to update projectflock: %+v", err) - return nil, err } + + if req.KandangIds != nil { + existingIDs := make(map[uint]struct{}, len(existing.Kandangs)) + for _, k := range existing.Kandangs { + existingIDs[k.Id] = struct{}{} + } + newSet := make(map[uint]struct{}, len(newKandangIDs)) + for _, kid := range newKandangIDs { + newSet[kid] = struct{}{} + } + + var toDetach []uint + for kid := range existingIDs { + if _, ok := newSet[kid]; !ok { + toDetach = append(toDetach, kid) + } + } + + var toAttach []uint + for kid := range newSet { + if _, ok := existingIDs[kid]; !ok { + toAttach = append(toAttach, kid) + } + } + + if len(toDetach) > 0 { + if err := s.detachKandangs(c.Context(), dbTransaction, id, toDetach, true); err != nil { + return err + } + } + + if len(toAttach) > 0 { + if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach); err != nil { + return err + } + } + } + + if hasChanges { + actorID := uint(1) //TODO: Change From Auth + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + if approvalSvc != nil { + latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil) + if err != nil { + return err + } + shouldRecordUpdate := latestBeforeReset == nil || + latestBeforeReset.StepNumber != uint16(utils.ProjectFlockStepPengajuan) || + latestBeforeReset.Action == nil || + (latestBeforeReset.Action != nil && *latestBeforeReset.Action != entity.ApprovalActionUpdated) + + if shouldRecordUpdate { + action := entity.ApprovalActionUpdated + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlock, + id, + utils.ProjectFlockStepPengajuan, + &action, + actorID, + nil, + ); err != nil { + return err + } + } + } + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + } + s.Log.Errorf("Failed to update projectflock %d: %+v", id, err) + return nil, err } - if req.KandangIds != nil { - existingIDs := make(map[uint]struct{}, len(existing.Kandangs)) - for _, k := range existing.Kandangs { - existingIDs[k.Id] = struct{}{} - } - newSet := make(map[uint]struct{}, len(newKandangIDs)) - for _, id := range newKandangIDs { - newSet[id] = struct{}{} - } + return s.GetOne(c, id) +} - var toDetach []uint - for id := range existingIDs { - if _, ok := newSet[id]; !ok { - toDetach = append(toDetach, id) - } - } - - var toAttach []uint - for id := range newSet { - if _, ok := existingIDs[id]; !ok { - toAttach = append(toAttach, id) - } - } - - if len(toDetach) > 0 { - if err := s.detachKandangs(c.Context(), tx, id, toDetach, false); err != nil { - tx.Rollback() - s.Log.Errorf("Failed to detach kandangs from projectflock %d: %+v", id, err) - return nil, err - } - } - - if len(toAttach) > 0 { - if err := s.attachKandangs(c.Context(), tx, id, toAttach); err != nil { - tx.Rollback() - s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", id, err) - return nil, err - } - } +func (s projectflockService) Approval(c *fiber.Ctx, id uint, req *validation.Approve) (*entity.ProjectFlock, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err } - if err := tx.Commit().Error; err != nil { - tx.Rollback() - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") + project, err := s.GetOne(c, id) + if err != nil { + s.Log.Errorf("Failed to fetch projectflock %d before approval: %+v", id, err) + return nil, err + } + + actorID := uint(1) // TODO: change from auth context + var action entity.ApprovalAction + switch strings.ToUpper(strings.TrimSpace(req.Action)) { + case string(entity.ApprovalActionRejected): + action = entity.ApprovalActionRejected + case string(entity.ApprovalActionApproved): + action = entity.ApprovalActionApproved + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") + } + + step := utils.ProjectFlockStepPengajuan + if action == entity.ApprovalActionApproved { + step = utils.ProjectFlockStepAktif + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlock, + project.Id, + step, + &action, + actorID, + req.Notes, + ); err != nil { + return err + } + + kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction) + switch action { + case entity.ApprovalActionApproved: + if err := kandangRepoTx.UpdateStatusByProjectFlockID( + c.Context(), + project.Id, + utils.KandangStatusActive, + ); err != nil { + return err + } + case entity.ApprovalActionRejected: + if err := kandangRepoTx.UpdateStatusByProjectFlockID( + c.Context(), + project.Id, + utils.KandangStatusNonActive, + ); err != nil { + return err + } + } + + return nil + }) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + } + s.Log.Errorf("Failed to record approval for projectflock %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } return s.GetOne(c, id) @@ -420,37 +586,34 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - tx := s.Repository.DB().Begin() - if tx.Error != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") - } - - if len(existing.Kandangs) > 0 { - ids := make([]uint, len(existing.Kandangs)) - for i, k := range existing.Kandangs { - ids[i] = k.Id + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + if len(existing.Kandangs) > 0 { + ids := make([]uint, len(existing.Kandangs)) + for i, k := range existing.Kandangs { + ids[i] = k.Id + } + if err := s.detachKandangs(c.Context(), dbTransaction, id, ids, true); err != nil { + return err + } } - if err := s.detachKandangs(c.Context(), tx, id, ids, true); err != nil { - tx.Rollback() - s.Log.Errorf("Failed to detach kandangs before deleting projectflock %d: %+v", id, err) + + if err := repository.NewProjectflockRepository(dbTransaction).DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + } return err } - } - if err := repository.NewProjectflockRepository(tx).DeleteOne(c.Context(), id); err != nil { - tx.Rollback() - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + return nil + }) + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return fiberErr } - s.Log.Errorf("Failed to delete projectflock: %+v", err) + s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err) return err } - if err := tx.Commit().Error; err != nil { - tx.Rollback() - return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") - } - return nil } @@ -532,12 +695,12 @@ func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []s } } -func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint) error { +func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error { if len(kandangIDs) == 0 { return nil } - if err := tx.Model(&entity.Kandang{}). + if err := dbTransaction.Model(&entity.Kandang{}). Where("id IN ?", kandangIDs). Updates(map[string]any{ "project_flock_id": projectFlockID, @@ -546,7 +709,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, pr return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } - pivotRepo := s.pivotRepoWithTx(tx) + pivotRepo := s.pivotRepoWithTx(dbTransaction) records := make([]*entity.ProjectFlockKandang, len(kandangIDs)) for i, id := range kandangIDs { records[i] = &entity.ProjectFlockKandang{ @@ -560,7 +723,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, pr return nil } -func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error { +func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error { if len(kandangIDs) == 0 { return nil } @@ -570,21 +733,21 @@ func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, pr updates["status"] = string(utils.KandangStatusNonActive) } - if err := tx.Model(&entity.Kandang{}). + if err := dbTransaction.Model(&entity.Kandang{}). Where("id IN ?", kandangIDs). Updates(updates).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } - if err := s.pivotRepoWithTx(tx).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil { + if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") } return nil } -func (s projectflockService) pivotRepoWithTx(tx *gorm.DB) repository.ProjectFlockKandangRepository { +func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository { if s.PivotRepo == nil { - return repository.NewProjectFlockKandangRepository(tx) + return repository.NewProjectFlockKandangRepository(dbTransaction) } - return s.PivotRepo.WithTx(tx) + return s.PivotRepo.WithTx(dbTransaction) } diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index bbe957b6..9ee066af 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -3,7 +3,7 @@ package validation type Create struct { FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` - Category string `json:"category" validate:"required_strict,oneof=growing laying GROWING LAYING"` + Category string `json:"category" validate:"required_strict"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` @@ -12,10 +12,9 @@ type Create struct { type Update struct { FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` - Category *string `json:"category,omitempty" validate:"omitempty,oneof=growing laying GROWING LAYING"` + Category *string `json:"category,omitempty" validate:"omitempty"` FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` - Period *int `json:"period,omitempty" validate:"omitempty,number,gt=0"` KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"` } @@ -23,10 +22,15 @@ 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"` - SortBy string `query:"sort_by" validate:"omitempty,oneof=area location kandangs period"` + SortBy string `query:"sort_by" validate:"omitempty"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` Period int `query:"period" validate:"omitempty,number,gt=0"` KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` } + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index 73bbe8da..b41ef1e7 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -7,6 +7,7 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins" projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" // MODULE IMPORTS @@ -18,6 +19,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida allModules := []modules.Module{ projectflocks.ProjectflockModule{}, recordings.RecordingModule{}, + chickins.ChickinModule{}, // MODULE REGISTRY } diff --git a/internal/modules/shared/repositories/audit-logs.repository.go b/internal/modules/shared/repositories/audit-logs.repository.go new file mode 100644 index 00000000..b247f3f2 --- /dev/null +++ b/internal/modules/shared/repositories/audit-logs.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type AuditLogRepository interface { + repository.BaseRepository[entity.AuditLog] +} + +type AuditLogRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.AuditLog] +} + +func NewAuditLogRepository(db *gorm.DB) AuditLogRepository { + return &AuditLogRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.AuditLog](db), + } +} diff --git a/internal/modules/shared/repositories/stock-availabilites.repository.go b/internal/modules/shared/repositories/stock-availabilites.repository.go new file mode 100644 index 00000000..9d3ae632 --- /dev/null +++ b/internal/modules/shared/repositories/stock-availabilites.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type StockAvailabilityRepository interface { + repository.BaseRepository[entity.StockAvailability] +} + +type StockAvailabilityRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.StockAvailability] +} + +func NewStockAvailabilityRepository(db *gorm.DB) StockAvailabilityRepository { + return &StockAvailabilityRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.StockAvailability](db), + } +} diff --git a/internal/modules/shared/stock-logs/repositories/stock-logs.repository.go b/internal/modules/shared/repositories/stock-logs.repository.go similarity index 100% rename from internal/modules/shared/stock-logs/repositories/stock-logs.repository.go rename to internal/modules/shared/repositories/stock-logs.repository.go diff --git a/internal/route/route.go b/internal/route/route.go index b1cd62a4..60f0fe26 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -13,6 +13,7 @@ import ( master "gitlab.com/mbugroup/lti-api.git/internal/modules/master" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" production "gitlab.com/mbugroup/lti-api.git/internal/modules/production" + approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals" // MODULE IMPORTS ) @@ -28,6 +29,7 @@ func Routes(app *fiber.App, db *gorm.DB) { constants.ConstantModule{}, inventory.InventoryModule{}, production.ProductionModule{}, + approvals.ApprovalModule{}, // MODULE REGISTRY } diff --git a/internal/utils/approvals/util.approval_workflow.go b/internal/utils/approvals/util.approval_workflow.go new file mode 100644 index 00000000..78f1de8e --- /dev/null +++ b/internal/utils/approvals/util.approval_workflow.go @@ -0,0 +1,243 @@ +package approvals + +import ( + "errors" + "fmt" + "strings" + "sync" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +type ApprovalStep uint16 + +type ApprovalWorkflowKey string + +func (k ApprovalWorkflowKey) String() string { + return string(k) +} + +type NextStepCallback func(current ApprovalStep, decision entity.ApprovalAction) (ApprovalStep, bool) + +var ( + approvalActions = map[entity.ApprovalAction]struct{}{ + entity.ApprovalActionApproved: {}, + entity.ApprovalActionRejected: {}, + entity.ApprovalActionCreated: {}, + entity.ApprovalActionUpdated: {}, + } + + approvalWorkflows = make(map[ApprovalWorkflowKey]map[ApprovalStep]string) + approvalWorkflowsMu sync.RWMutex +) + +// WorkflowConstants prepares the registered workflows for exposure via constants endpoints. +func WorkflowConstants() map[string]map[string]string { + approvalWorkflowsMu.RLock() + defer approvalWorkflowsMu.RUnlock() + + if len(approvalWorkflows) == 0 { + return nil + } + + result := make(map[string]map[string]string, len(approvalWorkflows)) + for workflow, steps := range approvalWorkflows { + if len(steps) == 0 { + continue + } + stepMap := make(map[string]string, len(steps)) + for step, label := range steps { + stepMap[fmt.Sprintf("%d", step)] = label + } + result[workflow.String()] = stepMap + } + if len(result) == 0 { + return nil + } + return result +} + +// RegisterWorkflowSteps stores the available steps for a workflow key (usually matching approvable type). +func RegisterWorkflowSteps(workflow ApprovalWorkflowKey, steps map[ApprovalStep]string) error { + workflowStr := strings.TrimSpace(workflow.String()) + if workflowStr == "" { + return errors.New("workflow key is required") + } + if len(steps) == 0 { + return fmt.Errorf("no steps defined for workflow %q", workflowStr) + } + + copied := make(map[ApprovalStep]string, len(steps)) + for step, label := range steps { + if step == 0 { + return fmt.Errorf("workflow %q contains step 0 which is not allowed", workflowStr) + } + trimmed := strings.TrimSpace(label) + if trimmed == "" { + return fmt.Errorf("workflow %q contains empty label for step %d", workflowStr, step) + } + copied[step] = trimmed + } + + approvalWorkflowsMu.Lock() + defer approvalWorkflowsMu.Unlock() + approvalWorkflows[ApprovalWorkflowKey(workflowStr)] = copied + return nil +} + +// WorkflowSteps returns the steps registered for the given workflow key. +func WorkflowSteps(workflow ApprovalWorkflowKey) map[ApprovalStep]string { + approvalWorkflowsMu.RLock() + defer approvalWorkflowsMu.RUnlock() + + workflowStr := strings.TrimSpace(workflow.String()) + if workflowStr == "" { + return nil + } + + steps, ok := approvalWorkflows[ApprovalWorkflowKey(workflowStr)] + if !ok || len(steps) == 0 { + return nil + } + + copied := make(map[ApprovalStep]string, len(steps)) + for step, label := range steps { + copied[step] = label + } + return copied +} + +// ApprovalStepName fetches the label for the target step inside the workflow. +func ApprovalStepName(workflow ApprovalWorkflowKey, step ApprovalStep) (string, bool) { + steps := WorkflowSteps(workflow) + if len(steps) == 0 { + return "", false + } + label, ok := steps[step] + return label, ok +} + +// ValidateApprovalStep ensures the workflow contains the provided step. +func ValidateApprovalStep(workflow ApprovalWorkflowKey, step ApprovalStep) error { + if _, ok := ApprovalStepName(workflow, step); ok { + return nil + } + return fmt.Errorf("invalid approval step %d for workflow %s", step, workflow) +} + +// IsValidApprovalAction reports whether the action is supported. +func IsValidApprovalAction(action entity.ApprovalAction) bool { + _, ok := approvalActions[action] + return ok +} + +// NewApproval creates an approval record for the given approvable target. +func NewApproval(workflow ApprovalWorkflowKey, approvableId uint, step ApprovalStep, action *entity.ApprovalAction, actorId uint, note *string) (*entity.Approval, error) { + if approvableId == 0 { + return nil, errors.New("approvable id is required") + } + + workflowStr := strings.TrimSpace(workflow.String()) + if workflowStr == "" { + return nil, errors.New("approval workflow key is required") + } + + key := ApprovalWorkflowKey(workflowStr) + + if err := ValidateApprovalStep(key, step); err != nil { + return nil, err + } + + var actionPtr *entity.ApprovalAction + if action != nil { + if !IsValidApprovalAction(*action) { + return nil, fmt.Errorf("invalid approval action %q", *action) + } + actionCopy := *action + actionPtr = &actionCopy + } + + if actorId == 0 { + return nil, errors.New("actor id is required") + } + + var notes *string + if note != nil { + trimmed := strings.TrimSpace(*note) + if trimmed != "" { + notes = &trimmed + } + } + + actor := actorId + var stepName string + if label, ok := ApprovalStepName(key, step); ok { + labelCopy := label + stepName = labelCopy + } + + return &entity.Approval{ + ApprovableType: workflowStr, + ApprovableId: approvableId, + StepNumber: uint16(step), + StepName: stepName, + Action: actionPtr, + Notes: notes, + ActionBy: &actor, + }, nil +} + +// SetApprovalAction updates the approval action, notes, and optionally advances to another step. +func SetApprovalAction(approval *entity.Approval, action entity.ApprovalAction, actorId uint, note *string, nextStep NextStepCallback) error { + if approval == nil { + return errors.New("approval is nil") + } + if !IsValidApprovalAction(action) { + return fmt.Errorf("invalid approval action %q", action) + } + if actorId == 0 { + return errors.New("actor id is required for approval decision") + } + + act := action + approval.Action = &act + approval.ActionBy = &actorId + + if note != nil { + trimmed := strings.TrimSpace(*note) + if trimmed == "" { + approval.Notes = nil + } else { + approval.Notes = &trimmed + } + } else { + approval.Notes = nil + } + + if nextStep != nil { + current := ApprovalStep(approval.StepNumber) + if proposed, ok := nextStep(current, action); ok { + if err := ValidateApprovalStep(ApprovalWorkflowKey(approval.ApprovableType), proposed); err != nil { + return err + } + approval.StepNumber = uint16(proposed) + } + } + + if label, ok := ApprovalStepName(ApprovalWorkflowKey(approval.ApprovableType), ApprovalStep(approval.StepNumber)); ok { + labelCopy := label + approval.StepName = labelCopy + } + + return nil +} + +// Approve marks the approval as approved by the given actor, applying the optional step callback. +func Approve(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error { + return SetApprovalAction(approval, entity.ApprovalActionApproved, actorId, note, nextStep) +} + +// Reject marks the approval as rejected by the given actor, applying the optional step callback. +func Reject(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error { + return SetApprovalAction(approval, entity.ApprovalActionRejected, actorId, note, nextStep) +} diff --git a/internal/utils/constant.go b/internal/utils/constant.go index d780d2ae..bdbc53b6 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -1,6 +1,10 @@ package utils -import "strings" +import ( + "strings" + + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" +) // ------------------------------------------------------------------- // FlagType & Groups @@ -120,6 +124,22 @@ const ( ProjectFlockCategoryLaying ProjectFlockCategory = "LAYING" ) +// ------------------------------------------------------------------- +// Project Flock Approval +// ------------------------------------------------------------------- + +const ( + ApprovalWorkflowProjectFlock approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCKS") + ProjectFlockStepPengajuan approvalutils.ApprovalStep = 1 + ProjectFlockStepAktif approvalutils.ApprovalStep = 2 +) + +// projectFlockApprovalSteps keeps the workflow step definitions for project flock approvals. +var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{ + ProjectFlockStepPengajuan: "Pengajuan", + ProjectFlockStepAktif: "Aktif", +} + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- @@ -230,19 +250,12 @@ func IsValidCustomerSupplierType(v string) bool { return false } -func NormalizeProjectFlockCategory(v string) (ProjectFlockCategory, bool) { - normalized := ProjectFlockCategory(strings.ToUpper(strings.TrimSpace(v))) - switch normalized { - case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying: - return normalized, true - default: - return "", false - } -} - func IsValidProjectFlockCategory(v string) bool { - _, ok := NormalizeProjectFlockCategory(v) - return ok + switch ProjectFlockCategory(v) { + case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying: + return true + } + return false } func IsValidSupplierCategory(v string) bool { diff --git a/tools/templates/validation.tmpl b/tools/templates/validation.tmpl index 3aa587eb..031b76c5 100644 --- a/tools/templates/validation.tmpl +++ b/tools/templates/validation.tmpl @@ -9,8 +9,8 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Search string `query:"search" validate:"omitempty,max=50"` } {{end}}