From a390d1d23a5ebec7c7d47b95d945ed2cd8aa7ee9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 29 Oct 2025 14:19:08 +0700 Subject: [PATCH 01/59] FIX[BE]: Fix Delete one on chickin match with BE standard --- .../product_warehouse.repository.go | 29 ++ .../project_chickin_detail.repository.go | 25 ++ .../chickins/services/chickin.service.go | 258 ++++++++---------- .../project_flock_population_repository.go | 20 +- 4 files changed, 194 insertions(+), 138 deletions(-) diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index f1f1fa57..0017fe6c 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -17,6 +17,9 @@ type ProductWarehouseRepository interface { ExistsByID(ctx context.Context, id uint) (bool, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) + GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) + WithTxRepo(tx *gorm.DB) ProductWarehouseRepository + DB() *gorm.DB } type ProductWarehouseRepositoryImpl struct { @@ -31,6 +34,15 @@ func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository { } } +func (r *ProductWarehouseRepositoryImpl) WithTxRepo(tx *gorm.DB) ProductWarehouseRepository { + return &ProductWarehouseRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](tx), + db: tx, + } +} +func (r *ProductWarehouseRepositoryImpl) DB() *gorm.DB { + return r.db +} func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) { return repository.Exists[entity.Product](ctx, r.db, productId) } @@ -89,3 +101,20 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con } return productWarehouses, nil } + +func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) { + var productWarehouse entity.ProductWarehouse + err := r.db.WithContext(ctx). + Table("product_warehouses"). + Select("product_warehouses.*"). + Joins("JOIN products ON products.id = product_warehouses.product_id"). + Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). + Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId). + Order("product_warehouses.created_at DESC"). + Limit(1). + First(&productWarehouse).Error + if err != nil { + return nil, err + } + return &productWarehouse, nil +} diff --git a/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go b/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go index 42c267ec..f5be8770 100644 --- a/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go @@ -1,6 +1,8 @@ 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" @@ -8,6 +10,10 @@ import ( type ProjectChickinDetailRepository interface { repository.BaseRepository[entity.ProjectChickinDetail] + CreateOne(ctx context.Context, entity *entity.ProjectChickinDetail, modifier func(*gorm.DB) *gorm.DB) error + DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error + GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) + WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository } type ChickinDetailRepositoryImpl struct { @@ -19,3 +25,22 @@ func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository { BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db), } } + +func (r *ChickinDetailRepositoryImpl) WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository { + return &ChickinDetailRepositoryImpl{BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](tx)} +} + +func (r *ChickinDetailRepositoryImpl) DB() *gorm.DB { + return r.BaseRepositoryImpl.DB() +} + +func (r *ChickinDetailRepositoryImpl) GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) { + var records []entity.ProjectChickinDetail + if err := r.DB().WithContext(ctx).Where("project_chickin_id = ?", projectChickinID).Find(&records).Error; err != nil { + return nil, err + } + if len(records) == 0 { + return nil, gorm.ErrRecordNotFound + } + return records, nil +} diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index f422666f..83d94ba5 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -77,6 +77,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity } 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 { @@ -241,143 +242,129 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { - db := s.Repository.DB() - tx := db.WithContext(c.Context()).Begin() - if tx.Error != nil { - s.Log.Errorf("Failed to begin transaction: %+v", tx.Error) - return tx.Error - } - rollback := func(err error) error { - if rerr := tx.Rollback().Error; rerr != nil { - s.Log.Errorf("Rollback failed: %+v", rerr) + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + chickinRepo := repository.NewChickinRepository(dbTransaction) + projectFlockKandangRepo := s.ProjectflockKandangRepo.WithTx(dbTransaction) + productWarehouseRepo := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) + projectFlockPopulationRepo := s.ProjectflockPopulationRepo.WithTx(dbTransaction) + projectChickinDetailRepo := s.ProjectChickinDetailRepo.WithTxRepo(dbTransaction) + warehouseRepoTx := rWarehouse.NewWarehouseRepository(dbTransaction) + + chickin, err := chickinRepo.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + return err + } + + population, err := projectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Project flock population not found") + } + return err + } + + newReserved := population.ReservedQuantity - chickin.Quantity + if newReserved < 0 { + newReserved = 0 + } + + err = projectFlockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{ + "reserved_quantity": newReserved, + }, nil) + if err != nil { + return err + } + + restoreFromDetails := func() (bool, error) { + + details, err := projectChickinDetailRepo.GetByProjectChickinID(c.Context(), chickin.Id) + if err != nil { + return false, err + } + + for _, d := range details { + productWarehouse, err := productWarehouseRepo.GetByID(c.Context(), d.ProductWarehouseId, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return false, err + } + + updatedQuantity := productWarehouse.Quantity + d.Quantity + if err := productWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { + return false, err + } + } + + if err := projectChickinDetailRepo.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("project_chickin_id = ?", chickin.Id) + }); err != nil { + return false, err + } + + return true, nil + } + + restored, err := restoreFromDetails() + if err != nil { + return err + } + + if !restored { + projectflockkandang, err := projectFlockKandangRepo.GetByID(c.Context(), population.ProjectFlockKandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") + } + return err + } + + warehouse, err := warehouseRepoTx.GetByKandangID(c.Context(), projectflockkandang.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang") + } + return err + } + + productWarehouse, err := productWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) + 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") + } + return err + } + + updatedQuantity := productWarehouse.Quantity + chickin.Quantity + + if err := productWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { + return err + } + } + + if err := chickinRepo.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + return err + } + + return nil + }) + + if err != nil { + if ferr, ok := err.(*fiber.Error); ok { + return ferr } return err } - chickinRepoTx := s.Repository.WithTx(tx) - pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx) - productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx) - - chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found")) - } - if err != nil { - s.Log.Errorf("Failed get chickin by id: %+v", err) - return rollback(err) - } - - var population entity.ProjectFlockPopulation - if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found")) - } - s.Log.Errorf("Failed to get project flock population: %+v", err) - return rollback(err) - } - - newReserved := population.ReservedQuantity - chickin.Quantity - if newReserved < 0 { - newReserved = 0 - } - if err := tx.WithContext(c.Context()).Model(&entity.ProjectFlockPopulation{}).Where("id = ?", population.Id).Updates(map[string]any{"reserved_quantity": newReserved}).Error; err != nil { - s.Log.Errorf("Failed to update project flock population: %+v", err) - return rollback(err) - } - - restoreFromDetails := func() (bool, error) { - var details []entity.ProjectChickinDetail - if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil { - return false, err - } - if len(details) == 0 { - return false, nil - } - - for _, d := range details { - var pw entity.ProductWarehouse - if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - - continue - } - return false, err - } - - updatedQuantity := pw.Quantity + d.Quantity - if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { - return false, err - } - } - - if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil { - return false, err - } - return true, nil - } - - restored, err := restoreFromDetails() - if err != nil { - s.Log.Errorf("Failed to restore from chickin details: %+v", err) - return rollback(err) - } - - if !restored { - - projectflockkandang, err := pfkRepoTx.GetByID(c.Context(), population.ProjectFlockKandangId) - if err != nil { - s.Log.Errorf("Failed to get projectflock kandang: %+v", err) - return rollback(err) - } - - var warehouse entity.Warehouse - if err := tx.WithContext(c.Context()).Where("kandang_id = ?", projectflockkandang.KandangId).First(&warehouse).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang")) - } - s.Log.Errorf("Failed to get warehouse: %+v", err) - return rollback(err) - } - - var productWarehouse entity.ProductWarehouse - err = tx.WithContext(c.Context()).Table("product_warehouses"). - Select("product_warehouses.*"). - Joins("JOIN products ON products.id = product_warehouses.product_id"). - Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). - Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id). - Order("product_warehouses.created_at DESC"). - First(&productWarehouse).Error - - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(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 rollback(err) - } - - updatedQuantity := productWarehouse.Quantity + chickin.Quantity - if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { - s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) - return rollback(err) - } - } - - // delete chickin (single place) - if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found")) - } - s.Log.Errorf("Failed to delete chickin: %+v", err) - return rollback(err) - } - - if err := tx.Commit().Error; err != nil { - s.Log.Errorf("Failed to commit transaction: %+v", err) - return rollback(err) - } - return nil } @@ -385,11 +372,8 @@ 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, - ) + chickin, err := s.Repository.GetByID(c.Context(), id, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Chickin not found") } 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 index cb4b0d5f..3328d286 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -9,8 +9,16 @@ import ( ) type ProjectFlockPopulationRepository interface { - repository.BaseRepository[entity.ProjectFlockPopulation] + // domain-specific GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) + + // subset of base repository methods used by services + CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error + PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error + + // transaction helpers + WithTx(tx *gorm.DB) ProjectFlockPopulationRepository + DB() *gorm.DB } type projectFlockPopulationRepositoryImpl struct { @@ -23,6 +31,16 @@ func NewProjectFlockPopulationRepository(db *gorm.DB) ProjectFlockPopulationRepo } } +func (r *projectFlockPopulationRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockPopulationRepository { + return &projectFlockPopulationRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockPopulation](tx), + } +} + +func (r *projectFlockPopulationRepositoryImpl) DB() *gorm.DB { + return r.BaseRepositoryImpl.DB() +} + func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) { var record entity.ProjectFlockPopulation err := r.DB().WithContext(ctx). From 31bb28f7daf38c7116a1991be7688dafe8340448 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 30 Oct 2025 09:06:21 +0700 Subject: [PATCH 02/59] Feat(BE-127): create migration for transfer to laying and inisiate module --- ...825_create_laying_transfers_table.down.sql | 1 + ...74825_create_laying_transfers_table.up.sql | 42 +++++ ...te_laying_kandang_transfers_table.down.sql | 1 + ...eate_laying_kandang_transfers_table.up.sql | 40 +++++ internal/entities/transfer_laying.go | 18 +++ internal/modules/production/route.go | 4 +- .../controllers/transfer_laying.controller.go | 144 ++++++++++++++++++ .../dto/transfer_laying.dto.go | 64 ++++++++ .../production/transfer_layings/module.go | 26 ++++ .../transfer_laying.repository.go | 21 +++ .../production/transfer_layings/route.go | 28 ++++ .../services/transfer_laying.service.go | 129 ++++++++++++++++ .../validations/transfer_laying.validation.go | 15 ++ 13 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql create mode 100644 internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql create mode 100644 internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql create mode 100644 internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql create mode 100644 internal/entities/transfer_laying.go create mode 100644 internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go create mode 100644 internal/modules/production/transfer_layings/dto/transfer_laying.dto.go create mode 100644 internal/modules/production/transfer_layings/module.go create mode 100644 internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go create mode 100644 internal/modules/production/transfer_layings/route.go create mode 100644 internal/modules/production/transfer_layings/services/transfer_laying.service.go create mode 100644 internal/modules/production/transfer_layings/validations/transfer_laying.validation.go diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql new file mode 100644 index 00000000..6ad77820 --- /dev/null +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS laying_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql new file mode 100644 index 00000000..8677ca71 --- /dev/null +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -0,0 +1,42 @@ +CREATE TABLE IF NOT EXISTS laying_transfers ( + id BIGSERIAL PRIMARY KEY, + from_project_flock_id BIGINT NOT NULL, + to_project_flock_id BIGINT NOT NULL, + total_quantity INTEGER, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ, + created_by BIGINT +); + +-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_from_project_flock + FOREIGN KEY (from_project_flock_id) + REFERENCES project_flocks(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_to_project_flock + FOREIGN KEY (to_project_flock_id) + REFERENCES project_flocks(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_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_laying_transfers_from_project_flock_id ON laying_transfers(from_project_flock_id); +CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers(to_project_flock_id); +CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers(created_by); +CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers(deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql new file mode 100644 index 00000000..caf4f52d --- /dev/null +++ b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS laying_kandang_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql new file mode 100644 index 00000000..786f8de3 --- /dev/null +++ b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql @@ -0,0 +1,40 @@ +CREATE TABLE IF NOT EXISTS laying_kandang_transfers ( + id BIGSERIAL PRIMARY KEY, + kandang_id BIGINT, + product_warehouse_id BIGINT, + qty NUMERIC(15,3), + laying_transfer_id BIGINT NOT NULL +); + +-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN + ALTER TABLE laying_kandang_transfers + ADD CONSTRAINT fk_laying_kandang_transfers_kandang + FOREIGN KEY (kandang_id) + REFERENCES kandangs(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE laying_kandang_transfers + ADD CONSTRAINT fk_laying_kandang_transfers_product_warehouse + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN + ALTER TABLE laying_kandang_transfers + ADD CONSTRAINT fk_laying_kandang_transfers_laying_transfer + FOREIGN KEY (laying_transfer_id) + REFERENCES laying_transfers(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- INDEXES +CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_kandang_id ON laying_kandang_transfers(kandang_id); +CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_product_warehouse_id ON laying_kandang_transfers(product_warehouse_id); +CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_laying_transfer_id ON laying_kandang_transfers(laying_transfer_id); \ No newline at end of file diff --git a/internal/entities/transfer_laying.go b/internal/entities/transfer_laying.go new file mode 100644 index 00000000..fd60a435 --- /dev/null +++ b/internal/entities/transfer_laying.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type TransferLaying struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index b41ef1e7..10effb58 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -10,6 +10,7 @@ import ( 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" + transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings" // MODULE IMPORTS ) @@ -20,8 +21,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida projectflocks.ProjectflockModule{}, recordings.RecordingModule{}, chickins.ChickinModule{}, + transferLayings.TransferLayingModule{}, // MODULE REGISTRY - } +} for _, m := range allModules { m.RegisterRoutes(group, db, validate) diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go new file mode 100644 index 00000000..01f49867 --- /dev/null +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type TransferLayingController struct { + TransferLayingService service.TransferLayingService +} + +func NewTransferLayingController(transferLayingService service.TransferLayingService) *TransferLayingController { + return &TransferLayingController{ + TransferLayingService: transferLayingService, + } +} + +func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.TransferLayingService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all transferLayings successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToTransferLayingListDTOs(result), + }) +} + +func (u *TransferLayingController) 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.TransferLayingService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get transferLaying successfully", + Data: dto.ToTransferLayingListDTO(*result), + }) +} + +func (u *TransferLayingController) 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.TransferLayingService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create transferLaying successfully", + Data: dto.ToTransferLayingListDTO(*result), + }) +} + +func (u *TransferLayingController) 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.TransferLayingService.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 transferLaying successfully", + Data: dto.ToTransferLayingListDTO(*result), + }) +} + +func (u *TransferLayingController) 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.TransferLayingService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete transferLaying successfully", + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go new file mode 100644 index 00000000..103c4c35 --- /dev/null +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -0,0 +1,64 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type TransferLayingBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type TransferLayingListDTO struct { + TransferLayingBaseDTO + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TransferLayingDetailDTO struct { + TransferLayingListDTO +} + +// === Mapper Functions === + +func ToTransferLayingBaseDTO(e entity.TransferLaying) TransferLayingBaseDTO { + return TransferLayingBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToTransferLayingListDTO(e entity.TransferLaying) TransferLayingListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + return TransferLayingListDTO{ + TransferLayingBaseDTO: ToTransferLayingBaseDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToTransferLayingListDTOs(e []entity.TransferLaying) []TransferLayingListDTO { + result := make([]TransferLayingListDTO, len(e)) + for i, r := range e { + result[i] = ToTransferLayingListDTO(r) + } + return result +} + +func ToTransferLayingDetailDTO(e entity.TransferLaying) TransferLayingDetailDTO { + return TransferLayingDetailDTO{ + TransferLayingListDTO: ToTransferLayingListDTO(e), + } +} diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go new file mode 100644 index 00000000..d3c6279c --- /dev/null +++ b/internal/modules/production/transfer_layings/module.go @@ -0,0 +1,26 @@ +package transfer_layings + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" + sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type TransferLayingModule struct{} + +func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db) + userRepo := rUser.NewUserRepository(db) + + transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + TransferLayingRoutes(router, userService, transferLayingService) +} + diff --git a/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go b/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go new file mode 100644 index 00000000..32ea27ee --- /dev/null +++ b/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type TransferLayingRepository interface { + repository.BaseRepository[entity.TransferLaying] +} + +type TransferLayingRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.TransferLaying] +} + +func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { + return &TransferLayingRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.TransferLaying](db), + } +} diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go new file mode 100644 index 00000000..ee806506 --- /dev/null +++ b/internal/modules/production/transfer_layings/route.go @@ -0,0 +1,28 @@ +package transfer_layings + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/controllers" + transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.TransferLayingService) { + ctrl := controller.NewTransferLayingController(s) + + route := v1.Group("/transfer_layings") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go new file mode 100644 index 00000000..36541600 --- /dev/null +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -0,0 +1,129 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type TransferLayingService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.TransferLaying, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type transferLayingService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.TransferLayingRepository +} + +func NewTransferLayingService(repo repository.TransferLayingRepository, validate *validator.Validate) TransferLayingService { + return &transferLayingService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + +func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.Search != "" { + return db.Where("name LIKE ?", "%"+params.Search+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get transferLayings: %+v", err) + return nil, 0, err + } + return transferLayings, total, nil +} + +func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.TransferLaying, error) { + transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + if err != nil { + s.Log.Errorf("Failed get transferLaying by id: %+v", err) + return nil, err + } + return transferLaying, nil +} + +func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + createBody := &entity.TransferLaying{ + Name: req.Name, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create transferLaying: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.Name != nil { + updateBody["name"] = *req.Name + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + s.Log.Errorf("Failed to update transferLaying: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + s.Log.Errorf("Failed to delete transferLaying: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go new file mode 100644 index 00000000..7d16d3ee --- /dev/null +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` +} From bf14ab78652d7e87fc6b34013a288a987ba36cd3 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 31 Oct 2025 14:27:08 +0700 Subject: [PATCH 03/59] fix(BE): Change migration chickin and project flock population to refactored one --- ...74825_create_laying_transfers_table.up.sql | 14 +++-- ...1030134228_refactor_project_chikins.up.sql | 5 ++ ...27_recreate_project_chikins_table.down.sql | 0 ...4527_recreate_project_chikins_table.up.sql | 58 +++++++++++++++++++ ...ecreate_project_flock_populations.down.sql | 0 ..._recreate_project_flock_populations.up.sql | 57 ++++++++++++++++++ 6 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 internal/database/migrations/20251030134228_refactor_project_chikins.up.sql create mode 100644 internal/database/migrations/20251030134527_recreate_project_chikins_table.down.sql create mode 100644 internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql create mode 100644 internal/database/migrations/20251030134542_recreate_project_flock_populations.down.sql create mode 100644 internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql index 8677ca71..10ced117 100644 --- a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -2,7 +2,8 @@ CREATE TABLE IF NOT EXISTS laying_transfers ( id BIGSERIAL PRIMARY KEY, from_project_flock_id BIGINT NOT NULL, to_project_flock_id BIGINT NOT NULL, - total_quantity INTEGER, + transfer_date DATE NOT NULL, + total_qty INTEGER, notes TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), @@ -36,7 +37,10 @@ BEGIN END $$; -- INDEXES -CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers(from_project_flock_id); -CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers(to_project_flock_id); -CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers(created_by); -CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers(deleted_at); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id); + +CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id); + +CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers (created_by); + +CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251030134228_refactor_project_chikins.up.sql b/internal/database/migrations/20251030134228_refactor_project_chikins.up.sql new file mode 100644 index 00000000..98ac9b73 --- /dev/null +++ b/internal/database/migrations/20251030134228_refactor_project_chikins.up.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS project_chickin_details; + +DROP TABLE IF EXISTS project_chickins; + +DROP TABLE IF EXISTS project_flock_populations; \ No newline at end of file diff --git a/internal/database/migrations/20251030134527_recreate_project_chikins_table.down.sql b/internal/database/migrations/20251030134527_recreate_project_chikins_table.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql new file mode 100644 index 00000000..c00765b1 --- /dev/null +++ b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql @@ -0,0 +1,58 @@ +-- ============================================ +-- MIGRATION: project_chickins +-- ============================================ + +-- STEP 1: Hapus tabel jika sudah ada + +-- STEP 2: Buat tabel project_chickins +CREATE TABLE IF NOT EXISTS project_chickins ( + id BIGSERIAL PRIMARY KEY, + project_flock_kandang_id BIGINT NOT NULL, + product_warehouse_id BIGINT NOT NULL, + chick_in_date DATE NOT NULL, + usage_qty NUMERIC(15, 3) NOT NULL, + pending_usage_qty NUMERIC(15, 3) DEFAULT 0, + notes TEXT, + created_by BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- STEP 3: FOREIGN KEYS +DO $$ +BEGIN + -- Relasi ke project_flock_kandangs + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE project_chickins + ADD CONSTRAINT fk_project_chickins_kandang + FOREIGN KEY (project_flock_kandang_id) + REFERENCES project_flock_kandangs(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + -- Relasi ke product_warehouses + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE project_chickins + ADD CONSTRAINT fk_project_chickins_warehouse + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + -- Relasi ke users + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE project_chickins + ADD CONSTRAINT fk_project_chickins_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- STEP 4: INDEXES +CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id); + +CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id); + +CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by); \ No newline at end of file diff --git a/internal/database/migrations/20251030134542_recreate_project_flock_populations.down.sql b/internal/database/migrations/20251030134542_recreate_project_flock_populations.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql new file mode 100644 index 00000000..2cb76e8f --- /dev/null +++ b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql @@ -0,0 +1,57 @@ +-- ============================================ +-- MIGRATION: project_flock_populations +-- ============================================ + +-- STEP 1: Hapus tabel jika sudah ada + +-- STEP 2: Buat tabel project_flock_populations +CREATE TABLE IF NOT EXISTS project_flock_populations ( + id BIGSERIAL PRIMARY KEY, + project_chickin_id BIGINT NOT NULL, + product_warehouse_id BIGINT NOT NULL, + total_qty NUMERIC(15, 3) NOT NULL, + total_used_qty NUMERIC(15, 3) DEFAULT 0, + notes TEXT, + created_by BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- STEP 3: FOREIGN KEYS +DO $$ +BEGIN + -- Relasi ke project_chickins + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN + ALTER TABLE project_flock_populations + ADD CONSTRAINT fk_project_flock_populations_chickin + FOREIGN KEY (project_chickin_id) + REFERENCES project_chickins(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + -- Relasi ke product_warehouses + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE project_flock_populations + ADD CONSTRAINT fk_project_flock_populations_warehouse + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + -- Relasi ke users + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE project_flock_populations + ADD CONSTRAINT fk_project_flock_populations_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- STEP 4: INDEXES +CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id); + +CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id); + +CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by); \ No newline at end of file From c91d84b652468a7a19e9149518ff8dffe9e8e2b5 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 31 Oct 2025 14:30:45 +0700 Subject: [PATCH 04/59] feat[BE-127]: inisiate transfer laying for base template API --- internal/entities/laying_kandang_transfer.go | 22 +++++ internal/entities/laying_transfer.go | 24 +++++ internal/entities/transfer_laying.go | 18 ---- .../dto/transfer_laying.dto.go | 12 +-- ...itory.go => laying_transfer.repository.go} | 9 +- .../services/transfer_laying.service.go | 88 ++++++++++++++----- .../validations/transfer_laying.validation.go | 13 ++- 7 files changed, 134 insertions(+), 52 deletions(-) create mode 100644 internal/entities/laying_kandang_transfer.go create mode 100644 internal/entities/laying_transfer.go delete mode 100644 internal/entities/transfer_laying.go rename internal/modules/production/transfer_layings/repositories/{transfer_laying.repository.go => laying_transfer.repository.go} (67%) diff --git a/internal/entities/laying_kandang_transfer.go b/internal/entities/laying_kandang_transfer.go new file mode 100644 index 00000000..8f514f71 --- /dev/null +++ b/internal/entities/laying_kandang_transfer.go @@ -0,0 +1,22 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type LayingKandangTransfer struct { + Id uint `gorm:"primaryKey"` + KandangId uint + ProductWarehouseId uint + Qty float64 `gorm:"type:numeric(15,3)"` + LayingTransferId uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` + + Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"` +} diff --git a/internal/entities/laying_transfer.go b/internal/entities/laying_transfer.go new file mode 100644 index 00000000..75c5e23a --- /dev/null +++ b/internal/entities/laying_transfer.go @@ -0,0 +1,24 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type LayingTransfer struct { + Id uint `gorm:"primaryKey"` + FromProjectFlockId uint `gorm:"not null"` + ToProjectFlockId uint `gorm:"not null"` + TotalQty float64 `gorm:"type:numeric(15,3)"` + TransferDate time.Time `gorm:"type:date;not null"` + Notes string `gorm:"type:text"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` + + FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` + ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/entities/transfer_laying.go b/internal/entities/transfer_laying.go deleted file mode 100644 index fd60a435..00000000 --- a/internal/entities/transfer_laying.go +++ /dev/null @@ -1,18 +0,0 @@ -package entities - -import ( - "time" - - "gorm.io/gorm" -) - -type TransferLaying struct { - Id uint `gorm:"primaryKey"` - Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` - CreatedBy uint `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` -} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 103c4c35..69aced33 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -27,17 +27,17 @@ type TransferLayingDetailDTO struct { // === Mapper Functions === -func ToTransferLayingBaseDTO(e entity.TransferLaying) TransferLayingBaseDTO { +func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO { return TransferLayingBaseDTO{ Id: e.Id, - Name: e.Name, + } } -func ToTransferLayingListDTO(e entity.TransferLaying) TransferLayingListDTO { +func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } @@ -49,7 +49,7 @@ func ToTransferLayingListDTO(e entity.TransferLaying) TransferLayingListDTO { } } -func ToTransferLayingListDTOs(e []entity.TransferLaying) []TransferLayingListDTO { +func ToTransferLayingListDTOs(e []entity.LayingTransfer) []TransferLayingListDTO { result := make([]TransferLayingListDTO, len(e)) for i, r := range e { result[i] = ToTransferLayingListDTO(r) @@ -57,7 +57,7 @@ func ToTransferLayingListDTOs(e []entity.TransferLaying) []TransferLayingListDTO return result } -func ToTransferLayingDetailDTO(e entity.TransferLaying) TransferLayingDetailDTO { +func ToTransferLayingDetailDTO(e entity.LayingTransfer) TransferLayingDetailDTO { return TransferLayingDetailDTO{ TransferLayingListDTO: ToTransferLayingListDTO(e), } diff --git a/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go similarity index 67% rename from internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go rename to internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index 32ea27ee..e92e8f95 100644 --- a/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -1,21 +1,22 @@ package repository import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" ) type TransferLayingRepository interface { - repository.BaseRepository[entity.TransferLaying] + repository.BaseRepository[entity.LayingTransfer] + } type TransferLayingRepositoryImpl struct { - *repository.BaseRepositoryImpl[entity.TransferLaying] + *repository.BaseRepositoryImpl[entity.LayingTransfer] } func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { return &TransferLayingRepositoryImpl{ - BaseRepositoryImpl: repository.NewBaseRepository[entity.TransferLaying](db), + BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransfer](db), } } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 36541600..bc66bd13 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -3,7 +3,9 @@ package service import ( "errors" + common "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -15,24 +17,28 @@ import ( ) type TransferLayingService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.TransferLaying, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) DeleteOne(ctx *fiber.Ctx, id uint) error } type transferLayingService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.TransferLayingRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.TransferLayingRepository + ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository + ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository } -func NewTransferLayingService(repo repository.TransferLayingRepository, validate *validator.Validate) TransferLayingService { +func NewTransferLayingService(repo repository.TransferLayingRepository, projectFlockRepo ProjectFlockRepository.ProjectflockRepository, projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository, validate *validator.Validate) TransferLayingService { return &transferLayingService{ - Log: utils.Log, - Validate: validate, - Repository: repo, + Log: utils.Log, + Validate: validate, + Repository: repo, + ProjectFlockRepo: projectFlockRepo, + ProjectFlockKandangRepo: projectFlockKandangRepo, } } @@ -40,7 +46,7 @@ func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { return db.Preload("CreatedUser") } -func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) { +func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } @@ -62,7 +68,7 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return transferLayings, total, nil } -func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.TransferLaying, error) { +func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) { transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") @@ -74,24 +80,62 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.TransferLa return transferLaying, nil } -func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) { +func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - createBody := &entity.TransferLaying{ - Name: req.Name, - } - - if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { - s.Log.Errorf("Failed to create transferLaying: %+v", err) + if err := common.EnsureRelations(c.Context(), + common.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + common.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + ); err != nil { return nil, err } - return s.GetOne(c, createBody.Id) + for _, detail := range req.Details { + if err := common.EnsureRelations(c.Context(), + common.RelationCheck{Name: "Project Flock Kandang", ID: &detail.SourceProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + ); err != nil { + return nil, err + } + } + + transferDate, err := utils.ParseDateString(req.TransferDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") + } + + var totalQty float64 + for _, item := range req.Details { + totalQty += item.Quantity + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + createBody := &entity.LayingTransfer{ + Notes: req.Reason, + FromProjectFlockId: req.SourceProjectFlockId, + ToProjectFlockId: req.TargetProjectFlockId, + TransferDate: transferDate, + TotalQty: totalQty, + CreatedBy: 1, //todo : harus diambil dari auth + } + + if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + return nil, err } -func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) { +func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 7d16d3ee..3b83132f 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -1,11 +1,20 @@ package validation +type CreateDetail struct { + SourceProjectFlockKandangId uint `json:"source_project_flock_kandang_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` +} + type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` + TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"` + SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"` + TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"` + Details []CreateDetail `json:"details" validate:"required,min=1,dive,required"` + Reason string `json:"reason" validate:"omitempty,max=1000"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + Name *string `json:"name,omitempty" validate:"omitempty"` } type Query struct { From 219a6a39edee3e6195cbc56a16c51f405bdd0f81 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 31 Oct 2025 15:33:31 +0700 Subject: [PATCH 05/59] Feat[BE]: refactored Chickin createone and implement approvals and add more needed constant --- internal/database/seed/seeder.go | 272 ++++++------ internal/entities/project_chickin.go | 11 +- internal/entities/project_flock_population.go | 24 +- internal/entities/projectflock.go | 1 + internal/entities/projectflock_kandang.go | 14 +- .../repositories/constant.repository.go | 4 + .../controllers/chickin.controller.go | 28 +- .../production/chickins/dto/chickin.dto.go | 36 +- .../modules/production/chickins/module.go | 13 + internal/modules/production/chickins/route.go | 2 +- .../chickins/services/chickin.service.go | 414 +++++++++--------- .../validations/chickin.validation.go | 8 +- .../repositories/projectflock.repository.go | 5 + .../projectflock_kandang.repository.go | 5 + .../recordings/services/recording.service.go | 25 +- .../production/transfer_layings/module.go | 6 +- internal/utils/constant.go | 32 ++ 17 files changed, 485 insertions(+), 415 deletions(-) diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 791cfddb..ca8e8cf3 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -93,9 +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 - } + // if err := seedChickin(tx, adminID); err != nil { + // return err + // } fmt.Println("✅ Master data seeding completed") return nil @@ -1134,151 +1134,151 @@ 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"}, - } +// 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 - } +// 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 - } +// // Insert ProjectChickin jika belum ada +// var chickin entity.ProjectChickin +// err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate). +// First(&chickin).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// chickin = entity.ProjectChickin{ +// ProjectFlockKandangId: seed.ProjectFlockKandangId, +// ChickInDate: chickinDate, +// Quantity: seed.Quantity, +// Note: seed.Note, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&chickin).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } - var population entity.ProjectFlockPopulation - err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - population = entity.ProjectFlockPopulation{ - ProjectFlockKandangId: seed.ProjectFlockKandangId, - InitialQuantity: seed.Quantity, - CurrentQuantity: seed.Quantity, - ReservedQuantity: 0, - CreatedBy: createdBy, - } - if err := tx.Create(&population).Error; err != nil { - return err - } - } else if err != nil { - return err - } else { - // Update population quantities - if err := tx.Model(&entity.ProjectFlockPopulation{}). - Where("id = ?", population.Id). - Updates(map[string]any{ - "initial_quantity": population.InitialQuantity + seed.Quantity, - "current_quantity": population.CurrentQuantity + seed.Quantity, - "reserved_quantity": 0, - }).Error; err != nil { - return err - } - } +// var population entity.ProjectFlockPopulation +// err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// population = entity.ProjectFlockPopulation{ +// ProjectFlockKandangId: seed.ProjectFlockKandangId, +// InitialQuantity: seed.Quantity, +// CurrentQuantity: seed.Quantity, +// ReservedQuantity: 0, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&population).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// // Update population quantities +// if err := tx.Model(&entity.ProjectFlockPopulation{}). +// Where("id = ?", population.Id). +// Updates(map[string]any{ +// "initial_quantity": population.InitialQuantity + seed.Quantity, +// "current_quantity": population.CurrentQuantity + seed.Quantity, +// "reserved_quantity": 0, +// }).Error; err != nil { +// return err +// } +// } - var pfk entity.ProjectFlockKandang - if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - // no pivot found; skip creating details - continue - } - return err - } +// var pfk entity.ProjectFlockKandang +// if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil { +// if errors.Is(err, gorm.ErrRecordNotFound) { +// // no pivot found; skip creating details +// continue +// } +// return err +// } - var warehouse entity.Warehouse - if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil { - // if warehouse not found, cannot create details - if errors.Is(err, gorm.ErrRecordNotFound) { - continue - } - return err - } +// var warehouse entity.Warehouse +// if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil { +// // if warehouse not found, cannot create details +// if errors.Is(err, gorm.ErrRecordNotFound) { +// continue +// } +// return err +// } - var productWarehouses []entity.ProductWarehouse - err = tx.Table("product_warehouses"). - Select("product_warehouses.*"). - Joins("JOIN products ON products.id = product_warehouses.product_id"). - Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). - Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id). - Order("product_warehouses.created_at DESC"). - Find(&productWarehouses).Error - if err != nil { - return err - } +// var productWarehouses []entity.ProductWarehouse +// err = tx.Table("product_warehouses"). +// Select("product_warehouses.*"). +// Joins("JOIN products ON products.id = product_warehouses.product_id"). +// Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). +// Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id). +// Order("product_warehouses.created_at DESC"). +// Find(&productWarehouses).Error +// if err != nil { +// return err +// } - // If no product warehouses found, keep existing chickin.Quantity and skip details - if len(productWarehouses) == 0 { - continue - } +// // If no product warehouses found, keep existing chickin.Quantity and skip details +// if len(productWarehouses) == 0 { +// continue +// } - // sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne) - totalQty := 0.0 - for _, pw := range productWarehouses { - totalQty += pw.Quantity - } +// // sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne) +// totalQty := 0.0 +// for _, pw := range productWarehouses { +// totalQty += pw.Quantity +// } - if chickin.Quantity != totalQty { - if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil { - return err - } - chickin.Quantity = totalQty - } +// if chickin.Quantity != totalQty { +// if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil { +// return err +// } +// chickin.Quantity = totalQty +// } - for _, pw := range productWarehouses { - // ensure detail exists or create it with full pw.Quantity - var detail entity.ProjectChickinDetail - err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - detail = entity.ProjectChickinDetail{ - ProjectChickinId: chickin.Id, - ProductWarehouseId: pw.Id, - Quantity: pw.Quantity, - CreatedBy: createdBy, - } - if err := tx.Create(&detail).Error; err != nil { - return err - } - } else if err != nil { - return err - } else { - if detail.Quantity != pw.Quantity { - if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil { - return err - } - } - } +// for _, pw := range productWarehouses { +// // ensure detail exists or create it with full pw.Quantity +// var detail entity.ProjectChickinDetail +// err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// detail = entity.ProjectChickinDetail{ +// ProjectChickinId: chickin.Id, +// ProductWarehouseId: pw.Id, +// Quantity: pw.Quantity, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&detail).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// if detail.Quantity != pw.Quantity { +// if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil { +// return err +// } +// } +// } - // zero out pw quantity - if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil { - return err - } - } - } +// // zero out pw quantity +// if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil { +// return err +// } +// } +// } - return nil -} +// return nil +// } func ptr[T any](v T) *T { return &v diff --git a/internal/entities/project_chickin.go b/internal/entities/project_chickin.go index 95a658c8..c3ca2671 100644 --- a/internal/entities/project_chickin.go +++ b/internal/entities/project_chickin.go @@ -12,13 +12,16 @@ 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"` + ProductWarehouseId uint `gorm:"not null"` + UsageQty float64 `gorm:"type:numeric(15,3);not null"` + PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"` + Notes 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"` + ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;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 index 184ace65..e5b3216c 100644 --- a/internal/entities/project_flock_population.go +++ b/internal/entities/project_flock_population.go @@ -7,16 +7,18 @@ import ( ) 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"` + Id uint `gorm:"primaryKey"` + ProjectChickinId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + TotalQty float64 `gorm:"type:numeric(15,3);not null"` + TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"` + Notes string `gorm:"type:text"` + 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"` + ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/entities/projectflock.go b/internal/entities/projectflock.go index c840892f..4c05298f 100644 --- a/internal/entities/projectflock.go +++ b/internal/entities/projectflock.go @@ -28,3 +28,4 @@ type ProjectFlock struct { KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` LatestApproval *Approval `gorm:"-" json:"-"` } + diff --git a/internal/entities/projectflock_kandang.go b/internal/entities/projectflock_kandang.go index 1c29c22e..f10dbd17 100644 --- a/internal/entities/projectflock_kandang.go +++ b/internal/entities/projectflock_kandang.go @@ -3,10 +3,12 @@ package entities import "time" type ProjectFlockKandang struct { - Id uint `gorm:"primaryKey"` - ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"` - KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"` - CreatedAt time.Time `gorm:"autoCreateTime"` - ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` - Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` + Id uint `gorm:"primaryKey"` + ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"` + KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"` + CreatedAt time.Time `gorm:"autoCreateTime"` + + ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` + Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` + Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` } diff --git a/internal/modules/constants/repositories/constant.repository.go b/internal/modules/constants/repositories/constant.repository.go index 4b44d553..493f4cb9 100644 --- a/internal/modules/constants/repositories/constant.repository.go +++ b/internal/modules/constants/repositories/constant.repository.go @@ -82,6 +82,10 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} { "LOKASI", "KANDANG", }, + "stock_log": map[string][]string{ + "log_types": []string{"TRANSFER", "ADJUSTMENT"}, + "transaction_types": []string{"INCREASE", "DECREASE"}, + }, "supplier_categories": []string{ "BOP", "SAPRONAK", diff --git a/internal/modules/production/chickins/controllers/chickin.controller.go b/internal/modules/production/chickins/controllers/chickin.controller.go index fadcbc3e..c132b279 100644 --- a/internal/modules/production/chickins/controllers/chickin.controller.go +++ b/internal/modules/production/chickins/controllers/chickin.controller.go @@ -139,23 +139,33 @@ func (u *ChickinController) DeleteOne(c *fiber.Ctx) error { }) } -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") +func (u *ChickinController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - if err := u.ChickinService.Approve(c, uint(id)); err != nil { + results, err := u.ChickinService.Approval(c, req) + if err != nil { return err } + var ( + data interface{} + message = "Submit chickin approval successfully" + ) + if len(results) == 1 { + data = dto.ToChickinListDTO(results[0]) + } else { + message = "Submit chickin approvals successfully" + data = dto.ToChickinListDTOs(results) + } + return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", - Message: "Approve chickin successfully", - Data: nil, + Message: message, + Data: data, }) } diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 193257b6..823cbfa5 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -18,8 +18,10 @@ 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"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` } type ProjectFlockDTO struct { @@ -44,8 +46,10 @@ 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"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` CreatedBy uint `json:"created_by"` } @@ -138,16 +142,18 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { var pfk *ProjectFlockKandangDTO - if e.ProjectFlockKandang.Id != 0 { - mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + if e.ProjectFlockKandang != nil && 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, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, } } @@ -156,21 +162,23 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { Id: e.Id, ProjectFlockKandangId: e.ProjectFlockKandangId, ChickInDate: e.ChickInDate, - Quantity: e.Quantity, - Note: e.Note, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, CreatedBy: e.CreatedBy, } } func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { var createdUser *userBaseDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser) + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } var pfk *ProjectFlockKandangDTO - if e.ProjectFlockKandang.Id != 0 { - mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 { + mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang) pfk = &mapped } return ChickinListDTO{ diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index f1e0baea..f6dd554b 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -1,10 +1,15 @@ package chickins import ( + "fmt" + "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" + 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" @@ -15,6 +20,8 @@ import ( rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + utils "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type ChickinModule struct{} @@ -32,6 +39,12 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * userRepo := rUser.NewUserRepository(db) + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err)) + } + chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go index 5fa5237a..0bb5e93d 100644 --- a/internal/modules/production/chickins/route.go +++ b/internal/modules/production/chickins/route.go @@ -25,5 +25,5 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) - route.Post("/:id/approve", ctrl.Approve) + route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 83d94ba5..2b44c242 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -2,7 +2,11 @@ package service import ( "errors" + "fmt" + "strings" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" 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" @@ -24,7 +28,7 @@ type ChickinService interface { 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 + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) } type chickinService struct { @@ -110,107 +114,98 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } - projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) + projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) if err != nil { - s.Log.Errorf("Failed to get projectflock kandang: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") } - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId) + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId) if err != nil { - s.Log.Errorf("Failed to get warehouse: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found") } - // move complex DB query into repository for cleaner service - productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) - 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") - } - totalQuantity := 0.0 - for _, pw := range productWarehouses { - totalQuantity += pw.Quantity - } + var productWarehouses []entity.ProductWarehouse - if totalQuantity < 1 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses") + if strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) == string(utils.ProjectFlockCategoryGrowing) { + + productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) + if err != nil || len(productWarehouses) == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, "Product for growing category in the Kandang's warehouse not found") + } } 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.Id, - ChickInDate: chickinDate, - Quantity: totalQuantity, - Note: req.Note, - CreatedBy: 1, //todo: ganti dengan user login + + actorID := uint(1) // todo nanti ambil dari auth context + newChikins := make([]*entity.ProjectChickin, 0) + for _, productWarehouse := range productWarehouses { + + if productWarehouse.Quantity > 0 { + newChickin := &entity.ProjectChickin{ + ProjectFlockKandangId: req.ProjectFlockKandangId, + ChickInDate: chickinDate, + UsageQty: 0, + PendingUsageQty: productWarehouse.Quantity, + ProductWarehouseId: productWarehouse.Id, + Notes: req.Note, + CreatedBy: actorID, + } + + newChikins = append(newChikins, newChickin) + } } - err = s.Repository.CreateOne(c.Context(), newChickin, nil) + + if len(newChikins) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create") + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) + + if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil { + return err + } + + latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil) + if err != nil { + return err + } + + for _, chickin := range newChikins { + + updates := map[string]any{"quantity": 0} + + if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to update product warehouse quantity for id %d", chickin.ProductWarehouseId) + } + return err + } + } + + if latest == nil { + + action := entity.ApprovalActionCreated + if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &action, actorID, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + } + } + + return nil + }) if err != nil { - s.Log.Errorf("Failed to create chickin: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } - // 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 - } - - newChickinDetail := &entity.ProjectChickinDetail{ - ProjectChickinId: newChickin.Id, - ProductWarehouseId: pw.Id, - Quantity: pw.Quantity, - CreatedBy: 1, // todo: ganti dengan user login - } - err = s.ProjectChickinDetailRepo.CreateOne(c.Context(), newChickinDetail, nil) - if err != nil { - s.Log.Errorf("Failed to create chickin detail: %+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) + return newChikins[0], nil } func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { @@ -224,7 +219,8 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody["chick_in_date"] = req.ChickInDate } if req.Note != "" { - updateBody["note"] = req.Note + // entity uses `Notes` => column `notes` + updateBody["notes"] = req.Note } if len(updateBody) == 0 { return s.GetOne(c, id) @@ -243,160 +239,140 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { + // Simplified delete: directly call repository delete. Complex restore logic removed for now. + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + return err + } + + return nil +} + +func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + actorID := uint(1) // todo nanti ambil dari 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") + } + + approvableIDs := uniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + step := utils.ProjectFlockKandangStepPengajuan + if action == entity.ApprovalActionApproved { + step = utils.ProjectFlockKandangStepDisetujui + } + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) + chickinRepoTx := s.Repository.WithTx(dbTransaction) - chickinRepo := repository.NewChickinRepository(dbTransaction) - projectFlockKandangRepo := s.ProjectflockKandangRepo.WithTx(dbTransaction) - productWarehouseRepo := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) - projectFlockPopulationRepo := s.ProjectflockPopulationRepo.WithTx(dbTransaction) - projectChickinDetailRepo := s.ProjectChickinDetailRepo.WithTxRepo(dbTransaction) - warehouseRepoTx := rWarehouse.NewWarehouseRepository(dbTransaction) + for _, approvableID := range approvableIDs { - chickin, err := chickinRepo.GetByID(c.Context(), id, nil) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Chickin not found") - } - return err - } - - population, err := projectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Project flock population not found") - } - return err - } - - newReserved := population.ReservedQuantity - chickin.Quantity - if newReserved < 0 { - newReserved = 0 - } - - err = projectFlockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{ - "reserved_quantity": newReserved, - }, nil) - if err != nil { - return err - } - - restoreFromDetails := func() (bool, error) { - - details, err := projectChickinDetailRepo.GetByProjectChickinID(c.Context(), chickin.Id) + exists, err := s.ProjectflockKandangRepo.WithTx(dbTransaction).IdExists(c.Context(), approvableID) if err != nil { - return false, err + return err + } + if !exists { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) } - for _, d := range details { - productWarehouse, err := productWarehouseRepo.GetByID(c.Context(), d.ProductWarehouseId, nil) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - continue + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlockKandang, + approvableID, + step, + &action, + actorID, + req.Notes, + ); err != nil { + + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + s.Log.Infof("ignored duplicate approval for kandang %d: %v", approvableID, err) + } + + if action == entity.ApprovalActionApproved { + + var chickins []entity.ProjectChickin + if err := chickinRepoTx.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", approvableID).Find(&chickins).Error; err != nil { + return err + } + + for _, chickin := range chickins { + population := &entity.ProjectFlockPopulation{ + ProjectChickinId: chickin.Id, + ProductWarehouseId: chickin.ProductWarehouseId, + TotalQty: chickin.PendingUsageQty, + TotalUsedQty: 0, + Notes: chickin.Notes, + CreatedBy: actorID, + } + if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) } - return false, err } - updatedQuantity := productWarehouse.Quantity + d.Quantity - if err := productWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { - return false, err - } } - if err := projectChickinDetailRepo.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("project_chickin_id = ?", chickin.Id) - }); err != nil { - return false, err - } - - return true, nil - } - - restored, err := restoreFromDetails() - if err != nil { - return err - } - - if !restored { - projectflockkandang, err := projectFlockKandangRepo.GetByID(c.Context(), population.ProjectFlockKandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") - } - return err - } - - warehouse, err := warehouseRepoTx.GetByKandangID(c.Context(), projectflockkandang.KandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang") - } - return err - } - - productWarehouse, err := productWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) - 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") - } - return err - } - - updatedQuantity := productWarehouse.Quantity + chickin.Quantity - - if err := productWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { - return err - } - } - - if err := chickinRepo.DeleteOne(c.Context(), id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Chickin not found") - } - return err } return nil }) if err != nil { - if ferr, ok := err.(*fiber.Error); ok { - return ferr + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr } - return err + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + s.Log.Errorf("Failed to record approval for chickins %+v: %+v", approvableIDs, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } - return nil + updated := make([]entity.ProjectChickin, 0) + for _, kandangID := range approvableIDs { + var chickins []entity.ProjectChickin + if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil { + return nil, err + } + updated = append(updated, chickins...) + } + + return updated, 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") +func uniqueUintSlice(values []uint) []uint { + seen := make(map[uint]struct{}, len(values)) + result := make([]uint, 0, len(values)) + for _, v := range values { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + result = append(result, v) } - 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 + return result } diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go index 9747ee07..c2676130 100644 --- a/internal/modules/production/chickins/validations/chickin.validation.go +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -3,7 +3,7 @@ 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"` - Note string `json:"note" validate:"omitempty` + Note string `json:"note" validate:"omitempty"` } type Update struct { @@ -16,3 +16,9 @@ type Query struct { Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` } + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 476b061b..bc5a0c6b 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -16,6 +16,7 @@ type ProjectflockRepository interface { GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) + IdExists(ctx context.Context, id uint) (bool, error) } type ProjectflockRepositoryImpl struct { @@ -28,6 +29,10 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository { } } +func (r *ProjectflockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.ProjectFlock](ctx, r.DB(), id) +} + func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) { var records []entity.ProjectFlock if err := r.DB().WithContext(ctx). 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 5c78f830..7205b72a 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -3,6 +3,7 @@ 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" ) @@ -14,6 +15,7 @@ type ProjectFlockKandangRepository interface { DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) WithTx(tx *gorm.DB) ProjectFlockKandangRepository + IdExists(ctx context.Context, id uint) (bool, error) DB() *gorm.DB } @@ -67,6 +69,9 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB { return r.db } +func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id) +} func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) { record := new(entity.ProjectFlockKandang) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 46ba36cc..b3bdaeaf 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -670,18 +670,19 @@ func (s *recordingService) getPreviousRecording(tx *gorm.DB, projectFlockKandang } func (s *recordingService) getTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) { - var population entity.ProjectFlockPopulation - err := tx. - Where("project_flock_kandang_id = ?", projectFlockKandangId). - Order("created_at DESC"). - First(&population).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, nil - } - if err != nil { - return 0, err - } - return int64(math.Round(population.InitialQuantity)), nil + // var population entity.ProjectFlockPopulation + // err := tx. + // Where("project_flock_kandang_id = ?", projectFlockKandangId). + // Order("created_at DESC"). + // First(&population).Error + // if errors.Is(err, gorm.ErrRecordNotFound) { + // return 0, nil + // } + // if err != nil { + // return 0, err + // } + //todo : nanti ganti lagi mas saya hardcode dulu + return int64(math.Round(1000)), nil } func (s *recordingService) getAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) { diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go index d3c6279c..b309f19e 100644 --- a/internal/modules/production/transfer_layings/module.go +++ b/internal/modules/production/transfer_layings/module.go @@ -5,6 +5,7 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" @@ -17,10 +18,11 @@ type TransferLayingModule struct{} func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db) userRepo := rUser.NewUserRepository(db) + projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) + projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) - transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, validate) + transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, projectFlockRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) TransferLayingRoutes(router, userService, transferLayingService) } - diff --git a/internal/utils/constant.go b/internal/utils/constant.go index bdbc53b6..8328f4d7 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -79,6 +79,24 @@ const ( WarehouseTypeKandang WarehouseType = "KANDANG" ) +// ------------------------------------------------------------------- +// Stock log +// ------------------------------------------------------------------- + +type StockLogTransactionType string + +const ( + StockLogTransactionTypeIncrease StockLogTransactionType = "INCREASE" + StockLogTransactionTypeDecrease StockLogTransactionType = "DECREASE" +) + +type StockLogType string + +const ( + StockLogTypeAdjustment StockLogType = "ADJUSTMENT" + StockLogTypeTransfer StockLogType = "TRANSFER" +) + // ------------------------------------------------------------------- // WarehouseType // ------------------------------------------------------------------- @@ -140,6 +158,20 @@ var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{ ProjectFlockStepAktif: "Aktif", } +// ------------------------------------------------------------------- +// Project Flock Kandang Approval +// ------------------------------------------------------------------- +const ( + ApprovalWorkflowProjectFlockKandang approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCK_KANDANGS") + ProjectFlockKandangStepPengajuan approvalutils.ApprovalStep = 1 + ProjectFlockKandangStepDisetujui approvalutils.ApprovalStep = 2 +) + +var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{ + ProjectFlockKandangStepPengajuan: "Pengajuan", + ProjectFlockKandangStepDisetujui: "Disetujui", +} + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- From 20f1be2ef82a332b71343922431a503ec98d39df Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sun, 2 Nov 2025 21:06:03 +0700 Subject: [PATCH 06/59] feat[BE]: Refactor Chickin create and approvals support chickin growing and chickin laying, and create get one project flock kandang API --- ...0649_create_project_chick_ins_table.up.sql | 36 -- ...ate_project_flock_populations_table.up.sql | 36 -- ...4829_create_project_chickin_details.up.sql | 2 +- ...4527_recreate_project_chikins_table.up.sql | 60 +-- ..._recreate_project_flock_populations.up.sql | 65 ++-- internal/database/seed/seeder.go | 21 ++ internal/entities/projectflock_kandang.go | 7 +- .../product_warehouse.repository.go | 17 + .../production/chickins/dto/chickin.dto.go | 36 +- .../project_chickin.repository.go | 17 +- .../chickins/services/chickin.service.go | 224 ++++++++--- .../project_flock_kandang.controller.go | 76 ++++ .../dto/project_flock_kandang.dto.go | 353 ++++++++++++++++++ .../project-flock-kandangs/module.go | 42 +++ .../project-flock-kandangs/route.go | 26 ++ .../services/project_flock_kandang.service.go | 147 ++++++++ .../project_flock_kandang.validation.go | 17 + .../project_flock_population_repository.go | 13 + .../projectflock_kandang.repository.go | 9 + internal/modules/production/route.go | 2 + internal/utils/constant.go | 2 + 21 files changed, 1002 insertions(+), 206 deletions(-) delete mode 100644 internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql delete mode 100644 internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql create mode 100644 internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go create mode 100644 internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go create mode 100644 internal/modules/production/project-flock-kandangs/module.go create mode 100644 internal/modules/production/project-flock-kandangs/route.go create mode 100644 internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go create mode 100644 internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go 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 deleted file mode 100644 index 25d3476d..00000000 --- a/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql +++ /dev/null @@ -1,36 +0,0 @@ -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.up.sql b/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql deleted file mode 100644 index 82b3e9a7..00000000 --- a/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql +++ /dev/null @@ -1,36 +0,0 @@ -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/migrations/20251022024829_create_project_chickin_details.up.sql b/internal/database/migrations/20251022024829_create_project_chickin_details.up.sql index 349086ba..a2364abe 100644 --- a/internal/database/migrations/20251022024829_create_project_chickin_details.up.sql +++ b/internal/database/migrations/20251022024829_create_project_chickin_details.up.sql @@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_chickin_details ( deleted_at TIMESTAMPTZ ); --- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) + DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN diff --git a/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql index c00765b1..e029646b 100644 --- a/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql +++ b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql @@ -3,6 +3,7 @@ -- ============================================ -- STEP 1: Hapus tabel jika sudah ada +DROP TABLE IF EXISTS project_chickins; -- STEP 2: Buat tabel project_chickins CREATE TABLE IF NOT EXISTS project_chickins ( @@ -20,39 +21,38 @@ CREATE TABLE IF NOT EXISTS project_chickins ( ); -- STEP 3: FOREIGN KEYS -DO $$ -BEGIN - -- Relasi ke project_flock_kandangs - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN - ALTER TABLE project_chickins - ADD CONSTRAINT fk_project_chickins_kandang - FOREIGN KEY (project_flock_kandang_id) - REFERENCES project_flock_kandangs(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; +BEGIN; - -- Relasi ke product_warehouses - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN - ALTER TABLE project_chickins - ADD CONSTRAINT fk_project_chickins_warehouse - FOREIGN KEY (product_warehouse_id) - REFERENCES product_warehouses(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; +-- Relasi ke project_flock_kandangs +ALTER TABLE project_chickins +ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs (id) ON DELETE RESTRICT ON UPDATE CASCADE; - -- Relasi ke users - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN - ALTER TABLE project_chickins - ADD CONSTRAINT fk_project_chickins_created_by - FOREIGN KEY (created_by) - REFERENCES users(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; +-- Relasi ke product_warehouses +ALTER TABLE project_chickins +ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Relasi ke users +ALTER TABLE project_chickins +ADD CONSTRAINT fk_project_chickins_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE; + +COMMIT; -- STEP 4: INDEXES -CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id); +CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id) +WHERE + deleted_at IS NULL; -CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id); +CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id) +WHERE + deleted_at IS NULL; -CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by); + +-- Composite index for common queries +CREATE INDEX IF NOT EXISTS idx_chickins_kandang_deleted ON project_chickins ( + project_flock_kandang_id, + deleted_at +); + +-- Index for soft delete queries +CREATE INDEX IF NOT EXISTS idx_chickins_deleted_at ON project_chickins (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql index 2cb76e8f..aa529408 100644 --- a/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql +++ b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql @@ -3,6 +3,7 @@ -- ============================================ -- STEP 1: Hapus tabel jika sudah ada +DROP TABLE IF EXISTS project_flock_populations; -- STEP 2: Buat tabel project_flock_populations CREATE TABLE IF NOT EXISTS project_flock_populations ( @@ -19,39 +20,43 @@ CREATE TABLE IF NOT EXISTS project_flock_populations ( ); -- STEP 3: FOREIGN KEYS -DO $$ -BEGIN - -- Relasi ke project_chickins - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN - ALTER TABLE project_flock_populations - ADD CONSTRAINT fk_project_flock_populations_chickin - FOREIGN KEY (project_chickin_id) - REFERENCES project_chickins(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; +BEGIN; - -- Relasi ke product_warehouses - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN - ALTER TABLE project_flock_populations - ADD CONSTRAINT fk_project_flock_populations_warehouse - FOREIGN KEY (product_warehouse_id) - REFERENCES product_warehouses(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; +-- Relasi ke project_chickins +ALTER TABLE project_flock_populations +ADD CONSTRAINT fk_project_flock_populations_chickin FOREIGN KEY (project_chickin_id) REFERENCES project_chickins (id) ON DELETE RESTRICT ON UPDATE CASCADE; - -- Relasi ke users - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN - ALTER TABLE project_flock_populations - ADD CONSTRAINT fk_project_flock_populations_created_by - FOREIGN KEY (created_by) - REFERENCES users(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; +-- Relasi ke product_warehouses +ALTER TABLE project_flock_populations +ADD CONSTRAINT fk_project_flock_populations_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Relasi ke users +ALTER TABLE project_flock_populations +ADD CONSTRAINT fk_project_flock_populations_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE; + +COMMIT; -- STEP 4: INDEXES -CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id); +CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id) +WHERE + deleted_at IS NULL; -CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id); +CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id) +WHERE + deleted_at IS NULL; -CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by); + +-- Composite index for common queries +CREATE INDEX IF NOT EXISTS idx_populations_chickin_deleted ON project_flock_populations ( + project_chickin_id, + deleted_at +); + +-- Index for soft delete queries +CREATE INDEX IF NOT EXISTS idx_populations_deleted_at ON project_flock_populations (deleted_at); + +-- Unique constraint: one population per chickin +CREATE UNIQUE INDEX IF NOT EXISTS idx_populations_chickin_unique ON project_flock_populations (project_chickin_id) +WHERE + deleted_at IS NULL; \ No newline at end of file diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index ca8e8cf3..3b7378c4 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -573,6 +573,7 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) }{ {"Bahan Baku", "RAW"}, {"Day Old Chick", "DOC"}, + {"Pullet", "PULLET"}, } result := make(map[string]uint, len(seeds)) @@ -787,6 +788,26 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, }, + { + Name: "DOC MAlindo", + Brand: "MAlindo", + Sku: "MAL0001", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 8000, + Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, + Flags: []utils.FlagType{utils.FlagDOC}, + }, + { + Name: "Ayam Pullet", + Brand: "MBU Pullet", + Sku: "PUL0001", + Uom: "Ekor", + Category: "Pullet", + Price: 15000, + Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, + Flags: []utils.FlagType{utils.FlagPullet}, + }, } for _, seed := range seeds { diff --git a/internal/entities/projectflock_kandang.go b/internal/entities/projectflock_kandang.go index f10dbd17..c02eafe1 100644 --- a/internal/entities/projectflock_kandang.go +++ b/internal/entities/projectflock_kandang.go @@ -8,7 +8,8 @@ type ProjectFlockKandang struct { KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"` CreatedAt time.Time `gorm:"autoCreateTime"` - ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` - Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` - Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` + Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` + Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + LatestApproval *Approval `gorm:"-" json:"-"` } diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index 0017fe6c..3bf7f36f 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -18,6 +18,7 @@ type ProductWarehouseRepository interface { GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) + GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) WithTxRepo(tx *gorm.DB) ProductWarehouseRepository DB() *gorm.DB } @@ -89,6 +90,8 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) { var productWarehouses []entity.ProductWarehouse err := r.db.WithContext(ctx). + Preload("Product"). + Preload("Warehouse"). Table("product_warehouses"). Select("product_warehouses.*"). Joins("JOIN products ON products.id = product_warehouses.product_id"). @@ -105,6 +108,8 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) { var productWarehouse entity.ProductWarehouse err := r.db.WithContext(ctx). + Preload("Product"). + Preload("Warehouse"). Table("product_warehouses"). Select("product_warehouses.*"). Joins("JOIN products ON products.id = product_warehouses.product_id"). @@ -118,3 +123,15 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c } return &productWarehouse, nil } + +func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) { + var product entity.Product + err := r.db.WithContext(ctx). + Joins("JOIN product_categories ON products.product_category_id = product_categories.id"). + Where("product_categories.code = ?", categoryCode). + First(&product).Error + if err != nil { + return nil, err + } + return &product, nil +} diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 823cbfa5..1f35c5a8 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -15,8 +15,8 @@ import ( // === DTO Structs (ordered) === type ChickinBaseDTO struct { - Id uint `json:"id"` - ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ChickInDate time.Time `json:"chick_in_date"` ProductWarehouseId uint `json:"product_warehouse_id"` UsageQty float64 `json:"usage_qty"` @@ -55,10 +55,9 @@ type ChickinSimpleDTO struct { 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"` + CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type ChickinDetailDTO struct { @@ -141,14 +140,17 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD } func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { - var pfk *ProjectFlockKandangDTO + var projectFlockKandangId uint + // Check if ProjectFlockKandang relation is loaded if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 { - mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang) - pfk = &mapped + projectFlockKandangId = e.ProjectFlockKandang.Id + } else if e.ProjectFlockKandangId != 0 { + // If relation is not loaded but ID is available, use the ID + projectFlockKandangId = e.ProjectFlockKandangId } return ChickinBaseDTO{ Id: e.Id, - ProjectFlockKandang: pfk, + ProjectFlockKandangId: projectFlockKandangId, ChickInDate: e.ChickInDate, ProductWarehouseId: e.ProductWarehouseId, UsageQty: e.UsageQty, @@ -176,17 +178,11 @@ func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } - var pfk *ProjectFlockKandangDTO - if e.ProjectFlockKandang != nil && 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, + ChickinBaseDTO: ToChickinBaseDTO(e), + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, } } diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index 64e2e4b4..fa6f13e6 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -11,21 +11,24 @@ import ( type ProjectChickinRepository interface { repository.BaseRepository[entity.ProjectChickin] GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) + GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) } type ChickinRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.ProjectChickin] + db *gorm.DB } func NewChickinRepository(db *gorm.DB) ProjectChickinRepository { return &ChickinRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db), + db: db, } } func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) { var chickin entity.ProjectChickin - err := r.DB().WithContext(ctx). + err := r.db.WithContext(ctx). Where("project_floc_id = ?", projectFlockID). Where("deleted_at IS NULL"). First(&chickin).Error @@ -34,3 +37,15 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr } return &chickin, nil } + +func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) { + var chickins []entity.ProjectChickin + err := r.db.WithContext(ctx). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + Order("created_at DESC"). + Find(&chickins).Error + if err != nil { + return nil, err + } + return chickins, nil +} diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 2b44c242..3074b36a 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -125,13 +125,21 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit } var productWarehouses []entity.ProductWarehouse + category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) - if strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) == string(utils.ProjectFlockCategoryGrowing) { + var productCategoryCode string + switch category { + case string(utils.ProjectFlockCategoryGrowing): + productCategoryCode = "DOC" + case string(utils.ProjectFlockCategoryLaying): + productCategoryCode = "PULLET" + default: + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category)) + } - productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) - if err != nil || len(productWarehouses) == 0 { - return nil, fiber.NewError(fiber.StatusNotFound, "Product for growing category in the Kandang's warehouse not found") - } + productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) + if err != nil || len(productWarehouses) == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category))) } chickinDate, err := utils.ParseDateString(req.ChickInDate) @@ -142,20 +150,17 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit actorID := uint(1) // todo nanti ambil dari auth context newChikins := make([]*entity.ProjectChickin, 0) for _, productWarehouse := range productWarehouses { - - if productWarehouse.Quantity > 0 { - newChickin := &entity.ProjectChickin{ - ProjectFlockKandangId: req.ProjectFlockKandangId, - ChickInDate: chickinDate, - UsageQty: 0, - PendingUsageQty: productWarehouse.Quantity, - ProductWarehouseId: productWarehouse.Id, - Notes: req.Note, - CreatedBy: actorID, - } - - newChikins = append(newChikins, newChickin) + newChickin := &entity.ProjectChickin{ + ProjectFlockKandangId: req.ProjectFlockKandangId, + ChickInDate: chickinDate, + UsageQty: 0, + PendingUsageQty: productWarehouse.Quantity, + ProductWarehouseId: productWarehouse.Id, + Notes: req.Note, + CreatedBy: actorID, } + + newChikins = append(newChikins, newChickin) } if len(newChikins) == 0 { @@ -219,7 +224,6 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody["chick_in_date"] = req.ChickInDate } if req.Note != "" { - // entity uses `Notes` => column `notes` updateBody["notes"] = req.Note } if len(updateBody) == 0 { @@ -239,7 +243,6 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { - // Simplified delete: directly call repository delete. Complex restore logic removed for now. if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Chickin not found") @@ -254,7 +257,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if err := s.Validate.Struct(req); err != nil { return nil, err } - actorID := uint(1) // todo nanti ambil dari auth context var action entity.ApprovalAction @@ -272,26 +274,40 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } + // Validate all ProjectFlockKandang IDs exist and have valid approval status + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB())) + for _, id := range approvableIDs { + idCopy := id + if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil { + return nil, err + } + + // Check latest approval status - must be PENGAJUAN to be approvable + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + if latestApproval == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id)) + } + if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id)) + } + } + step := utils.ProjectFlockKandangStepPengajuan if action == entity.ApprovalActionApproved { step = utils.ProjectFlockKandangStepDisetujui } err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) - ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) - chickinRepoTx := s.Repository.WithTx(dbTransaction) + chickinRepoTx := repository.NewChickinRepository(dbTransaction) + productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) for _, approvableID := range approvableIDs { - exists, err := s.ProjectflockKandangRepo.WithTx(dbTransaction).IdExists(c.Context(), approvableID) - if err != nil { - return err - } - if !exists { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) - } - if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlockKandang, @@ -311,31 +327,54 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if action == entity.ApprovalActionApproved { - var chickins []entity.ProjectChickin - if err := chickinRepoTx.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", approvableID).Find(&chickins).Error; err != nil { + chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) + if err != nil { return err } - for _, chickin := range chickins { - population := &entity.ProjectFlockPopulation{ - ProjectChickinId: chickin.Id, - ProductWarehouseId: chickin.ProductWarehouseId, - TotalQty: chickin.PendingUsageQty, - TotalUsedQty: 0, - Notes: chickin.Notes, - CreatedBy: actorID, - } - if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err - } - s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) + kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) } + return err } - } + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) + } + return err + } + pulletPW, err := s.getOrCreatePulletProductWarehouse(c, warehouse.Id, dbTransaction, actorID) + if err != nil { + + continue + } + + if err := s.convertChickinsToPullet(c, chickins, pulletPW, dbTransaction, actorID); err != nil { + return err + } + + } else if action == entity.ApprovalActionRejected { + + chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) + if err != nil { + s.Log.Warnf("failed to get chickins for rejection %d: %v", approvableID, err) + continue + } + + for _, chickin := range chickins { + + updates := map[string]any{"quantity": chickin.PendingUsageQty} + + if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { + s.Log.Warnf("failed to restore product warehouse quantity for rejection: %v", err) + } + } + } } return nil @@ -364,6 +403,93 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return updated, nil } +func (s *chickinService) getOrCreatePulletProductWarehouse(ctx *fiber.Ctx, warehouseId uint, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { + + pulletProducts, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "PULLET", warehouseId) + if err == nil && len(pulletProducts) > 0 { + return &pulletProducts[0], nil + } + + pulletProduct, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), "PULLET") + if err != nil { + return nil, fmt.Errorf("failed to get PULLET product: %w", err) + } + if pulletProduct == nil { + return nil, fmt.Errorf("no PULLET product found in system") + } + + newPulletPW := &entity.ProductWarehouse{ + ProductId: pulletProduct.Id, + WarehouseId: warehouseId, + Quantity: 0, + CreatedBy: actorID, + } + + if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPulletPW, nil); err != nil { + return nil, fmt.Errorf("failed to create PULLET product warehouse: %w", err) + } + + return newPulletPW, nil +} + +func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []entity.ProjectChickin, pulletPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error { + + if pulletPW == nil || pulletPW.Id == 0 { + return fmt.Errorf("invalid PULLET product warehouse") + } + + chickinRepoTx := repository.NewChickinRepository(dbTransaction) + productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) + ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) + + for _, chickin := range chickins { + + quantityToConvert := chickin.PendingUsageQty + + if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{ + "usage_qty": quantityToConvert, + "pending_usage_qty": 0, + }, nil); err != nil { + return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err) + } + + // Update quantity di PULLET product warehouse dengan quantity dari chickin + if err := productWarehouseTx.PatchOne(ctx.Context(), pulletPW.Id, map[string]any{ + "quantity": gorm.Expr("quantity + ?", quantityToConvert), + }, nil); err != nil { + s.Log.Warnf("failed to update PULLET warehouse quantity: %v", err) + } + + populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id) + if err != nil { + s.Log.Warnf("failed to check population existence for chickin %d: %v", chickin.Id, err) + continue + } + + if !populationExists { + population := &entity.ProjectFlockPopulation{ + ProjectChickinId: chickin.Id, + ProductWarehouseId: pulletPW.Id, + TotalQty: quantityToConvert, + TotalUsedQty: 0, + Notes: chickin.Notes, + CreatedBy: actorID, + } + if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) + } + } else { + s.Log.Infof("population already exists for chickin %d", chickin.Id) + } + } + + return nil +} + func uniqueUintSlice(values []uint) []uint { seen := make(map[uint]struct{}, len(values)) result := make([]uint, 0, len(values)) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go new file mode 100644 index 00000000..92743035 --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -0,0 +1,76 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type ProjectFlockKandangController struct { + ProjectFlockKandangService service.ProjectFlockKandangService +} + +func NewProjectFlockKandangController(projectFlockKandangService service.ProjectFlockKandangService) *ProjectFlockKandangController { + return &ProjectFlockKandangController{ + ProjectFlockKandangService: projectFlockKandangService, + } +} + +func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all projectFlockKandangs successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToProjectFlockKandangListDTOs(result), + }) +} + +func (u *ProjectFlockKandangController) 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, availableQtys, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get projectFlockKandang successfully", + Data: dto.ToProjectFlockKandangListDTOWithAvailableQty(*result, availableQtys), + }) +} diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go new file mode 100644 index 00000000..ac0678bd --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -0,0 +1,353 @@ +package dto + +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" + chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto" + projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +type ProjectFlockKandangBaseDTO struct { + Id uint `json:"id"` +} + +type ProjectFlockCustomDTO struct { + Id uint `json:"id"` + Period int `json:"period"` + 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,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type KandangCustomDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` +} + +type ProductBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type WarehouseBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +type ProductWarehouseDTO struct { + Id uint `json:"id"` + Quantity float64 `json:"quantity"` + Product *ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` +} + +type AvailableQtyDTO struct { + ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"` +} + +type ProjectFlockKandangListDTO struct { + ProjectFlockKandangBaseDTO + ProjectFlock *ProjectFlockCustomDTO `json:"project_flock,omitempty"` + Kandang *KandangCustomDTO `json:"kandang,omitempty"` + Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"` + AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` +} + +type ProjectFlockKandangDetailDTO struct { + ProjectFlockKandangListDTO +} + +func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO { + return ProjectFlockKandangBaseDTO{ + Id: e.Id, + } +} + +func toProjectFlockCustomDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockCustomDTO { + if pf == nil { + return nil + } + + return &ProjectFlockCustomDTO{ + Id: pf.Id, + Period: pf.Period, + Flock: pf.Flock, + Area: pf.Area, + Category: pf.Category, + Fcr: pf.Fcr, + Location: pf.Location, + Kandangs: pf.Kandangs, + CreatedUser: pf.CreatedUser, + CreatedAt: pf.CreatedAt, + UpdatedAt: pf.UpdatedAt, + } +} + +func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { + if len(chickins) == 0 { + return nil + } + + availableQtyMap := make(map[uint]AvailableQtyDTO) + for _, ch := range chickins { + if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 { + continue + } + + if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists { + continue + } + + pwDTO := &ProductWarehouseDTO{ + Id: ch.ProductWarehouse.Id, + Quantity: ch.ProductWarehouse.Quantity, + } + + if ch.ProductWarehouse.Product.Id != 0 { + pwDTO.Product = &ProductBaseDTO{ + Id: ch.ProductWarehouse.Product.Id, + Name: ch.ProductWarehouse.Product.Name, + } + } + + if ch.ProductWarehouse.Warehouse.Id != 0 { + pwDTO.Warehouse = &WarehouseBaseDTO{ + Id: ch.ProductWarehouse.Warehouse.Id, + Name: ch.ProductWarehouse.Warehouse.Name, + Type: ch.ProductWarehouse.Warehouse.Type, + } + } + + availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{ + ProductWarehouse: pwDTO, + } + } + + if len(availableQtyMap) == 0 { + return nil + } + + result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) + for _, v := range availableQtyMap { + result = append(result, v) + } + return result +} + +func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { + if len(chickins) == 0 { + return nil + } + + result := make([]chickinDTO.ChickinBaseDTO, len(chickins)) + for i, ch := range chickins { + result[i] = chickinDTO.ToChickinBaseDTO(ch) + } + return result +} + +func defaultProjectFlockKandangLatestApproval(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { + if e.LatestApproval != nil { + result := approvalDTO.ToApprovalDTO(*e.LatestApproval) + return &result + } + return nil +} + +func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKandangListDTO { + var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO + if e.ProjectFlock.Id != 0 { + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + projectFlockSummary = &mapped + } + + var kandangSummary *KandangCustomDTO + if e.Kandang.Id != 0 { + kandangSummary = &KandangCustomDTO{ + Id: e.Kandang.Id, + Name: e.Kandang.Name, + Status: e.Kandang.Status, + } + } + + var createdUser *userDTO.UserBaseDTO + if e.ProjectFlock.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser) + createdUser = &mapped + } else if e.ProjectFlock.CreatedBy != 0 { + mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)} + createdUser = &mapped + } + + return ProjectFlockKandangListDTO{ + ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), + ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), + Kandang: kandangSummary, + Chickins: buildChickins(e.Chickins), + AvailableQtys: buildAvailableQtys(e.Chickins), + CreatedAt: e.CreatedAt, + CreatedUser: createdUser, + Approval: defaultProjectFlockKandangLatestApproval(e), + } +} + +func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFlockKandangListDTO { + result := make([]ProjectFlockKandangListDTO, len(e)) + for i, r := range e { + result[i] = ToProjectFlockKandangListDTO(r) + } + return result +} + +func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDetailDTO { + return ProjectFlockKandangDetailDTO{ + ProjectFlockKandangListDTO: ToProjectFlockKandangListDTO(e), + } +} + +func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO { + var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO + if e.ProjectFlock.Id != 0 { + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + projectFlockSummary = &mapped + } + + var kandangSummary *KandangCustomDTO + if e.Kandang.Id != 0 { + kandangSummary = &KandangCustomDTO{ + Id: e.Kandang.Id, + Name: e.Kandang.Name, + Status: e.Kandang.Status, + } + } + + var createdUser *userDTO.UserBaseDTO + if e.ProjectFlock.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser) + createdUser = &mapped + } else if e.ProjectFlock.CreatedBy != 0 { + mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)} + createdUser = &mapped + } + + // convert available qtys from raw map data + var availableQtys []AvailableQtyDTO + if len(availableQtysRaw) > 0 { + availableQtys = buildAvailableQtysFromRaw(availableQtysRaw) + } + + return ProjectFlockKandangListDTO{ + ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), + ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), + Kandang: kandangSummary, + Chickins: buildChickins(e.Chickins), + AvailableQtys: availableQtys, + CreatedAt: e.CreatedAt, + CreatedUser: createdUser, + Approval: defaultProjectFlockKandangLatestApproval(e), + } +} + +func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { + if len(availableQtysRaw) == 0 { + return nil + } + + result := make([]AvailableQtyDTO, len(availableQtysRaw)) + for i, v := range availableQtysRaw { + pwData, ok := v["product_warehouse"].(map[string]interface{}) + if !ok { + continue + } + + pwDTO := buildProductWarehouseFromMap(pwData) + result[i] = AvailableQtyDTO{ + ProductWarehouse: pwDTO, + } + } + return result +} + +func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehouseDTO { + if pwData == nil { + return nil + } + + dto := &ProductWarehouseDTO{} + + if id, ok := pwData["id"].(float64); ok { + dto.Id = uint(id) + } else if id, ok := pwData["id"].(uint); ok { + dto.Id = id + } + + if qty, ok := pwData["quantity"].(float64); ok { + dto.Quantity = qty + } + + if pData, ok := pwData["product"].(map[string]interface{}); ok { + dto.Product = buildProductFromMap(pData) + } + + if wData, ok := pwData["warehouse"].(map[string]interface{}); ok { + dto.Warehouse = buildWarehouseFromMap(wData) + } + + return dto +} + +func buildProductFromMap(pData map[string]interface{}) *ProductBaseDTO { + if pData == nil { + return nil + } + + product := &ProductBaseDTO{} + if id, ok := pData["id"].(float64); ok { + product.Id = uint(id) + } else if id, ok := pData["id"].(uint); ok { + product.Id = id + } + if name, ok := pData["name"].(string); ok { + product.Name = name + } + return product +} + +func buildWarehouseFromMap(wData map[string]interface{}) *WarehouseBaseDTO { + if wData == nil { + return nil + } + + warehouse := &WarehouseBaseDTO{} + if id, ok := wData["id"].(float64); ok { + warehouse.Id = uint(id) + } else if id, ok := wData["id"].(uint); ok { + warehouse.Id = id + } + if name, ok := wData["name"].(string); ok { + warehouse.Name = name + } + if wType, ok := wData["type"].(string); ok { + warehouse.Type = wType + } + return warehouse +} diff --git a/internal/modules/production/project-flock-kandangs/module.go b/internal/modules/production/project-flock-kandangs/module.go new file mode 100644 index 00000000..2457f691 --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/module.go @@ -0,0 +1,42 @@ +package project_flock_kandangs + +import ( + "fmt" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" + rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + "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" +) + +type ProjectFlockKandangModule struct{} + +func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) + userRepo := rUser.NewUserRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + // register workflow steps for project flock kandang approvals + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err)) + } + + projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + ProjectFlockKandangRoutes(router, userService, projectFlockKandangService) +} diff --git a/internal/modules/production/project-flock-kandangs/route.go b/internal/modules/production/project-flock-kandangs/route.go new file mode 100644 index 00000000..8057e847 --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/route.go @@ -0,0 +1,26 @@ +package project_flock_kandangs + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/controllers" + projectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlockKandang.ProjectFlockKandangService) { + ctrl := controller.NewProjectFlockKandangController(s) + + route := v1.Group("/project-flock-kandangs") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Get("/:id", ctrl.GetOne) + +} diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go new file mode 100644 index 00000000..d3f1f0b7 --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -0,0 +1,147 @@ +package service + +import ( + "errors" + + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/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 ProjectFlockKandangService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) +} + +type projectFlockKandangService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProjectFlockKandangRepository + ApprovalSvc commonSvc.ApprovalService + WarehouseRepo rWarehouse.WarehouseRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository +} + +func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) ProjectFlockKandangService { + return &projectFlockKandangService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + ApprovalSvc: approvalSvc, + WarehouseRepo: warehouseRepo, + ProductWarehouseRepo: productWarehouseRepo, + } +} + +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + projectFlockKandangs, err := s.Repository.GetAll(c.Context()) + if err != nil { + s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) + return nil, 0, err + } + total := int64(len(projectFlockKandangs)) + + return projectFlockKandangs, total, nil +} + +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) { + projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + } + if err != nil { + s.Log.Errorf("Failed get projectFlockKandang by id: %+v", err) + return nil, nil, err + } + + if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil { + latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil) + if err != nil { + s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err) + } + + if latest != nil { + projectFlockKandang.LatestApproval = latest + } + } + + availableQtys, err := s.getAvailableQuantities(c, projectFlockKandang) + if err != nil { + s.Log.Errorf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandang.Kandang.Id, err) + availableQtys = nil + } + + return projectFlockKandang, availableQtys, nil +} + +func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) ([]map[string]interface{}, error) { + if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil { + return nil, nil + } + + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.Kandang.Id) + if err != nil || warehouse == nil { + return nil, nil + } + + var productCategoryCode string + if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) { + productCategoryCode = "DOC" + } else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + productCategoryCode = "PULLET" + } else { + + return nil, nil + } + + products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) + if err != nil || len(products) == 0 { + return nil, nil + } + + var result []map[string]interface{} + for _, pw := range products { + if pw.Quantity > 0 { + // Build product data + productData := map[string]interface{}{ + "id": pw.Product.Id, + "name": pw.Product.Name, + } + + // Build warehouse data + warehouseData := map[string]interface{}{ + "id": pw.Warehouse.Id, + "name": pw.Warehouse.Name, + "type": pw.Warehouse.Type, + } + + // Build product warehouse data with nested product and warehouse + productWarehouseData := map[string]interface{}{ + "id": pw.Id, + "quantity": pw.Quantity, + "product": productData, + "warehouse": warehouseData, + } + + result = append(result, map[string]interface{}{ + "available_qty": pw.Quantity, + "product_warehouse": productWarehouseData, + }) + } + } + + return result, nil +} diff --git a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go new file mode 100644 index 00000000..1e95fd2c --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go @@ -0,0 +1,17 @@ +package validation + +type Create struct { + ProjectFlockId uint `json:"project_flock_id" validate:"required"` + KandangId uint `json:"kandang_id" validate:"required"` +} + +type Update struct { + ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty"` + KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty"` +} + +type Query struct { + 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"` +} 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 index 3328d286..fdb1249f 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -11,6 +11,7 @@ import ( type ProjectFlockPopulationRepository interface { // domain-specific GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) + ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) // subset of base repository methods used by services CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error @@ -51,3 +52,15 @@ func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx co } return &record, nil } + +func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) { + var count int64 + err := r.DB().WithContext(ctx). + Where("project_chickin_id = ?", projectChickinID). + Model(&entity.ProjectFlockPopulation{}). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, 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 7205b72a..dba1489b 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -55,6 +55,9 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse"). Order("project_flock_id ASC, created_at ASC"). Find(&records).Error; err != nil { return nil, err @@ -85,6 +88,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse"). First(record, id).Error; err != nil { return nil, err } @@ -104,6 +110,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse"). First(record).Error; err != nil { return nil, err } diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index 10effb58..d1425b7c 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -11,6 +11,7 @@ import ( projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings" + projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs" // MODULE IMPORTS ) @@ -22,6 +23,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida recordings.RecordingModule{}, chickins.ChickinModule{}, transferLayings.TransferLayingModule{}, + projectFlockKandangs.ProjectFlockKandangModule{}, // MODULE REGISTRY } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 8328f4d7..c7bcc99e 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -18,6 +18,7 @@ const ( FlagIsActive FlagType = "IS_ACTIVE" FlagDOC FlagType = "DOC" + FlagPullet FlagType = "PULLET" FlagPakan FlagType = "PAKAN" FlagPreStarter FlagType = "PRE-STARTER" FlagStarter FlagType = "STARTER" @@ -37,6 +38,7 @@ const ( var flagGroupOptions = map[FlagGroup][]FlagType{ FlagGroupProduct: { FlagDOC, + FlagPullet, FlagPakan, FlagPreStarter, FlagStarter, From 86f37a89c1f11f92b9a2328be203b02635295388 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 3 Nov 2025 09:16:29 +0700 Subject: [PATCH 07/59] Feat[BE]: add multilpple type of chickin growing and laying, make convertion product when chickin approved, add projectflockkandangid on projectflock api --- internal/database/seed/seeder.go | 11 + .../controllers/chickin.controller.go | 189 ++++++----- .../production/chickins/dto/chickin.dto.go | 64 +++- internal/modules/production/chickins/route.go | 6 +- .../chickins/services/chickin.service.go | 196 ++++++----- .../dto/project_flock_kandang.dto.go | 313 +++++++++--------- .../project_flocks/dto/projectflock.dto.go | 46 ++- .../services/projectflock.service.go | 4 +- internal/utils/constant.go | 2 + 9 files changed, 485 insertions(+), 346 deletions(-) diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 3b7378c4..93c75b29 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -574,6 +574,7 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {"Bahan Baku", "RAW"}, {"Day Old Chick", "DOC"}, {"Pullet", "PULLET"}, + {"Layer", "LAYER"}, } result := make(map[string]uint, len(seeds)) @@ -808,6 +809,16 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPullet}, }, + { + Name: "Ayam Layer", + Brand: "MBU Layer", + Sku: "LAY0001", + Uom: "Ekor", + Category: "Layer", + Price: 20000, + Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, + Flags: []utils.FlagType{utils.FlagLayer}, + }, } for _, seed := range seeds { diff --git a/internal/modules/production/chickins/controllers/chickin.controller.go b/internal/modules/production/chickins/controllers/chickin.controller.go index c132b279..7f8e0d5b 100644 --- a/internal/modules/production/chickins/controllers/chickin.controller.go +++ b/internal/modules/production/chickins/controllers/chickin.controller.go @@ -1,7 +1,6 @@ package controller import ( - "math" "strconv" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto" @@ -22,30 +21,84 @@ func NewChickinController(chickinService service.ChickinService) *ChickinControl } } -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)), +// 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, totalResults, err := u.ChickinService.GetAll(c, query) + results, err := u.ChickinService.CreateOne(c, req) if err != nil { return err } - return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{ - Code: fiber.StatusOK, + var ( + data interface{} + message = "Create chickin successfully" + ) + if len(results) == 1 { + data = dto.ToChickinListDTO(results[0]) + } else { + message = "Create chickins successfully" + data = dto.ToChickinListDTOs(results) + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, 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), + Message: message, + Data: data, }) } @@ -67,80 +120,60 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get chickin successfully", - Data: dto.ToChickinListDTO(*result), + Data: dto.ToChickinDetailDTO(*result), }) } -func (u *ChickinController) CreateOne(c *fiber.Ctx) error { - req := new(validation.Create) +// func (u *ChickinController) UpdateOne(c *fiber.Ctx) error { +// req := new(validation.Update) +// param := c.Params("id") - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") - } +// id, err := strconv.Atoi(param) +// if err != nil { +// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") +// } - result, err := u.ChickinService.CreateOne(c, req) - if err != nil { - return err - } +// if err := c.BodyParser(req); err != nil { +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") +// } - return c.Status(fiber.StatusCreated). - JSON(response.Success{ - Code: fiber.StatusCreated, - Status: "success", - Message: "Create chickin successfully", - Data: dto.ToChickinListDTO(*result), - }) -} +// result, err := u.ChickinService.UpdateOne(c, req, uint(id)) +// if err != nil { +// return err +// } -func (u *ChickinController) UpdateOne(c *fiber.Ctx) error { - req := new(validation.Update) - param := c.Params("id") +// return c.Status(fiber.StatusOK). +// JSON(response.Success{ +// Code: fiber.StatusOK, +// Status: "success", +// Message: "Update chickin successfully", +// Data: dto.ToChickinListDTO(*result), +// }) +// } - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") - } +// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error { +// param := c.Params("id") - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") - } +// id, err := strconv.Atoi(param) +// if err != nil { +// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") +// } - result, err := u.ChickinService.UpdateOne(c, req, uint(id)) - if err != nil { - return err - } +// if err := u.ChickinService.DeleteOne(c, uint(id)); 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", - }) -} +// return c.Status(fiber.StatusOK). +// JSON(response.Common{ +// Code: fiber.StatusOK, +// Status: "success", +// Message: "Delete chickin successfully", +// }) +// } func (u *ChickinController) Approval(c *fiber.Ctx) error { req := new(validation.Approve) + if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 1f35c5a8..44373b11 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -15,13 +15,13 @@ import ( // === DTO Structs (ordered) === type ChickinBaseDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ChickInDate time.Time `json:"chick_in_date"` - ProductWarehouseId uint `json:"product_warehouse_id"` - UsageQty float64 `json:"usage_qty"` - PendingUsageQty float64 `json:"pending_usage_qty"` - Notes string `json:"notes"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` } type ProjectFlockDTO struct { @@ -61,7 +61,17 @@ type ChickinListDTO struct { } type ChickinDetailDTO struct { - ChickinListDTO + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` + CreatedBy uint `json:"created_by"` + CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // === Mapper Functions (ordered) === @@ -149,13 +159,13 @@ func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { projectFlockKandangId = e.ProjectFlockKandangId } return ChickinBaseDTO{ - Id: e.Id, + Id: e.Id, ProjectFlockKandangId: projectFlockKandangId, - ChickInDate: e.ChickInDate, - ProductWarehouseId: e.ProductWarehouseId, - UsageQty: e.UsageQty, - PendingUsageQty: e.PendingUsageQty, - Notes: e.Notes, + ChickInDate: e.ChickInDate, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, } } @@ -203,7 +213,31 @@ func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO { } func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO { + var createdUser *userBaseDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + return ChickinDetailDTO{ - ChickinListDTO: ToChickinListDTO(e), + Id: e.Id, + ProjectFlockKandangId: e.ProjectFlockKandangId, + ChickInDate: e.ChickInDate, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, + CreatedBy: e.CreatedBy, + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, } } + +func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO { + result := make([]ChickinDetailDTO, len(e)) + for i, r := range e { + result[i] = ToChickinDetailDTO(r) + } + return result +} diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go index 0bb5e93d..e2b64846 100644 --- a/internal/modules/production/chickins/route.go +++ b/internal/modules/production/chickins/route.go @@ -20,10 +20,10 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) + // route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + // route.Patch("/:id", ctrl.UpdateOne) + // route.Delete("/:id", ctrl.DeleteOne) route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 3074b36a..243bdf52 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -25,7 +25,7 @@ import ( 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) + 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 Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) @@ -109,7 +109,7 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e return chickin, nil } -func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) { +func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -150,6 +150,11 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit actorID := uint(1) // todo nanti ambil dari auth context newChikins := make([]*entity.ProjectChickin, 0) for _, productWarehouse := range productWarehouses { + + if productWarehouse.Quantity <= 0 { + continue + } + newChickin := &entity.ProjectChickin{ ProjectFlockKandangId: req.ProjectFlockKandangId, ChickInDate: chickinDate, @@ -167,6 +172,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create") } + existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins") + } + isFirstTime := len(existingChikins) == 0 + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) @@ -187,16 +198,27 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("failed to update product warehouse quantity for id %d", chickin.ProductWarehouseId) + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity") } } + var approvalAction entity.ApprovalAction + if isFirstTime { + approvalAction = entity.ApprovalActionCreated + } else { + approvalAction = entity.ApprovalActionUpdated + } if latest == nil { - - action := entity.ApprovalActionCreated - if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &action, actorID, nil); err != nil { + if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + } + } else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) { + if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil { lower := strings.ToLower(err.Error()) if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { return err @@ -210,7 +232,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } - return newChikins[0], nil + result := make([]entity.ProjectChickin, 0, len(newChikins)) + for _, chickin := range newChikins { + loaded, err := s.GetOne(c, chickin.Id) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reload chickin %d with relations: %v", chickin.Id, err)) + } + result = append(result, *loaded) + } + if len(result) == 0 { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins") + } + + return result, nil } func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { @@ -257,7 +291,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if err := s.Validate.Struct(req); err != nil { return nil, err } - actorID := uint(1) // todo nanti ambil dari auth context + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB())) var action entity.ApprovalAction switch strings.ToUpper(strings.TrimSpace(req.Action)) { @@ -269,21 +304,19 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") } - approvableIDs := uniqueUintSlice(req.ApprovableIds) + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) if len(approvableIDs) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } - // Validate all ProjectFlockKandang IDs exist and have valid approval status - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB())) for _, id := range approvableIDs { idCopy := id if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil { return nil, err } - // Check latest approval status - must be PENGAJUAN to be approvable latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil) + if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } @@ -308,6 +341,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit for _, approvableID := range approvableIDs { + actorID := uint(1) // todo nanti ambil dari auth context if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlockKandang, @@ -348,21 +382,34 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return err } - pulletPW, err := s.getOrCreatePulletProductWarehouse(c, warehouse.Id, dbTransaction, actorID) - if err != nil { + category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category)) + var conversionCategoryCode string - continue + switch category { + case string(utils.ProjectFlockCategoryGrowing): + conversionCategoryCode = "PULLET" + case string(utils.ProjectFlockCategoryLaying): + conversionCategoryCode = "LAYER" + default: + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category for conversion: %s", category)) } - if err := s.convertChickinsToPullet(c, chickins, pulletPW, dbTransaction, actorID); err != nil { + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, conversionCategoryCode, dbTransaction, actorID) + if err != nil { + return fmt.Errorf("failed to get/create %s product warehouse: %w", conversionCategoryCode, err) + } + if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { return err } } else if action == entity.ApprovalActionRejected { chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) - if err != nil { - s.Log.Warnf("failed to get chickins for rejection %d: %v", approvableID, err) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to get chickins for rejection %d: %w", approvableID, err) + } + + if len(chickins) == 0 { continue } @@ -371,12 +418,21 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit updates := map[string]any{"quantity": chickin.PendingUsageQty} if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { - s.Log.Warnf("failed to restore product warehouse quantity for rejection: %v", err) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId)) + } + return fmt.Errorf("failed to restore product warehouse quantity for chickin %d: %w", chickin.Id, err) + } + + if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to delete rejected chickin %d: %w", chickin.Id, err) + } + s.Log.Infof("chickin %d already deleted during rejection", chickin.Id) } } } } - return nil }) @@ -387,7 +443,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") } - s.Log.Errorf("Failed to record approval for chickins %+v: %+v", approvableIDs, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } @@ -403,39 +458,39 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return updated, nil } -func (s *chickinService) getOrCreatePulletProductWarehouse(ctx *fiber.Ctx, warehouseId uint, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { +func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { - pulletProducts, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "PULLET", warehouseId) - if err == nil && len(pulletProducts) > 0 { - return &pulletProducts[0], nil + products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), categoryCode, warehouseId) + if err == nil && len(products) > 0 { + return &products[0], nil } - pulletProduct, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), "PULLET") + product, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), categoryCode) if err != nil { - return nil, fmt.Errorf("failed to get PULLET product: %w", err) + return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err) } - if pulletProduct == nil { - return nil, fmt.Errorf("no PULLET product found in system") + if product == nil { + return nil, fmt.Errorf("no %s product found in system", categoryCode) } - newPulletPW := &entity.ProductWarehouse{ - ProductId: pulletProduct.Id, + newPW := &entity.ProductWarehouse{ + ProductId: product.Id, WarehouseId: warehouseId, Quantity: 0, CreatedBy: actorID, } - if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPulletPW, nil); err != nil { - return nil, fmt.Errorf("failed to create PULLET product warehouse: %w", err) + if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil { + return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err) } - return newPulletPW, nil + return newPW, nil } -func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []entity.ProjectChickin, pulletPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error { +func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error { - if pulletPW == nil || pulletPW.Id == 0 { - return fmt.Errorf("invalid PULLET product warehouse") + if targetPW == nil || targetPW.Id == 0 { + return fmt.Errorf("invalid target product warehouse") } chickinRepoTx := repository.NewChickinRepository(dbTransaction) @@ -444,6 +499,16 @@ func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []enti for _, chickin := range chickins { + populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id) + if err != nil { + return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err) + } + + if populationExists { + s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id) + continue + } + quantityToConvert := chickin.PendingUsageQty if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{ @@ -453,52 +518,31 @@ func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []enti return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err) } - // Update quantity di PULLET product warehouse dengan quantity dari chickin - if err := productWarehouseTx.PatchOne(ctx.Context(), pulletPW.Id, map[string]any{ + if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{ "quantity": gorm.Expr("quantity + ?", quantityToConvert), }, nil); err != nil { - s.Log.Warnf("failed to update PULLET warehouse quantity: %v", err) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id)) + } + return fmt.Errorf("failed to update target warehouse quantity: %w", err) } - populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id) - if err != nil { - s.Log.Warnf("failed to check population existence for chickin %d: %v", chickin.Id, err) - continue + population := &entity.ProjectFlockPopulation{ + ProjectChickinId: chickin.Id, + ProductWarehouseId: targetPW.Id, + TotalQty: quantityToConvert, + TotalUsedQty: 0, + Notes: chickin.Notes, + CreatedBy: actorID, } - - if !populationExists { - population := &entity.ProjectFlockPopulation{ - ProjectChickinId: chickin.Id, - ProductWarehouseId: pulletPW.Id, - TotalQty: quantityToConvert, - TotalUsedQty: 0, - Notes: chickin.Notes, - CreatedBy: actorID, + if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err } - if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err - } - s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) - } - } else { - s.Log.Infof("population already exists for chickin %d", chickin.Id) + s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) } } return nil } - -func uniqueUintSlice(values []uint) []uint { - seen := make(map[uint]struct{}, len(values)) - result := make([]uint, 0, len(values)) - for _, v := range values { - if _, ok := seen[v]; ok { - continue - } - seen[v] = struct{}{} - result = append(result, v) - } - return result -} diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index ac0678bd..1656eaba 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -8,8 +8,9 @@ import ( 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" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto" projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" @@ -27,7 +28,7 @@ type ProjectFlockCustomDTO struct { Category string `json:"category"` Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` - Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` + Kandangs []KandangCustomDTO `json:"kandangs,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -39,22 +40,11 @@ type KandangCustomDTO struct { Status string `json:"status"` } -type ProductBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type WarehouseBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` -} - type ProductWarehouseDTO struct { - Id uint `json:"id"` - Quantity float64 `json:"quantity"` - Product *ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` + Id uint `json:"id"` + Quantity float64 `json:"quantity"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` } type AvailableQtyDTO struct { @@ -95,74 +85,38 @@ func toProjectFlockCustomDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFl Category: pf.Category, Fcr: pf.Fcr, Location: pf.Location, - Kandangs: pf.Kandangs, + Kandangs: buildKandangCustomDTOs(pf.Kandangs), CreatedUser: pf.CreatedUser, CreatedAt: pf.CreatedAt, UpdatedAt: pf.UpdatedAt, } } -func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { - if len(chickins) == 0 { - return nil +func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO { + var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO + if e.ProjectFlock.Id != 0 { + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + projectFlockSummary = &mapped } - availableQtyMap := make(map[uint]AvailableQtyDTO) - for _, ch := range chickins { - if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 { - continue - } - - if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists { - continue - } - - pwDTO := &ProductWarehouseDTO{ - Id: ch.ProductWarehouse.Id, - Quantity: ch.ProductWarehouse.Quantity, - } - - if ch.ProductWarehouse.Product.Id != 0 { - pwDTO.Product = &ProductBaseDTO{ - Id: ch.ProductWarehouse.Product.Id, - Name: ch.ProductWarehouse.Product.Name, - } - } - - if ch.ProductWarehouse.Warehouse.Id != 0 { - pwDTO.Warehouse = &WarehouseBaseDTO{ - Id: ch.ProductWarehouse.Warehouse.Id, - Name: ch.ProductWarehouse.Warehouse.Name, - Type: ch.ProductWarehouse.Warehouse.Type, - } - } - - availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{ - ProductWarehouse: pwDTO, - } + return ProjectFlockKandangListDTO{ + ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), + ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), + Kandang: buildKandangFromEntity(e.Kandang), + Chickins: buildChickins(e.Chickins), + AvailableQtys: buildAvailableQtysFromRaw(availableQtysRaw), + CreatedAt: e.CreatedAt, + CreatedUser: buildCreatedUser(e.ProjectFlock), + Approval: defaultProjectFlockKandangLatestApproval(e), } - - if len(availableQtyMap) == 0 { - return nil - } - - result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) - for _, v := range availableQtyMap { - result = append(result, v) - } - return result } -func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { - if len(chickins) == 0 { - return nil +func toKandangCustomDTO(k projectFlockDTO.KandangWithProjectFlockIdDTO) KandangCustomDTO { + return KandangCustomDTO{ + Id: k.Id, + Name: k.Name, + Status: k.Status, } - - result := make([]chickinDTO.ChickinBaseDTO, len(chickins)) - for i, ch := range chickins { - result[i] = chickinDTO.ToChickinBaseDTO(ch) - } - return result } func defaultProjectFlockKandangLatestApproval(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { @@ -180,32 +134,14 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand projectFlockSummary = &mapped } - var kandangSummary *KandangCustomDTO - if e.Kandang.Id != 0 { - kandangSummary = &KandangCustomDTO{ - Id: e.Kandang.Id, - Name: e.Kandang.Name, - Status: e.Kandang.Status, - } - } - - var createdUser *userDTO.UserBaseDTO - if e.ProjectFlock.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser) - createdUser = &mapped - } else if e.ProjectFlock.CreatedBy != 0 { - mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)} - createdUser = &mapped - } - return ProjectFlockKandangListDTO{ ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), - Kandang: kandangSummary, + Kandang: buildKandangFromEntity(e.Kandang), Chickins: buildChickins(e.Chickins), AvailableQtys: buildAvailableQtys(e.Chickins), CreatedAt: e.CreatedAt, - CreatedUser: createdUser, + CreatedUser: buildCreatedUser(e.ProjectFlock), Approval: defaultProjectFlockKandangLatestApproval(e), } } @@ -224,69 +160,6 @@ func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKa } } -func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO { - var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO - if e.ProjectFlock.Id != 0 { - mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) - projectFlockSummary = &mapped - } - - var kandangSummary *KandangCustomDTO - if e.Kandang.Id != 0 { - kandangSummary = &KandangCustomDTO{ - Id: e.Kandang.Id, - Name: e.Kandang.Name, - Status: e.Kandang.Status, - } - } - - var createdUser *userDTO.UserBaseDTO - if e.ProjectFlock.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser) - createdUser = &mapped - } else if e.ProjectFlock.CreatedBy != 0 { - mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)} - createdUser = &mapped - } - - // convert available qtys from raw map data - var availableQtys []AvailableQtyDTO - if len(availableQtysRaw) > 0 { - availableQtys = buildAvailableQtysFromRaw(availableQtysRaw) - } - - return ProjectFlockKandangListDTO{ - ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), - ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), - Kandang: kandangSummary, - Chickins: buildChickins(e.Chickins), - AvailableQtys: availableQtys, - CreatedAt: e.CreatedAt, - CreatedUser: createdUser, - Approval: defaultProjectFlockKandangLatestApproval(e), - } -} - -func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { - if len(availableQtysRaw) == 0 { - return nil - } - - result := make([]AvailableQtyDTO, len(availableQtysRaw)) - for i, v := range availableQtysRaw { - pwData, ok := v["product_warehouse"].(map[string]interface{}) - if !ok { - continue - } - - pwDTO := buildProductWarehouseFromMap(pwData) - result[i] = AvailableQtyDTO{ - ProductWarehouse: pwDTO, - } - } - return result -} - func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehouseDTO { if pwData == nil { return nil @@ -315,12 +188,12 @@ func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehou return dto } -func buildProductFromMap(pData map[string]interface{}) *ProductBaseDTO { +func buildProductFromMap(pData map[string]interface{}) *productDTO.ProductBaseDTO { if pData == nil { return nil } - product := &ProductBaseDTO{} + product := &productDTO.ProductBaseDTO{} if id, ok := pData["id"].(float64); ok { product.Id = uint(id) } else if id, ok := pData["id"].(uint); ok { @@ -332,12 +205,12 @@ func buildProductFromMap(pData map[string]interface{}) *ProductBaseDTO { return product } -func buildWarehouseFromMap(wData map[string]interface{}) *WarehouseBaseDTO { +func buildWarehouseFromMap(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO { if wData == nil { return nil } - warehouse := &WarehouseBaseDTO{} + warehouse := &warehouseDTO.WarehouseBaseDTO{} if id, ok := wData["id"].(float64); ok { warehouse.Id = uint(id) } else if id, ok := wData["id"].(uint); ok { @@ -351,3 +224,123 @@ func buildWarehouseFromMap(wData map[string]interface{}) *WarehouseBaseDTO { } return warehouse } + +func buildCreatedUser(pf entity.ProjectFlock) *userDTO.UserBaseDTO { + if pf.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(pf.CreatedUser) + return &mapped + } else if pf.CreatedBy != 0 { + return &userDTO.UserBaseDTO{ + Id: pf.CreatedBy, + IdUser: int64(pf.CreatedBy), + } + } + return nil +} + +func buildKandangCustomDTOs(kandangs []projectFlockDTO.KandangWithProjectFlockIdDTO) []KandangCustomDTO { + if len(kandangs) == 0 { + return nil + } + + result := make([]KandangCustomDTO, len(kandangs)) + for i, k := range kandangs { + result[i] = toKandangCustomDTO(k) + } + return result +} + +func buildKandangFromEntity(kandang entity.Kandang) *KandangCustomDTO { + if kandang.Id == 0 { + return nil + } + + return &KandangCustomDTO{ + Id: kandang.Id, + Name: kandang.Name, + Status: kandang.Status, + } +} + +func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { + if len(chickins) == 0 { + return nil + } + + result := make([]chickinDTO.ChickinBaseDTO, len(chickins)) + for i, ch := range chickins { + result[i] = chickinDTO.ToChickinBaseDTO(ch) + } + return result +} + +func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { + if len(chickins) == 0 { + return nil + } + + availableQtyMap := make(map[uint]AvailableQtyDTO) + for _, ch := range chickins { + if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 { + continue + } + + if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists { + continue + } + + pwDTO := &ProductWarehouseDTO{ + Id: ch.ProductWarehouse.Id, + Quantity: ch.ProductWarehouse.Quantity, + } + + if ch.ProductWarehouse.Product.Id != 0 { + pwDTO.Product = &productDTO.ProductBaseDTO{ + Id: ch.ProductWarehouse.Product.Id, + Name: ch.ProductWarehouse.Product.Name, + } + } + + if ch.ProductWarehouse.Warehouse.Id != 0 { + pwDTO.Warehouse = &warehouseDTO.WarehouseBaseDTO{ + Id: ch.ProductWarehouse.Warehouse.Id, + Name: ch.ProductWarehouse.Warehouse.Name, + Type: ch.ProductWarehouse.Warehouse.Type, + } + } + + availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{ + ProductWarehouse: pwDTO, + } + } + + if len(availableQtyMap) == 0 { + return nil + } + + result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) + for _, v := range availableQtyMap { + result = append(result, v) + } + return result +} + +func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { + if len(availableQtysRaw) == 0 { + return nil + } + + result := make([]AvailableQtyDTO, len(availableQtysRaw)) + for i, v := range availableQtysRaw { + pwData, ok := v["product_warehouse"].(map[string]interface{}) + if !ok { + continue + } + + pwDTO := buildProductWarehouseFromMap(pwData) + result[i] = AvailableQtyDTO{ + ProductWarehouse: pwDTO, + } + } + return result +} diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index dff3bc61..2efd979b 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -20,18 +20,25 @@ type ProjectFlockBaseDTO struct { Period int `json:"period"` } +type KandangWithProjectFlockIdDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` +} + type ProjectFlockListDTO struct { ProjectFlockBaseDTO - 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"` + 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 []KandangWithProjectFlockIdDTO `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 { @@ -50,11 +57,24 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { createdUser = &mapped } - var kandangSummaries []kandangDTO.KandangBaseDTO + var kandangSummaries []KandangWithProjectFlockIdDTO if len(e.Kandangs) > 0 { - kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs)) + kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs)) + + // Create a map of KandangId -> ProjectFlockKandangId from KandangHistory + kandangIdToProjectFlockKandangId := make(map[uint]uint) + for _, kh := range e.KandangHistory { + kandangIdToProjectFlockKandangId[kh.KandangId] = kh.Id + } + for i, kandang := range e.Kandangs { - kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang) + baseDTO := kandangDTO.ToKandangBaseDTO(kandang) + kandangSummaries[i] = KandangWithProjectFlockIdDTO{ + Id: baseDTO.Id, + Name: baseDTO.Name, + Status: baseDTO.Status, + ProjectFlockKandangId: kandangIdToProjectFlockKandangId[kandang.Id], + } } } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 23097585..6f99ccb0 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -85,7 +85,9 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { Preload("Area"). Preload("Fcr"). Preload("Location"). - Preload("Kandangs") + Preload("Kandangs"). + Preload("KandangHistory"). + Preload("KandangHistory.Kandang") } func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index c7bcc99e..ef08eaed 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -19,6 +19,7 @@ const ( FlagDOC FlagType = "DOC" FlagPullet FlagType = "PULLET" + FlagLayer FlagType = "LAYER" FlagPakan FlagType = "PAKAN" FlagPreStarter FlagType = "PRE-STARTER" FlagStarter FlagType = "STARTER" @@ -39,6 +40,7 @@ var flagGroupOptions = map[FlagGroup][]FlagType{ FlagGroupProduct: { FlagDOC, FlagPullet, + FlagLayer, FlagPakan, FlagPreStarter, FlagStarter, From c72db5bd18a3856c42a8733a9c5ec6bc0ff8d40e Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 3 Nov 2025 09:29:00 +0700 Subject: [PATCH 08/59] FIX[BE]: delete redudant kandang response on projectflockkandang getone API --- .../dto/project_flock_kandang.dto.go | 14 -------------- .../services/project_flock_kandang.service.go | 4 +--- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 1656eaba..888964a7 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -28,7 +28,6 @@ type ProjectFlockCustomDTO struct { Category string `json:"category"` Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` - Kandangs []KandangCustomDTO `json:"kandangs,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -85,7 +84,6 @@ func toProjectFlockCustomDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFl Category: pf.Category, Fcr: pf.Fcr, Location: pf.Location, - Kandangs: buildKandangCustomDTOs(pf.Kandangs), CreatedUser: pf.CreatedUser, CreatedAt: pf.CreatedAt, UpdatedAt: pf.UpdatedAt, @@ -238,18 +236,6 @@ func buildCreatedUser(pf entity.ProjectFlock) *userDTO.UserBaseDTO { return nil } -func buildKandangCustomDTOs(kandangs []projectFlockDTO.KandangWithProjectFlockIdDTO) []KandangCustomDTO { - if len(kandangs) == 0 { - return nil - } - - result := make([]KandangCustomDTO, len(kandangs)) - for i, k := range kandangs { - result[i] = toKandangCustomDTO(k) - } - return result -} - func buildKandangFromEntity(kandang entity.Kandang) *KandangCustomDTO { if kandang.Id == 0 { return nil diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index d3f1f0b7..6258e34a 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -115,20 +115,18 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project var result []map[string]interface{} for _, pw := range products { if pw.Quantity > 0 { - // Build product data + productData := map[string]interface{}{ "id": pw.Product.Id, "name": pw.Product.Name, } - // Build warehouse data warehouseData := map[string]interface{}{ "id": pw.Warehouse.Id, "name": pw.Warehouse.Name, "type": pw.Warehouse.Type, } - // Build product warehouse data with nested product and warehouse productWarehouseData := map[string]interface{}{ "id": pw.Id, "quantity": pw.Quantity, From 8220e343029bf557a340589f32284111632199d5 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 4 Nov 2025 08:24:38 +0700 Subject: [PATCH 09/59] FIX[BE]: fix logic on Chickin Laying not convert to layer but still Pullet, and inisiate laying transfer migration and base basic API --- ...825_create_laying_transfers_table.down.sql | 2 +- ...74825_create_laying_transfers_table.up.sql | 9 +- ...te_laying_kandang_transfers_table.down.sql | 1 - ...eate_laying_kandang_transfers_table.up.sql | 40 ---- ...ansfer_sources_and_targets_tables.down.sql | 5 + ...transfer_sources_and_targets_tables.up.sql | 93 +++++++++ internal/database/seed/seeder.go | 32 ++-- internal/entities/laying_transfer.go | 12 +- internal/entities/laying_transfer_source.go | 23 +++ internal/entities/laying_transfer_target.go | 24 +++ .../project_chickin.repository.go | 15 ++ .../chickins/services/chickin.service.go | 177 +++++++++++++----- .../dto/project_flock_kandang.dto.go | 15 +- .../project-flock-kandangs/module.go | 3 +- .../services/project_flock_kandang.service.go | 100 +++++++--- .../project_flock_population_repository.go | 25 +++ .../controllers/transfer_laying.controller.go | 9 +- .../laying_transfer.repository.go | 18 +- .../laying_transfer_source.repository.go | 38 ++++ .../laying_transfer_target.repository.go | 38 ++++ .../services/transfer_laying.service.go | 37 +++- .../validations/transfer_laying.validation.go | 34 ++-- 22 files changed, 587 insertions(+), 163 deletions(-) delete mode 100644 internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql delete mode 100644 internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql create mode 100644 internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.down.sql create mode 100644 internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.up.sql create mode 100644 internal/entities/laying_transfer_source.go create mode 100644 internal/entities/laying_transfer_target.go create mode 100644 internal/modules/production/transfer_layings/repositories/laying_transfer_source.repository.go create mode 100644 internal/modules/production/transfer_layings/repositories/laying_transfer_target.repository.go diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql index 6ad77820..29313fe4 100644 --- a/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql @@ -1 +1 @@ -DROP TABLE IF EXISTS laying_transfers; \ No newline at end of file +DROP TABLE IF EXISTS laying_transfers CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql index 10ced117..a01f6e02 100644 --- a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -1,14 +1,15 @@ CREATE TABLE IF NOT EXISTS laying_transfers ( id BIGSERIAL PRIMARY KEY, + transfer_number VARCHAR(50) UNIQUE NOT NULL, from_project_flock_id BIGINT NOT NULL, to_project_flock_id BIGINT NOT NULL, transfer_date DATE NOT NULL, - total_qty INTEGER, + total_qty NUMERIC(15, 3) NOT NULL, notes TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), deleted_at TIMESTAMPTZ, - created_by BIGINT + created_by BIGINT NOT NULL ); -- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) @@ -37,6 +38,10 @@ BEGIN END $$; -- INDEXES +CREATE UNIQUE INDEX IF NOT EXISTS idx_laying_transfers_transfer_number ON laying_transfers (transfer_number) +WHERE + deleted_at IS NULL; + CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id); CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id); diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql deleted file mode 100644 index caf4f52d..00000000 --- a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS laying_kandang_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql deleted file mode 100644 index 786f8de3..00000000 --- a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql +++ /dev/null @@ -1,40 +0,0 @@ -CREATE TABLE IF NOT EXISTS laying_kandang_transfers ( - id BIGSERIAL PRIMARY KEY, - kandang_id BIGINT, - product_warehouse_id BIGINT, - qty NUMERIC(15,3), - laying_transfer_id BIGINT NOT NULL -); - --- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN - ALTER TABLE laying_kandang_transfers - ADD CONSTRAINT fk_laying_kandang_transfers_kandang - FOREIGN KEY (kandang_id) - REFERENCES kandangs(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; - - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN - ALTER TABLE laying_kandang_transfers - ADD CONSTRAINT fk_laying_kandang_transfers_product_warehouse - FOREIGN KEY (product_warehouse_id) - REFERENCES product_warehouses(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; - - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN - ALTER TABLE laying_kandang_transfers - ADD CONSTRAINT fk_laying_kandang_transfers_laying_transfer - FOREIGN KEY (laying_transfer_id) - REFERENCES laying_transfers(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; - --- INDEXES -CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_kandang_id ON laying_kandang_transfers(kandang_id); -CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_product_warehouse_id ON laying_kandang_transfers(product_warehouse_id); -CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_laying_transfer_id ON laying_kandang_transfers(laying_transfer_id); \ No newline at end of file diff --git a/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.down.sql b/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.down.sql new file mode 100644 index 00000000..2b890114 --- /dev/null +++ b/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.down.sql @@ -0,0 +1,5 @@ +-- Rollback laying_transfer_sources dan laying_transfer_targets tables + +DROP TABLE IF EXISTS laying_transfer_targets CASCADE; + +DROP TABLE IF EXISTS laying_transfer_sources CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.up.sql b/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.up.sql new file mode 100644 index 00000000..023fc7b4 --- /dev/null +++ b/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.up.sql @@ -0,0 +1,93 @@ +-- Create laying_transfer_sources dan laying_transfer_targets tables + +-- 1. Create laying_transfer_sources table (detail sumber - kandang asal growing) +CREATE TABLE laying_transfer_sources ( + id BIGSERIAL PRIMARY KEY, + laying_transfer_id BIGINT NOT NULL, + source_project_flock_kandang_id BIGINT NOT NULL, + product_warehouse_id BIGINT, + qty NUMERIC(15, 3) NOT NULL, + note TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- Add foreign keys untuk laying_transfer_sources +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN + ALTER TABLE laying_transfer_sources + ADD CONSTRAINT fk_laying_transfer_sources_laying_transfer_id + FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE laying_transfer_sources + ADD CONSTRAINT fk_laying_transfer_sources_project_flock_kandang_id + FOREIGN KEY (source_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE laying_transfer_sources + ADD CONSTRAINT fk_laying_transfer_sources_product_warehouse_id + FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL; + END IF; +END $$; + +-- 2. Create laying_transfer_targets table (detail tujuan - kandang laying) +CREATE TABLE laying_transfer_targets ( + id BIGSERIAL PRIMARY KEY, + laying_transfer_id BIGINT NOT NULL, + target_project_flock_kandang_id BIGINT NOT NULL, + qty NUMERIC(15, 3) NOT NULL, + product_warehouse_id BIGINT, + note TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- Add foreign keys untuk laying_transfer_targets +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN + ALTER TABLE laying_transfer_targets + ADD CONSTRAINT fk_laying_transfer_targets_laying_transfer_id + FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE laying_transfer_targets + ADD CONSTRAINT fk_laying_transfer_targets_project_flock_kandang_id + FOREIGN KEY (target_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE laying_transfer_targets + ADD CONSTRAINT fk_laying_transfer_targets_product_warehouse_id + FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL; + END IF; +END $$; + +-- 3. Create indexes untuk laying_transfer_sources +CREATE INDEX idx_laying_transfer_sources_laying_transfer_id ON laying_transfer_sources (laying_transfer_id); + +CREATE INDEX idx_laying_transfer_sources_source_kandang_id ON laying_transfer_sources ( + source_project_flock_kandang_id +); + +CREATE INDEX idx_laying_transfer_sources_product_warehouse_id ON laying_transfer_sources (product_warehouse_id); + +CREATE INDEX idx_laying_transfer_sources_deleted_at ON laying_transfer_sources (deleted_at); + +-- 4. Create indexes untuk laying_transfer_targets +CREATE INDEX idx_laying_transfer_targets_laying_transfer_id ON laying_transfer_targets (laying_transfer_id); + +CREATE INDEX idx_laying_transfer_targets_target_kandang_id ON laying_transfer_targets ( + target_project_flock_kandang_id +); + +CREATE INDEX idx_laying_transfer_targets_product_warehouse_id ON laying_transfer_targets (product_warehouse_id); + +CREATE INDEX idx_laying_transfer_targets_deleted_at ON laying_transfer_targets (deleted_at); \ No newline at end of file diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 93c75b29..3e4f1eb9 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -258,7 +258,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio Flock: "Flock Priangan", Area: "Priangan", Category: utils.ProjectFlockCategoryGrowing, - Fcr: "FCR Layer", + Fcr: "FCR DOC", Location: "Singaparna", Period: 1, }, @@ -267,7 +267,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio Flock: "Flock Banten", Area: "Banten", Category: utils.ProjectFlockCategoryGrowing, - Fcr: "FCR Layer", + Fcr: "FCR DOC", Location: "Cikaum", Period: 1, }, @@ -574,7 +574,6 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {"Bahan Baku", "RAW"}, {"Day Old Chick", "DOC"}, {"Pullet", "PULLET"}, - {"Layer", "LAYER"}, } result := make(map[string]uint, len(seeds)) @@ -698,14 +697,25 @@ func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) { } }{ { - Name: "FCR Layer", + Name: "FCR DOC", Standards: []struct { Weight float64 FcrNumber float64 Mortality float64 }{ - {Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0}, - {Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5}, + {Weight: 0.1, FcrNumber: 1.20, Mortality: 1.0}, + {Weight: 0.3, FcrNumber: 1.35, Mortality: 1.5}, + }, + }, + { + Name: "FCR Pullet", + Standards: []struct { + Weight float64 + FcrNumber float64 + Mortality float64 + }{ + {Weight: 0.5, FcrNumber: 1.45, Mortality: 2.0}, + {Weight: 0.8, FcrNumber: 1.50, Mortality: 2.5}, }, }, } @@ -809,16 +819,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPullet}, }, - { - Name: "Ayam Layer", - Brand: "MBU Layer", - Sku: "LAY0001", - Uom: "Ekor", - Category: "Layer", - Price: 20000, - Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, - Flags: []utils.FlagType{utils.FlagLayer}, - }, } for _, seed := range seeds { diff --git a/internal/entities/laying_transfer.go b/internal/entities/laying_transfer.go index 75c5e23a..8c2058df 100644 --- a/internal/entities/laying_transfer.go +++ b/internal/entities/laying_transfer.go @@ -8,17 +8,21 @@ import ( type LayingTransfer struct { Id uint `gorm:"primaryKey"` + TransferNumber string `gorm:"uniqueIndex;not null"` FromProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"` - TotalQty float64 `gorm:"type:numeric(15,3)"` TransferDate time.Time `gorm:"type:date;not null"` + TotalQty float64 `gorm:"type:numeric(15,3);not null"` Notes string `gorm:"type:text"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index"` - FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` - ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` - CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` + FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` + ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` + Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` + Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` } + diff --git a/internal/entities/laying_transfer_source.go b/internal/entities/laying_transfer_source.go new file mode 100644 index 00000000..6b54bd84 --- /dev/null +++ b/internal/entities/laying_transfer_source.go @@ -0,0 +1,23 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type LayingTransferSource struct { + Id uint `gorm:"primaryKey"` + LayingTransferId uint `gorm:"index;not null"` + SourceProjectFlockKandangId uint `gorm:"not null"` + ProductWarehouseId *uint `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null"` + Note string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` + + LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"` + SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` +} diff --git a/internal/entities/laying_transfer_target.go b/internal/entities/laying_transfer_target.go new file mode 100644 index 00000000..dec98f1f --- /dev/null +++ b/internal/entities/laying_transfer_target.go @@ -0,0 +1,24 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type LayingTransferTarget struct { + Id uint `gorm:"primaryKey"` + LayingTransferId uint `gorm:"index;not null"` + TargetProjectFlockKandangId uint `gorm:"not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + ProductWarehouseId *uint `gorm:""` + Note string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` + + LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"` + TargetProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:TargetProjectFlockKandangId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` +} + diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index fa6f13e6..c0f522ad 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -12,6 +12,7 @@ type ProjectChickinRepository interface { repository.BaseRepository[entity.ProjectChickin] GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) + GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) } type ChickinRepositoryImpl struct { @@ -49,3 +50,17 @@ func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, } return chickins, nil } + +func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) { + var chickins []entity.ProjectChickin + err := r.db.WithContext(ctx). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + Where("usage_qty = 0"). + Where("pending_usage_qty > 0"). + Order("created_at DESC"). + Find(&chickins).Error + if err != nil { + return nil, err + } + return chickins, nil +} diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 243bdf52..1970354a 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -150,8 +150,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti actorID := uint(1) // todo nanti ambil dari auth context newChikins := make([]*entity.ProjectChickin, 0) for _, productWarehouse := range productWarehouses { + availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category) + if err != nil { + s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", productWarehouse.Id, err) + } - if productWarehouse.Quantity <= 0 { + if availableQty <= 0 { continue } @@ -159,7 +163,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti ProjectFlockKandangId: req.ProjectFlockKandangId, ChickInDate: chickinDate, UsageQty: 0, - PendingUsageQty: productWarehouse.Quantity, + PendingUsageQty: availableQty, ProductWarehouseId: productWarehouse.Id, Notes: req.Note, CreatedBy: actorID, @@ -176,6 +180,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins") } + isFirstTime := len(existingChikins) == 0 err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { @@ -192,17 +197,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return err } - for _, chickin := range newChikins { + if category == string(utils.ProjectFlockCategoryLaying) { + for _, chickin := range newChikins { + updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)} - updates := map[string]any{"quantity": 0} - - if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId)) + if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity") } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity") } } + var approvalAction entity.ApprovalAction if isFirstTime { approvalAction = entity.ApprovalActionCreated @@ -287,6 +294,56 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { return nil } +func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) { + availableQty := productWarehouse.Quantity + + if category == string(utils.ProjectFlockCategoryGrowing) { + var totalPendingQty float64 + + chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) + if err == nil { + for _, chickin := range chickins { + + if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { + totalPendingQty += chickin.PendingUsageQty + } + } + } + + availableQty = productWarehouse.Quantity - totalPendingQty + if availableQty < 0 { + availableQty = 0 + } + } else if category == string(utils.ProjectFlockCategoryLaying) { + var totalPopulation float64 + var totalPendingQty float64 + + populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id) + if err == nil { + for _, pop := range populations { + totalPopulation += pop.TotalQty + } + } + + chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) + if err == nil { + for _, chickin := range chickins { + + if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { + totalPendingQty += chickin.PendingUsageQty + } + } + } + + availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty + if availableQty < 0 { + availableQty = 0 + } + } + + return availableQty, nil +} + func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err @@ -356,7 +413,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { return err } - s.Log.Infof("ignored duplicate approval for kandang %d: %v", approvableID, err) + } if action == entity.ApprovalActionApproved { @@ -374,54 +431,76 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return err } - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) - } - return err - } - category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category)) - var conversionCategoryCode string - switch category { - case string(utils.ProjectFlockCategoryGrowing): - conversionCategoryCode = "PULLET" - case string(utils.ProjectFlockCategoryLaying): - conversionCategoryCode = "LAYER" - default: - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category for conversion: %s", category)) + if category == string(utils.ProjectFlockCategoryGrowing) { + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) + } + return err + } + + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + if err != nil { + return fmt.Errorf("failed to get/create PULLET product warehouse: %w", err) + } + if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { + return err + } + } else if category == string(utils.ProjectFlockCategoryLaying) { + + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) + } + return err + } + + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + if err != nil { + return fmt.Errorf("failed to get/create PULLET product warehouse: %w", err) + } + if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { + return err + } } + } - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, conversionCategoryCode, dbTransaction, actorID) + if action == entity.ApprovalActionRejected { + + chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID) if err != nil { - return fmt.Errorf("failed to get/create %s product warehouse: %w", conversionCategoryCode, err) - } - if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { - return err - } - - } else if action == entity.ApprovalActionRejected { - - chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("failed to get chickins for rejection %d: %w", approvableID, err) + return fmt.Errorf("failed to get pending chickins for rejection %d: %w", approvableID, err) } if len(chickins) == 0 { continue } + kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) + } + return err + } + + categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category)) + for _, chickin := range chickins { - updates := map[string]any{"quantity": chickin.PendingUsageQty} + if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) { + updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)} - if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId)) + if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId)) + } + return fmt.Errorf("failed to restore product warehouse quantity for chickin %d: %w", chickin.Id, err) } - return fmt.Errorf("failed to restore product warehouse quantity for chickin %d: %w", chickin.Id, err) } if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil { @@ -518,6 +597,18 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err) } + if chickin.ProductWarehouseId != targetPW.Id { + if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{ + "quantity": gorm.Expr("quantity - ?", quantityToConvert), + }, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId)) + } + return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err) + } + } + + // Add to target product warehouse if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{ "quantity": gorm.Expr("quantity + ?", quantityToConvert), }, nil); err != nil { diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 888964a7..b7dcdbef 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -41,12 +41,12 @@ type KandangCustomDTO struct { type ProductWarehouseDTO struct { Id uint `json:"id"` - Quantity float64 `json:"quantity"` Product *productDTO.ProductBaseDTO `json:"product,omitempty"` Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` } type AvailableQtyDTO struct { + AvailableQty float64 `json:"available_qty"` ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"` } @@ -171,10 +171,6 @@ func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehou dto.Id = id } - if qty, ok := pwData["quantity"].(float64); ok { - dto.Quantity = qty - } - if pData, ok := pwData["product"].(map[string]interface{}); ok { dto.Product = buildProductFromMap(pData) } @@ -276,8 +272,7 @@ func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { } pwDTO := &ProductWarehouseDTO{ - Id: ch.ProductWarehouse.Id, - Quantity: ch.ProductWarehouse.Quantity, + Id: ch.ProductWarehouse.Id, } if ch.ProductWarehouse.Product.Id != 0 { @@ -324,7 +319,13 @@ func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []Avai } pwDTO := buildProductWarehouseFromMap(pwData) + availableQty := 0.0 + if qty, ok := v["available_qty"].(float64); ok { + availableQty = qty + } + result[i] = AvailableQtyDTO{ + AvailableQty: availableQty, ProductWarehouse: pwDTO, } } diff --git a/internal/modules/production/project-flock-kandangs/module.go b/internal/modules/production/project-flock-kandangs/module.go index 2457f691..160cec5e 100644 --- a/internal/modules/production/project-flock-kandangs/module.go +++ b/internal/modules/production/project-flock-kandangs/module.go @@ -24,6 +24,7 @@ type ProjectFlockKandangModule struct{} func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) + projectFlockPopulationRepo := rProjectFlockKandang.NewProjectFlockPopulationRepository(db) userRepo := rUser.NewUserRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) @@ -35,7 +36,7 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err)) } - projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, validate) + projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate) userService := sUser.NewUserService(userRepo, validate) ProjectFlockKandangRoutes(router, userService, projectFlockKandangService) diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 6258e34a..ec292d90 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -29,9 +29,10 @@ type projectFlockKandangService struct { ApprovalSvc commonSvc.ApprovalService WarehouseRepo rWarehouse.WarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + PopulationRepo repository.ProjectFlockPopulationRepository } -func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) ProjectFlockKandangService { +func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService { return &projectFlockKandangService{ Log: utils.Log, Validate: validate, @@ -39,6 +40,7 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository ApprovalSvc: approvalSvc, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, + PopulationRepo: populationRepo, } } @@ -114,32 +116,80 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project var result []map[string]interface{} for _, pw := range products { - if pw.Quantity > 0 { - - productData := map[string]interface{}{ - "id": pw.Product.Id, - "name": pw.Product.Name, - } - - warehouseData := map[string]interface{}{ - "id": pw.Warehouse.Id, - "name": pw.Warehouse.Name, - "type": pw.Warehouse.Type, - } - - productWarehouseData := map[string]interface{}{ - "id": pw.Id, - "quantity": pw.Quantity, - "product": productData, - "warehouse": warehouseData, - } - - result = append(result, map[string]interface{}{ - "available_qty": pw.Quantity, - "product_warehouse": productWarehouseData, - }) + availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) + if err != nil { + s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) } + + // Only include product warehouse if available_qty > 0 + if availableQty <= 0 { + continue + } + + productData := map[string]interface{}{ + "id": pw.Product.Id, + "name": pw.Product.Name, + } + + warehouseData := map[string]interface{}{ + "id": pw.Warehouse.Id, + "name": pw.Warehouse.Name, + "type": pw.Warehouse.Type, + } + + productWarehouseData := map[string]interface{}{ + "id": pw.Id, + "product": productData, + "warehouse": warehouseData, + } + + result = append(result, map[string]interface{}{ + "available_qty": availableQty, + "product_warehouse": productWarehouseData, + }) } return result, nil } + +func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) { + availableQty := productWarehouse.Quantity + + if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) { + var totalPendingQty float64 + + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { + totalPendingQty += chickin.PendingUsageQty + } + } + + availableQty = productWarehouse.Quantity - totalPendingQty + if availableQty < 0 { + availableQty = 0 + } + } else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + var totalPopulation float64 + var totalPendingQty float64 + + populations, err := s.PopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(c.Context(), projectFlockKandang.Id, productWarehouse.Id) + if err == nil { + for _, pop := range populations { + totalPopulation += pop.TotalQty + } + } + + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { + totalPendingQty += chickin.PendingUsageQty + } + } + + availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty + if availableQty < 0 { + availableQty = 0 + } + } + + return availableQty, nil +} 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 index fdb1249f..2f622656 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -12,6 +12,8 @@ type ProjectFlockPopulationRepository interface { // domain-specific GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) + GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) + GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) // subset of base repository methods used by services CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error @@ -64,3 +66,26 @@ func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx cont } return count > 0, nil } + +func (r *projectFlockPopulationRepositoryImpl) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) { + var records []entity.ProjectFlockPopulation + err := r.DB().WithContext(ctx). + Where("project_chickin_id = ? AND product_warehouse_id = ?", projectChickinID, productWarehouseID). + Find(&records).Error + if err != nil { + return nil, err + } + return records, nil +} + +func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) { + var records []entity.ProjectFlockPopulation + err := r.DB().WithContext(ctx). + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ? AND project_flock_populations.product_warehouse_id = ?", projectFlockKandangID, productWarehouseID). + Find(&records).Error + if err != nil { + return nil, err + } + return records, nil +} diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index 01f49867..f962c263 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -24,9 +24,12 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)), + TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)), + TransferDateFrom: c.Query("transfer_date_from", ""), + TransferDateTo: c.Query("transfer_date_to", ""), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index e92e8f95..2e5437da 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -1,6 +1,8 @@ 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" @@ -8,15 +10,29 @@ import ( type TransferLayingRepository interface { repository.BaseRepository[entity.LayingTransfer] - + GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) } type TransferLayingRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.LayingTransfer] + db *gorm.DB } func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { return &TransferLayingRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransfer](db), + db: db, } } + +func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) { + var transfer entity.LayingTransfer + err := r.db.WithContext(ctx). + Where("transfer_number = ?", transferNumber). + Where("deleted_at IS NULL"). + First(&transfer).Error + if err != nil { + return nil, err + } + return &transfer, nil +} diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer_source.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer_source.repository.go new file mode 100644 index 00000000..6c52683e --- /dev/null +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer_source.repository.go @@ -0,0 +1,38 @@ +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 LayingTransferSourceRepository interface { + repository.BaseRepository[entity.LayingTransferSource] + GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error) +} + +type LayingTransferSourceRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.LayingTransferSource] + db *gorm.DB +} + +func NewLayingTransferSourceRepository(db *gorm.DB) LayingTransferSourceRepository { + return &LayingTransferSourceRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferSource](db), + db: db, + } +} + +func (r *LayingTransferSourceRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error) { + var sources []entity.LayingTransferSource + err := r.db.WithContext(ctx). + Where("laying_transfer_id = ?", layingTransferId). + Order("created_at DESC"). + Find(&sources).Error + if err != nil { + return nil, err + } + return sources, nil +} diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer_target.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer_target.repository.go new file mode 100644 index 00000000..486008cc --- /dev/null +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer_target.repository.go @@ -0,0 +1,38 @@ +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 LayingTransferTargetRepository interface { + repository.BaseRepository[entity.LayingTransferTarget] + GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error) +} + +type LayingTransferTargetRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.LayingTransferTarget] + db *gorm.DB +} + +func NewLayingTransferTargetRepository(db *gorm.DB) LayingTransferTargetRepository { + return &LayingTransferTargetRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferTarget](db), + db: db, + } +} + +func (r *LayingTransferTargetRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error) { + var targets []entity.LayingTransferTarget + err := r.db.WithContext(ctx). + Where("laying_transfer_id = ?", layingTransferId). + Order("created_at DESC"). + Find(&targets).Error + if err != nil { + return nil, err + } + return targets, nil +} diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index bc66bd13..0a5b5dd2 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -55,8 +55,17 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - if params.Search != "" { - return db.Where("name LIKE ?", "%"+params.Search+"%") + if params.SourceProjectFlockId != 0 { + db = db.Where("from_project_flock_id = ?", params.SourceProjectFlockId) + } + if params.TargetProjectFlockId != 0 { + db = db.Where("to_project_flock_id = ?", params.TargetProjectFlockId) + } + if params.TransferDateFrom != "" { + db = db.Where("transfer_date >= ?", params.TransferDateFrom) + } + if params.TransferDateTo != "" { + db = db.Where("transfer_date <= ?", params.TransferDateTo) } return db.Order("created_at DESC").Order("updated_at DESC") }) @@ -92,9 +101,19 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - for _, detail := range req.Details { + // Validate source kandangs + for _, detail := range req.SourceKandangs { if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Project Flock Kandang", ID: &detail.SourceProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + common.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + ); err != nil { + return nil, err + } + } + + // Validate target kandangs + for _, detail := range req.TargetKandangs { + if err := common.EnsureRelations(c.Context(), + common.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, ); err != nil { return nil, err } @@ -106,7 +125,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) } var totalQty float64 - for _, item := range req.Details { + for _, item := range req.SourceKandangs { totalQty += item.Quantity } @@ -142,8 +161,12 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i updateBody := make(map[string]any) - if req.Name != nil { - updateBody["name"] = *req.Name + if req.TransferDate != nil { + updateBody["transfer_date"] = *req.TransferDate + } + + if req.Reason != nil { + updateBody["notes"] = *req.Reason } if len(updateBody) == 0 { diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 3b83132f..1d291ac5 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -1,24 +1,34 @@ package validation -type CreateDetail struct { - SourceProjectFlockKandangId uint `json:"source_project_flock_kandang_id" validate:"required"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` +type SourceKandangDetail struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` +} + +type TargetKandangDetail struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` } type Create struct { - TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"` - SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"` - TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"` - Details []CreateDetail `json:"details" validate:"required,min=1,dive,required"` - Reason string `json:"reason" validate:"omitempty,max=1000"` + TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"` + SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"` + TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"` + SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,dive,required"` + TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"` + Reason string `json:"reason" validate:"omitempty,max=1000"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + TransferDate *string `json:"transfer_date,omitempty" validate:"omitempty,datetime=2006-01-02"` + Reason *string `json:"reason,omitempty" validate:"omitempty,max=1000"` } type Query struct { - 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"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + SourceProjectFlockId uint `query:"source_project_flock_id" validate:"omitempty"` + TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"` + TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"` + TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"` } From f97d404121003f8e3cd58c4b996106b655ef5ba9 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 4 Nov 2025 12:05:04 +0700 Subject: [PATCH 10/59] fix(BE-78)change typedata in recording dto and validation for create and update --- .../recordings/dto/recording.dto.go | 162 +++++++++++------- .../validations/recording.validation.go | 16 +- internal/utils/recording/util.recording.go | 13 +- 3 files changed, 110 insertions(+), 81 deletions(-) diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index e8d04758..99afc013 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -2,10 +2,12 @@ package dto import ( "math" + "strings" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/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" @@ -17,19 +19,19 @@ type RecordingBaseDTO struct { Id uint `json:"id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"` RecordDatetime time.Time `json:"record_datetime"` - Day *int `json:"day,omitempty"` - ProjectFlockCategory *string `json:"project_flock_category,omitempty"` - TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"` - CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"` - DailyGain *float64 `json:"daily_gain,omitempty"` - AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"` - CumIntake *int `json:"cum_intake,omitempty"` - FcrValue *float64 `json:"fcr_value,omitempty"` - TotalChickQty *float64 `json:"total_chick_qty,omitempty"` + Day int `json:"day"` + ProjectFlockCategory string `json:"project_flock_category"` + TotalDepletionQty float64 `json:"total_depletion_qty"` + CumDepletionRate float64 `json:"cum_depletion_rate"` + DailyGain float64 `json:"daily_gain"` + AvgDailyGain float64 `json:"avg_daily_gain"` + CumIntake int `json:"cum_intake"` + FcrValue float64 `json:"fcr_value"` + TotalChickQty float64 `json:"total_chick_qty"` Approval approvalDTO.ApprovalBaseDTO `json:"approval"` - EggGradingStatus *string `json:"egg_grading_status,omitempty"` - EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"` - EggGradingCompletedQty *int `json:"egg_grading_completed_qty,omitempty"` + EggGradingStatus *string `json:"egg_grading_status"` + EggGradingPendingQty *int `json:"egg_grading_pending_qty"` + EggGradingCompletedQty *int `json:"egg_grading_completed_qty"` } type RecordingListDTO struct { @@ -54,31 +56,23 @@ type RecordingBodyWeightDTO struct { } type RecordingDepletionDTO struct { - ProductWarehouseId uint `json:"product_warehouse_id"` - Qty float64 `json:"qty"` - ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Qty float64 `json:"qty"` + ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"` } type RecordingStockDTO struct { - ProductWarehouseId uint `json:"product_warehouse_id"` - UsageAmount *float64 `json:"usage_amount,omitempty"` - PendingQty *float64 `json:"pending_qty,omitempty"` - ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageAmount float64 `json:"usage_amount"` + PendingQty *float64 `json:"pending_qty,omitempty"` + ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"` } type RecordingEggDTO struct { - ProductWarehouseId uint `json:"product_warehouse_id"` - Qty int `json:"qty"` - ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"` - Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"` -} - -type RecordingProductWarehouseDTO struct { - Id uint `json:"id"` - ProductId uint `json:"product_id"` - ProductName string `json:"product_name"` - WarehouseId uint `json:"warehouse_id"` - WarehouseName string `json:"warehouse_name"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Qty int `json:"qty"` + ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"` + Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"` } type RecordingEggGradingDTO struct { @@ -89,12 +83,46 @@ type RecordingEggGradingDTO struct { // === Mapper Functions === func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { - var projectFlockCategory *string + var ( + projectFlockCategory string + day int + totalDepletionQty float64 + cumDepletionRate float64 + dailyGain float64 + avgDailyGain float64 + cumIntake int + fcrValue float64 + totalChickQty float64 + ) + + if e.Day != nil { + day = *e.Day + } + if e.TotalDepletionQty != nil { + totalDepletionQty = *e.TotalDepletionQty + } + if e.CumDepletionRate != nil { + cumDepletionRate = *e.CumDepletionRate + } + if e.DailyGain != nil { + dailyGain = *e.DailyGain + } + if e.AvgDailyGain != nil { + avgDailyGain = *e.AvgDailyGain + } + if e.CumIntake != nil { + cumIntake = *e.CumIntake + } + if e.FcrValue != nil { + fcrValue = *e.FcrValue + } + if e.TotalChickQty != nil { + totalChickQty = *e.TotalChickQty + } + if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 { category := e.ProjectFlockKandang.ProjectFlock.Category - if category != "" { - projectFlockCategory = &category - } + projectFlockCategory = category } latestApproval := defaultRecordingLatestApproval(e) @@ -109,15 +137,15 @@ func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { Id: e.Id, ProjectFlockKandangId: e.ProjectFlockKandangId, RecordDatetime: e.RecordDatetime, - Day: e.Day, + Day: day, ProjectFlockCategory: projectFlockCategory, - TotalDepletionQty: e.TotalDepletionQty, - CumDepletionRate: e.CumDepletionRate, - DailyGain: e.DailyGain, - AvgDailyGain: e.AvgDailyGain, - CumIntake: e.CumIntake, - FcrValue: e.FcrValue, - TotalChickQty: e.TotalChickQty, + TotalDepletionQty: totalDepletionQty, + CumDepletionRate: cumDepletionRate, + DailyGain: dailyGain, + AvgDailyGain: avgDailyGain, + CumIntake: cumIntake, + FcrValue: fcrValue, + TotalChickQty: totalChickQty, Approval: latestApproval, EggGradingStatus: gradingStatus, EggGradingPendingQty: gradingPending, @@ -149,12 +177,21 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO { } func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO { + listDTO := ToRecordingListDTO(e) + + var eggs []RecordingEggDTO + if strings.EqualFold(listDTO.ProjectFlockCategory, string(utils.ProjectFlockCategoryLaying)) { + eggs = ToRecordingEggDTOs(e.Eggs) + } else if len(e.Eggs) > 0 { + eggs = ToRecordingEggDTOs(e.Eggs) + } + return RecordingDetailDTO{ - RecordingListDTO: ToRecordingListDTO(e), + RecordingListDTO: listDTO, BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights), Depletions: ToRecordingDepletionDTOs(e.Depletions), Stocks: ToRecordingStockDTOs(e.Stocks), - Eggs: ToRecordingEggDTOs(e.Eggs), + Eggs: eggs, } } @@ -176,7 +213,7 @@ func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []Recordin result[i] = RecordingDepletionDTO{ ProductWarehouseId: d.ProductWarehouseId, Qty: d.Qty, - ProductWarehouse: toRecordingProductWarehouseDTO(&d.ProductWarehouse), + ProductWarehouse: mapProductWarehouseDTO(&d.ProductWarehouse), } } return result @@ -185,11 +222,16 @@ func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []Recordin func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO { result := make([]RecordingStockDTO, len(stocks)) for i, s := range stocks { + var usageAmount float64 + if s.UsageQty != nil { + usageAmount = *s.UsageQty + } + result[i] = RecordingStockDTO{ ProductWarehouseId: s.ProductWarehouseId, - UsageAmount: s.UsageQty, + UsageAmount: usageAmount, PendingQty: s.PendingQty, - ProductWarehouse: toRecordingProductWarehouseDTO(&s.ProductWarehouse), + ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse), } } return result @@ -201,7 +243,7 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO { result[i] = RecordingEggDTO{ ProductWarehouseId: egg.ProductWarehouseId, Qty: egg.Qty, - ProductWarehouse: toRecordingProductWarehouseDTO(&egg.ProductWarehouse), + ProductWarehouse: mapProductWarehouseDTO(&egg.ProductWarehouse), Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs), } } @@ -224,25 +266,17 @@ func ToRecordingEggGradingDTOs(gradings []entity.GradingEgg) []RecordingEggGradi return result } -func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProductWarehouseDTO { - if pw == nil || pw.Id == 0 { - return nil +func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO { + if pw == nil { + return productWarehouseDTO.ProductWarehouseDTO{} } - dto := RecordingProductWarehouseDTO{ - Id: pw.Id, - ProductId: pw.ProductId, - WarehouseId: pw.WarehouseId, + mapped := productWarehouseDTO.ToProductWarehouseDTO(pw) + if mapped == nil { + return productWarehouseDTO.ProductWarehouseDTO{} } - if pw.Product.Id != 0 { - dto.ProductName = pw.Product.Name - } - if pw.Warehouse.Id != 0 { - dto.WarehouseName = pw.Warehouse.Name - } - - return &dto + return *mapped } const goodEggProductWarehouseID uint = 5 diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go index f058248c..62e5f8df 100644 --- a/internal/modules/production/recordings/validations/recording.validation.go +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -2,14 +2,14 @@ package validation type ( BodyWeight struct { - AvgWeight float64 `json:"avg_weight" validate:"required"` - Qty float64 `json:"qty" validate:"required,gt=0"` - TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"required"` + Qty float64 `json:"qty" validate:"required,gt=0"` + TotalWeight float64 `json:"total_weight" validate:"required,gte=0"` } Stock struct { ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` - Qty *float64 `json:"qty,omitempty" validate:"required_without=UsageAmount,gte=0"` + Qty float64 `json:"qty" validate:"required,gte=0"` PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"` } @@ -26,10 +26,10 @@ type ( type Create struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` - BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"` - Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` - Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` - Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"` + BodyWeights []BodyWeight `json:"body_weights" validate:"dive"` + Stocks []Stock `json:"stocks" validate:"dive"` + Depletions []Depletion `json:"depletions" validate:"dive"` + Eggs []Egg `json:"eggs" validate:"omitempty,dive"` } type Update struct { diff --git a/internal/utils/recording/util.recording.go b/internal/utils/recording/util.recording.go index fd463cf9..e9ae371c 100644 --- a/internal/utils/recording/util.recording.go +++ b/internal/utils/recording/util.recording.go @@ -13,16 +13,15 @@ func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.Re result := make([]entity.RecordingBW, 0, len(items)) for _, item := range items { totalWeight := item.TotalWeight - if totalWeight == nil { - calculated := item.AvgWeight * item.Qty - totalWeight = &calculated + if totalWeight <= 0 { + totalWeight = item.AvgWeight * item.Qty } result = append(result, entity.RecordingBW{ RecordingId: recordingID, AvgWeight: item.AvgWeight, Qty: item.Qty, - TotalWeight: *totalWeight, + TotalWeight: totalWeight, }) } return result @@ -35,12 +34,8 @@ func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingSto result := make([]entity.RecordingStock, 0, len(items)) for _, item := range items { - var usageAmount float64 - if item.Qty != nil { - usageAmount = *item.Qty - } usagePtr := new(float64) - *usagePtr = usageAmount + *usagePtr = item.Qty pending := item.PendingQty if pending == nil { pending = new(float64) From 48730e1b74231caccb6ceba1adfb89075598e033 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 4 Nov 2025 16:34:36 +0700 Subject: [PATCH 11/59] FIX[BE]: fix error handling on chickin service to better handler --- .../chickins/services/chickin.service.go | 90 +++++++++---------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 1970354a..c43a4817 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -152,7 +152,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti for _, productWarehouse := range productWarehouses { availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category) if err != nil { - s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", productWarehouse.Id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", productWarehouse.Id)) } if availableQty <= 0 { @@ -189,12 +189,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins") } latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil) if err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval") } if category == string(utils.ProjectFlockCategoryLaying) { @@ -218,17 +218,29 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti } if latest == nil { - if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlockKandang, + projectFlockKandang.Id, + utils.ProjectFlockKandangStepPengajuan, + &approvalAction, + actorID, + nil); err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } } } else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) { - if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlockKandang, + projectFlockKandang.Id, + utils.ProjectFlockKandangStepPengajuan, + &approvalAction, + actorID, + nil); err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } } } @@ -236,7 +248,10 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil }) if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins") } result := make([]entity.ProjectChickin, 0, len(newChikins)) @@ -324,7 +339,6 @@ func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockK totalPopulation += pop.TotalQty } } - chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) if err == nil { for _, chickin := range chickins { @@ -334,7 +348,6 @@ func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockK } } } - availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty if availableQty < 0 { availableQty = 0 @@ -397,7 +410,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) for _, approvableID := range approvableIDs { - actorID := uint(1) // todo nanti ambil dari auth context if _, err := approvalSvc.CreateApproval( c.Context(), @@ -408,19 +420,13 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit actorID, req.Notes, ); err != nil { - - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err - } - + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } if action == entity.ApprovalActionApproved { - chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) if err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID)) } kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID) @@ -428,7 +434,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang") } category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category)) @@ -439,41 +445,38 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) if err != nil { - return fmt.Errorf("failed to get/create PULLET product warehouse: %w", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target") } } else if category == string(utils.ProjectFlockCategoryLaying) { - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) if err != nil { - return fmt.Errorf("failed to get/create PULLET product warehouse: %w", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target") } } } - if action == entity.ApprovalActionRejected { - chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID) if err != nil { - return fmt.Errorf("failed to get pending chickins for rejection %d: %w", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID)) } if len(chickins) == 0 { @@ -485,7 +488,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang") } categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category)) @@ -499,15 +502,14 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId)) } - return fmt.Errorf("failed to restore product warehouse quantity for chickin %d: %w", chickin.Id, err) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id)) } } if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("failed to delete rejected chickin %d: %w", chickin.Id, err) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id)) } - s.Log.Infof("chickin %d already deleted during rejection", chickin.Id) } } } @@ -519,9 +521,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") - } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } @@ -529,7 +528,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit for _, kandangID := range approvableIDs { var chickins []entity.ProjectChickin if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil { - return nil, err + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load approved chickins") } updated = append(updated, chickins...) } @@ -608,7 +607,6 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti } } - // Add to target product warehouse if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{ "quantity": gorm.Expr("quantity + ?", quantityToConvert), }, nil); err != nil { @@ -627,11 +625,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti CreatedBy: actorID, } if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err - } - s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) + return err } } From 3a5c49c511251de1c550c3502c2c3b0b17b994cc Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 5 Nov 2025 08:40:27 +0700 Subject: [PATCH 12/59] fix[BE]: fix naming on project_flock_kandang dto to standarized project --- .../dto/project_flock_kandang.dto.go | 94 +++++++++---------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index b7dcdbef..10572c78 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -16,11 +16,13 @@ import ( userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) +// === DTO Structs (ordered) === + type ProjectFlockKandangBaseDTO struct { Id uint `json:"id"` } -type ProjectFlockCustomDTO struct { +type ProjectFlockDTO struct { Id uint `json:"id"` Period int `json:"period"` Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` @@ -33,7 +35,7 @@ type ProjectFlockCustomDTO struct { UpdatedAt time.Time `json:"updated_at"` } -type KandangCustomDTO struct { +type KandangDTO struct { Id uint `json:"id"` Name string `json:"name"` Status string `json:"status"` @@ -52,8 +54,8 @@ type AvailableQtyDTO struct { type ProjectFlockKandangListDTO struct { ProjectFlockKandangBaseDTO - ProjectFlock *ProjectFlockCustomDTO `json:"project_flock,omitempty"` - Kandang *KandangCustomDTO `json:"kandang,omitempty"` + ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` + Kandang *KandangDTO `json:"kandang,omitempty"` Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"` AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` @@ -65,18 +67,20 @@ type ProjectFlockKandangDetailDTO struct { ProjectFlockKandangListDTO } +// === Mapper Functions (ordered) === + func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO { return ProjectFlockKandangBaseDTO{ Id: e.Id, } } -func toProjectFlockCustomDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockCustomDTO { +func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO { if pf == nil { return nil } - return &ProjectFlockCustomDTO{ + return &ProjectFlockDTO{ Id: pf.Id, Period: pf.Period, Flock: pf.Flock, @@ -99,28 +103,32 @@ func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, return ProjectFlockKandangListDTO{ ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), - ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), - Kandang: buildKandangFromEntity(e.Kandang), - Chickins: buildChickins(e.Chickins), - AvailableQtys: buildAvailableQtysFromRaw(availableQtysRaw), + ProjectFlock: toProjectFlockDTO(projectFlockSummary), + Kandang: toKandangDTO(e.Kandang), + Chickins: toChickinDTOs(e.Chickins), + AvailableQtys: toAvailableQtyDTOsFromRaw(availableQtysRaw), CreatedAt: e.CreatedAt, - CreatedUser: buildCreatedUser(e.ProjectFlock), - Approval: defaultProjectFlockKandangLatestApproval(e), + CreatedUser: toCreatedUserDTO(e.ProjectFlock), + Approval: toApprovalDTO(e), } } -func toKandangCustomDTO(k projectFlockDTO.KandangWithProjectFlockIdDTO) KandangCustomDTO { - return KandangCustomDTO{ - Id: k.Id, - Name: k.Name, - Status: k.Status, +func toKandangDTO(kandang entity.Kandang) *KandangDTO { + if kandang.Id == 0 { + return nil + } + + return &KandangDTO{ + Id: kandang.Id, + Name: kandang.Name, + Status: kandang.Status, } } -func defaultProjectFlockKandangLatestApproval(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { +func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { if e.LatestApproval != nil { - result := approvalDTO.ToApprovalDTO(*e.LatestApproval) - return &result + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + return &mapped } return nil } @@ -134,13 +142,13 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand return ProjectFlockKandangListDTO{ ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), - ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), - Kandang: buildKandangFromEntity(e.Kandang), - Chickins: buildChickins(e.Chickins), - AvailableQtys: buildAvailableQtys(e.Chickins), + ProjectFlock: toProjectFlockDTO(projectFlockSummary), + Kandang: toKandangDTO(e.Kandang), + Chickins: toChickinDTOs(e.Chickins), + AvailableQtys: toAvailableQtyDTOs(e.Chickins), CreatedAt: e.CreatedAt, - CreatedUser: buildCreatedUser(e.ProjectFlock), - Approval: defaultProjectFlockKandangLatestApproval(e), + CreatedUser: toCreatedUserDTO(e.ProjectFlock), + Approval: toApprovalDTO(e), } } @@ -158,7 +166,9 @@ func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKa } } -func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehouseDTO { +// === Helper Functions (ordered) === + +func toProductWarehouseDTO(pwData map[string]interface{}) *ProductWarehouseDTO { if pwData == nil { return nil } @@ -172,17 +182,17 @@ func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehou } if pData, ok := pwData["product"].(map[string]interface{}); ok { - dto.Product = buildProductFromMap(pData) + dto.Product = toProductDTO(pData) } if wData, ok := pwData["warehouse"].(map[string]interface{}); ok { - dto.Warehouse = buildWarehouseFromMap(wData) + dto.Warehouse = toWarehouseDTO(wData) } return dto } -func buildProductFromMap(pData map[string]interface{}) *productDTO.ProductBaseDTO { +func toProductDTO(pData map[string]interface{}) *productDTO.ProductBaseDTO { if pData == nil { return nil } @@ -199,7 +209,7 @@ func buildProductFromMap(pData map[string]interface{}) *productDTO.ProductBaseDT return product } -func buildWarehouseFromMap(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO { +func toWarehouseDTO(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO { if wData == nil { return nil } @@ -219,7 +229,7 @@ func buildWarehouseFromMap(wData map[string]interface{}) *warehouseDTO.Warehouse return warehouse } -func buildCreatedUser(pf entity.ProjectFlock) *userDTO.UserBaseDTO { +func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO { if pf.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(pf.CreatedUser) return &mapped @@ -232,19 +242,7 @@ func buildCreatedUser(pf entity.ProjectFlock) *userDTO.UserBaseDTO { return nil } -func buildKandangFromEntity(kandang entity.Kandang) *KandangCustomDTO { - if kandang.Id == 0 { - return nil - } - - return &KandangCustomDTO{ - Id: kandang.Id, - Name: kandang.Name, - Status: kandang.Status, - } -} - -func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { +func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { if len(chickins) == 0 { return nil } @@ -256,7 +254,7 @@ func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO return result } -func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { +func toAvailableQtyDTOs(chickins []entity.ProjectChickin) []AvailableQtyDTO { if len(chickins) == 0 { return nil } @@ -306,7 +304,7 @@ func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { return result } -func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { +func toAvailableQtyDTOsFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { if len(availableQtysRaw) == 0 { return nil } @@ -318,7 +316,7 @@ func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []Avai continue } - pwDTO := buildProductWarehouseFromMap(pwData) + pwDTO := toProductWarehouseDTO(pwData) availableQty := 0.0 if qty, ok := v["available_qty"].(float64); ok { availableQty = qty From 1ee97b91a5c86dd7b4f1c546d0ff97d069d539f9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 5 Nov 2025 08:56:18 +0700 Subject: [PATCH 13/59] feat[BE-127]: Createing transfer laying create one, approvals, get one, get all, update, delete, but Still unfinished --- ...74825_create_laying_transfers_table.up.sql | 3 +- internal/entities/laying_transfer.go | 5 +- .../repositories/warehouse.repository.go | 14 + .../project_chickin.repository.go | 15 + .../project_flock_population_repository.go | 14 +- .../controllers/transfer_laying.controller.go | 39 +- .../dto/transfer_laying.dto.go | 233 ++++++- .../production/transfer_layings/module.go | 27 +- .../laying_transfer.repository.go | 4 + .../production/transfer_layings/route.go | 2 + .../services/transfer_laying.service.go | 620 ++++++++++++++++-- .../validations/transfer_laying.validation.go | 10 + internal/utils/constant.go | 16 + 13 files changed, 928 insertions(+), 74 deletions(-) diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql index a01f6e02..69b0fb5d 100644 --- a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -4,7 +4,8 @@ CREATE TABLE IF NOT EXISTS laying_transfers ( from_project_flock_id BIGINT NOT NULL, to_project_flock_id BIGINT NOT NULL, transfer_date DATE NOT NULL, - total_qty NUMERIC(15, 3) NOT NULL, + pending_usage_qty NUMERIC(15, 3), + usage_qty NUMERIC(15, 3), notes TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), diff --git a/internal/entities/laying_transfer.go b/internal/entities/laying_transfer.go index 8c2058df..dd173042 100644 --- a/internal/entities/laying_transfer.go +++ b/internal/entities/laying_transfer.go @@ -12,7 +12,8 @@ type LayingTransfer struct { FromProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"` TransferDate time.Time `gorm:"type:date;not null"` - TotalQty float64 `gorm:"type:numeric(15,3);not null"` + PendingUsageQty *float64 `gorm:"type:numeric(15,3)"` + UsageQty *float64 `gorm:"type:numeric(15,3)"` Notes string `gorm:"type:text"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` @@ -24,5 +25,5 @@ type LayingTransfer struct { CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` + LatestApproval *Approval `gorm:"-" json:"-"` } - diff --git a/internal/modules/master/warehouses/repositories/warehouse.repository.go b/internal/modules/master/warehouses/repositories/warehouse.repository.go index 956c30ef..e879e01a 100644 --- a/internal/modules/master/warehouses/repositories/warehouse.repository.go +++ b/internal/modules/master/warehouses/repositories/warehouse.repository.go @@ -16,6 +16,7 @@ type WarehouseRepository interface { 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) + GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) } type WarehouseRepositoryImpl struct { @@ -60,3 +61,16 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId } return &warehouse, nil } + +func (r *WarehouseRepositoryImpl) GetLatestByKandangID(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"). + Order("id DESC"). + First(&warehouse).Error + if err != nil { + return nil, err + } + return &warehouse, nil +} diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index c0f522ad..a98dab67 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -13,6 +13,7 @@ type ProjectChickinRepository interface { GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) + GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) } type ChickinRepositoryImpl struct { @@ -64,3 +65,17 @@ func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Co } return chickins, nil } + +func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { + var total float64 + err := r.db.WithContext(ctx). + Model(&entity.ProjectChickin{}). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + Where("pending_usage_qty > 0"). + Select("COALESCE(SUM(pending_usage_qty), 0)"). + Row().Scan(&total) + if err != nil { + return 0, err + } + return total, nil +} 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 index 2f622656..d914b128 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -10,7 +10,7 @@ import ( type ProjectFlockPopulationRepository interface { // domain-specific - GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) + GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) @@ -44,15 +44,17 @@ func (r *projectFlockPopulationRepositoryImpl) DB() *gorm.DB { return r.BaseRepositoryImpl.DB() } -func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) { - var record entity.ProjectFlockPopulation +func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) { + var records []entity.ProjectFlockPopulation err := r.DB().WithContext(ctx). - Where("project_flock_kandang_id = ?", projectFlockKandangID). - First(&record).Error + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID). + Preload("ProjectChickin"). + Find(&records).Error if err != nil { return nil, err } - return &record, nil + return records, nil } func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) { diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index f962c263..62d64127 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -30,6 +30,10 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)), TransferDateFrom: c.Query("transfer_date_from", ""), TransferDateTo: c.Query("transfer_date_to", ""), + ApprovalStatus: c.Query("approval_status", ""), + TransferNumber: c.Query("transfer_number", ""), + Sort: c.Query("sort", "created_at"), + Order: c.Query("order", "desc"), } if query.Page < 1 || query.Limit < 1 { @@ -64,7 +68,7 @@ func (u *TransferLayingController) GetOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, err := u.TransferLayingService.GetOne(c, uint(id)) + result, approval, err := u.TransferLayingService.GetOneWithApproval(c, uint(id)) if err != nil { return err } @@ -74,7 +78,7 @@ func (u *TransferLayingController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get transferLaying successfully", - Data: dto.ToTransferLayingListDTO(*result), + Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, approval), }) } @@ -145,3 +149,34 @@ func (u *TransferLayingController) DeleteOne(c *fiber.Ctx) error { Message: "Delete transferLaying successfully", }) } + +func (u *TransferLayingController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + results, err := u.TransferLayingService.Approval(c, req) + if err != nil { + return err + } + + var ( + data interface{} + message = "Submit transfer laying approval successfully" + ) + if len(results) == 1 { + data = dto.ToTransferLayingListDTO(results[0]) + } else { + message = "Submit transfer laying approvals successfully" + data = dto.ToTransferLayingListDTOs(results) + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: message, + Data: data, + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 69aced33..719e458a 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -4,61 +4,252 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs === type TransferLayingBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint `json:"id"` + TransferNumber string `json:"transfer_number"` + TransferDate time.Time `json:"transfer_date"` + Notes string `json:"notes"` +} + +type ProjectFlockSummaryDTO struct { + Id uint `json:"id"` + Period int `json:"period"` + Category string `json:"category"` +} + +type ProductSummaryDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type WarehouseSummaryDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +type ProductWarehouseSummaryDTO struct { + Product *ProductSummaryDTO `json:"product,omitempty"` + Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"` +} + +type ProjectFlockKandangSummaryDTO struct { + Id uint `json:"id"` + KandangId uint `json:"kandang_id"` +} + +type LayingTransferSourceDTO struct { + SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"` + Qty float64 `json:"qty"` + ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"` + Note string `json:"note,omitempty"` +} + +type LayingTransferTargetDTO struct { + TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"` + Qty float64 `json:"qty"` + ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"` + Note string `json:"note,omitempty"` } type TransferLayingListDTO struct { TransferLayingBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"` + ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"` + PendingUsageQty *float64 `json:"pending_usage_qty"` + UsageQty *float64 `json:"usage_qty"` + CreatedBy uint `json:"created_by"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } type TransferLayingDetailDTO struct { TransferLayingListDTO + Sources []LayingTransferSourceDTO `json:"sources,omitempty"` + Targets []LayingTransferTargetDTO `json:"targets,omitempty"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } // === Mapper Functions === +func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { + if pf == nil || pf.Id == 0 { + return nil + } + + return &ProjectFlockSummaryDTO{ + Id: pf.Id, + Period: pf.Period, + Category: pf.Category, + } +} + +func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO { + if pfk == nil || pfk.Id == 0 { + return nil + } + + return &ProjectFlockKandangSummaryDTO{ + Id: pfk.Id, + KandangId: pfk.KandangId, + } +} + +func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO { + if product == nil || product.Id == 0 { + return nil + } + + return &ProductSummaryDTO{ + Id: product.Id, + Name: product.Name, + } +} + +func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO { + if warehouse == nil || warehouse.Id == 0 { + return nil + } + + return &WarehouseSummaryDTO{ + Id: warehouse.Id, + Name: warehouse.Name, + Type: warehouse.Type, + } +} + +func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO { + if pw == nil || pw.Id == 0 { + return nil + } + + return &ProductWarehouseSummaryDTO{ + Product: ToProductSummaryDTO(&pw.Product), + Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse), + } +} + +func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO { + return LayingTransferSourceDTO{ + SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), + Qty: source.Qty, + ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), + Note: source.Note, + } +} + +func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingTransferSourceDTO { + if len(sources) == 0 { + return []LayingTransferSourceDTO{} + } + result := make([]LayingTransferSourceDTO, len(sources)) + for i, s := range sources { + result[i] = ToLayingTransferSourceDTO(s) + } + return result +} + +func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO { + return LayingTransferTargetDTO{ + TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang), + Qty: target.Qty, + ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse), + Note: target.Note, + } +} + +func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingTransferTargetDTO { + if len(targets) == 0 { + return []LayingTransferTargetDTO{} + } + result := make([]LayingTransferTargetDTO, len(targets)) + for i, t := range targets { + result[i] = ToLayingTransferTargetDTO(t) + } + return result +} + func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO { return TransferLayingBaseDTO{ - Id: e.Id, - + Id: e.Id, + TransferNumber: e.TransferNumber, + TransferDate: e.TransferDate, + Notes: e.Notes, } } func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } return TransferLayingListDTO{ TransferLayingBaseDTO: ToTransferLayingBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock), + ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock), + PendingUsageQty: e.PendingUsageQty, + UsageQty: e.UsageQty, + CreatedBy: e.CreatedBy, + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, } } -func ToTransferLayingListDTOs(e []entity.LayingTransfer) []TransferLayingListDTO { - result := make([]TransferLayingListDTO, len(e)) - for i, r := range e { - result[i] = ToTransferLayingListDTO(r) +func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO { + var latestApproval *approvalDTO.ApprovalBaseDTO + + // Use LatestApproval from entity if available + if e.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + latestApproval = &mapped + } else if len(approvals) > 0 { + // Fallback to approvals slice + latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1]) + latestApproval = &latest + } + + return TransferLayingDetailDTO{ + TransferLayingListDTO: ToTransferLayingListDTO(e), + Sources: ToLayingTransferSourceDTOs(e.Sources), + Targets: ToLayingTransferTargetDTOs(e.Targets), + Approval: latestApproval, + } +} + +func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO { + var mappedApproval *approvalDTO.ApprovalBaseDTO + + // Prefer LatestApproval from entity + if e.LatestApproval != nil && e.LatestApproval.Id != 0 { + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + mappedApproval = &mapped + } else if approval != nil && approval.Id != 0 { + // Fallback to passed approval parameter + mapped := approvalDTO.ToApprovalDTO(*approval) + mappedApproval = &mapped + } + + return TransferLayingDetailDTO{ + TransferLayingListDTO: ToTransferLayingListDTO(e), + Sources: ToLayingTransferSourceDTOs(e.Sources), + Targets: ToLayingTransferTargetDTOs(e.Targets), + Approval: mappedApproval, + } +} + +func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingListDTO { + result := make([]TransferLayingListDTO, len(items)) + for i, item := range items { + result[i] = ToTransferLayingListDTO(item) } return result } - -func ToTransferLayingDetailDTO(e entity.LayingTransfer) TransferLayingDetailDTO { - return TransferLayingDetailDTO{ - TransferLayingListDTO: ToTransferLayingListDTO(e), - } -} diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go index b309f19e..27851b71 100644 --- a/internal/modules/production/transfer_layings/module.go +++ b/internal/modules/production/transfer_layings/module.go @@ -1,14 +1,21 @@ package transfer_layings import ( + "fmt" + "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" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) @@ -20,8 +27,26 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val userRepo := rUser.NewUserRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) + projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db) + productWarehouseRepo := rInventory.NewProductWarehouseRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) - transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, projectFlockRepo, projectFlockKandangRepo, validate) + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register transfer to laying approval workflow: %v", err)) + } + + transferLayingService := sTransferLaying.NewTransferLayingService( + transferLayingRepo, + projectFlockRepo, + projectFlockKandangRepo, + projectFlockPopulationRepo, + productWarehouseRepo, + warehouseRepo, + approvalService, + validate, + ) userService := sUser.NewUserService(userRepo, validate) TransferLayingRoutes(router, userService, transferLayingService) diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index 2e5437da..3dab5120 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -11,6 +11,7 @@ import ( type TransferLayingRepository interface { repository.BaseRepository[entity.LayingTransfer] GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) + IdExists(ctx context.Context, id uint) (bool, error) } type TransferLayingRepositoryImpl struct { @@ -24,6 +25,9 @@ func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { db: db, } } +func (r *TransferLayingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.LayingTransfer](ctx, r.db, id) +} func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) { var transfer entity.LayingTransfer diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go index ee806506..13a0bc8f 100644 --- a/internal/modules/production/transfer_layings/route.go +++ b/internal/modules/production/transfer_layings/route.go @@ -19,10 +19,12 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying. // 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.Post("/approval", m.Auth(u), ctrl.Approval) 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("/approvals", ctrl.Approval) } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 0a5b5dd2..f9fc5987 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -1,10 +1,17 @@ package service import ( + "context" "errors" + "fmt" + "strings" + "time" - common "gitlab.com/mbugroup/lti-api.git/internal/common/service" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" @@ -19,31 +26,61 @@ import ( type TransferLayingService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error) + GetOneWithApproval(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) DeleteOne(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) } type transferLayingService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.TransferLayingRepository - ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository - ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.TransferLayingRepository + ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository + ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository + ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository + ProductWarehouseRepo rInventory.ProductWarehouseRepository + WarehouseRepo rWarehouse.WarehouseRepository + ApprovalService commonSvc.ApprovalService } -func NewTransferLayingService(repo repository.TransferLayingRepository, projectFlockRepo ProjectFlockRepository.ProjectflockRepository, projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository, validate *validator.Validate) TransferLayingService { +func NewTransferLayingService( + repo repository.TransferLayingRepository, + projectFlockRepo ProjectFlockRepository.ProjectflockRepository, + projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository, + projectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, + productWarehouseRepo rInventory.ProductWarehouseRepository, + warehouseRepo rWarehouse.WarehouseRepository, + approvalService commonSvc.ApprovalService, + validate *validator.Validate, +) TransferLayingService { return &transferLayingService{ - Log: utils.Log, - Validate: validate, - Repository: repo, - ProjectFlockRepo: projectFlockRepo, - ProjectFlockKandangRepo: projectFlockKandangRepo, + Log: utils.Log, + Validate: validate, + Repository: repo, + ProjectFlockRepo: projectFlockRepo, + ProjectFlockKandangRepo: projectFlockKandangRepo, + ProjectFlockPopulationRepo: projectFlockPopulationRepo, + ProductWarehouseRepo: productWarehouseRepo, + WarehouseRepo: warehouseRepo, + ApprovalService: approvalService, } } func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") + return db. + Preload("CreatedUser"). + Preload("Sources"). + Preload("Sources.SourceProjectFlockKandang"). + Preload("Sources.ProductWarehouse"). + Preload("Sources.ProductWarehouse.Product"). + Preload("Sources.ProductWarehouse.Warehouse"). + Preload("Targets"). + Preload("Targets.TargetProjectFlockKandang"). + Preload("Targets.ProductWarehouse"). + Preload("Targets.ProductWarehouse.Product"). + Preload("Targets.ProductWarehouse.Warehouse") } func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) { @@ -67,13 +104,45 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ if params.TransferDateTo != "" { db = db.Where("transfer_date <= ?", params.TransferDateTo) } - return db.Order("created_at DESC").Order("updated_at DESC") + if params.TransferNumber != "" { + db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%") + } + + // Handle sort + sortField := "created_at" + if params.Sort != "" { + sortField = params.Sort + } + sortOrder := "DESC" + if params.Order == "asc" { + sortOrder = "ASC" + } + db = db.Order(fmt.Sprintf("%s %s", sortField, sortOrder)) + + return db }) if err != nil { s.Log.Errorf("Failed to get transferLayings: %+v", err) return nil, 0, err } + + // Filter by approval status if requested + if params.ApprovalStatus != "" { + var filtered []entity.LayingTransfer + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + + for _, transfer := range transferLayings { + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil) + if err == nil && latestApproval != nil && latestApproval.Action != nil { + if string(*latestApproval.Action) == params.ApprovalStatus { + filtered = append(filtered, transfer) + } + } + } + transferLayings = filtered + } + return transferLayings, total, nil } @@ -86,37 +155,69 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran s.Log.Errorf("Failed get transferLaying by id: %+v", err) return nil, err } + + // Fetch and populate latest approval + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil) + if err == nil && latestApproval != nil { + transferLaying.LatestApproval = latestApproval + } + return transferLaying, nil } +func (s transferLayingService) GetOneWithApproval(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) { + transferLaying, err := s.GetOne(c, id) + if err != nil { + return nil, nil, err + } + + // Return the LatestApproval that was populated in GetOne + return transferLaying, transferLaying.LatestApproval, nil +} + func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, - common.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + commonSvc.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, ); err != nil { return nil, err } - // Validate source kandangs for _, detail := range req.SourceKandangs { - if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, ); err != nil { return nil, err } + + pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get source project flock kandang") + } + if pfk.ProjectFlockId != req.SourceProjectFlockId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d does not belong to source project flock %d", detail.ProjectFlockKandangId, req.SourceProjectFlockId)) + } } - // Validate target kandangs for _, detail := range req.TargetKandangs { - if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, ); err != nil { return nil, err } + + pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + } + if pfk.ProjectFlockId != req.TargetProjectFlockId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Target kandang %d does not belong to target project flock %d", detail.ProjectFlockKandangId, req.TargetProjectFlockId)) + } } transferDate, err := utils.ParseDateString(req.TransferDate) @@ -124,34 +225,133 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") } - var totalQty float64 - for _, item := range req.SourceKandangs { - totalQty += item.Quantity + var totalSourceQty, totalTargetQty float64 + sourceWarehouseMap := make(map[uint]uint) + + for _, sourceDetail := range req.SourceKandangs { + if sourceDetail.Quantity <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Source kandang quantity must be greater than 0") + } + totalSourceQty += sourceDetail.Quantity + + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId) + if err != nil { + return nil, err + } + + var totalPopulation float64 + var productWarehouseId uint + for _, pop := range populations { + totalPopulation += pop.TotalQty + if productWarehouseId == 0 { + productWarehouseId = pop.ProductWarehouseId + } + } + + if totalPopulation == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available for transfer", sourceDetail.ProjectFlockKandangId)) + } + + if totalPopulation < sourceDetail.Quantity { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity)) + } + + sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId + } + + for _, targetDetail := range req.TargetKandangs { + if targetDetail.Quantity <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Target kandang quantity must be greater than 0") + } + totalTargetQty += targetDetail.Quantity + } + + if totalSourceQty != totalTargetQty { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total source quantity (%f) must equal total target quantity (%f)", totalSourceQty, totalTargetQty)) + } + + transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano()) + + createBody := &entity.LayingTransfer{ + TransferNumber: transferNumber, + Notes: req.Reason, + FromProjectFlockId: req.SourceProjectFlockId, + ToProjectFlockId: req.TargetProjectFlockId, + TransferDate: transferDate, + PendingUsageQty: &totalSourceQty, + CreatedBy: 1, //todo : harus diambil dari auth } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - createBody := &entity.LayingTransfer{ - Notes: req.Reason, - FromProjectFlockId: req.SourceProjectFlockId, - ToProjectFlockId: req.TargetProjectFlockId, - TransferDate: transferDate, - TotalQty: totalQty, - CreatedBy: 1, //todo : harus diambil dari auth + if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record") } - if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil { - return err + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) + projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) + + for _, sourceDetail := range req.SourceKandangs { + productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] + + source := entity.LayingTransferSource{ + LayingTransferId: createBody.Id, + SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, + Qty: sourceDetail.Quantity, + ProductWarehouseId: &productWarehouseId, + } + if err := dbTransaction.Create(&source).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source") + } + + if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to reduce project flock population") + } + + if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("quantity - ?", sourceDetail.Quantity)}, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity") + } + } + + for _, targetDetail := range req.TargetKandangs { + + targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + } + + // Get warehouse for this kandang + targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") + } + + target := entity.LayingTransferTarget{ + LayingTransferId: createBody.Id, + TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId, + Qty: targetDetail.Quantity, + ProductWarehouseId: &targetWarehouse.Id, + } + if err := dbTransaction.Create(&target).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target") + } + } + + if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval") } return nil }) if err != nil { - return nil, err + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying") } - return nil, err + return s.GetOne(c, createBody.Id) } func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) { @@ -159,6 +359,30 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return nil, err } + // Check if transfer laying exists + _, err := s.Repository.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying") + } + + // Check if latest approval is PENDING (not approved) + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + // If latest approval exists and is APPROVED or REJECTED, cannot update + if latestApproval != nil && latestApproval.Action != nil { + action := string(*latestApproval.Action) + if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot update transfer laying with status %s", action)) + } + } + updateBody := make(map[string]any) if req.TransferDate != nil { @@ -178,19 +402,333 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") } s.Log.Errorf("Failed to update transferLaying: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying") } return s.GetOne(c, id) } func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + // Verify transfer laying exists + _, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return db.Preload("Sources.ProductWarehouse").Preload("Targets") + }) + if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") } - s.Log.Errorf("Failed to delete transferLaying: %+v", err) - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying") } + + // Check if latest approval is PENDING (not approved/rejected) + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + // If latest approval exists and is APPROVED or REJECTED, cannot delete + if latestApproval != nil && latestApproval.Action != nil { + action := string(*latestApproval.Action) + if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action)) + } + } + + // Delete in transaction to handle cascades and qty restoration + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + // Restore source warehouse quantities + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) + + // Get source repository for detail info + sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) + sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources") + } + + // Restore quantity for each source that was reduced + for _, source := range sources { + if source.ProductWarehouseId != nil && source.Qty > 0 { + // Add back the quantity that was transferred + if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{ + "quantity": gorm.Expr("quantity + ?", source.Qty), + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity") + } + } + } + + // Restore project flock population that was reduced + projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) + for _, source := range sources { + populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration") + } + + // Restore to latest populations first + remainingToRestore := source.Qty + for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- { + pop := populations[i] + restoreAmount := remainingToRestore + if remainingToRestore < pop.TotalQty { + // Cap restore to what can fit in this population + restoreAmount = remainingToRestore + } + + newQty := pop.TotalQty + restoreAmount + if err := projectFlockPopulationRepoTx.PatchOne(c.Context(), pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore population quantity") + } + + remainingToRestore -= restoreAmount + } + } + + // Delete the transfer laying (cascade will delete sources and targets) + if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying") + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return fiberErr + } + s.Log.Errorf("Failed to delete transferLaying: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying") + } + + return nil +} + +func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) { + if err := s.Validate.Struct(req); err != nil { + 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") + } + + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + step := utils.TransferToLayingStepPengajuan + if action == entity.ApprovalActionApproved { + step = utils.TransferToLayingStepDisetujui + } + + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) + targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) + + for _, approvableID := range approvableIDs { + transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID)) + } + return err + } + + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowTransferToLaying, + approvableID, + step, + &action, + actorID, + req.Notes, + ); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + if action == entity.ApprovalActionApproved && transfer.PendingUsageQty != nil && *transfer.PendingUsageQty > 0 { + + sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources") + } + + targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer targets") + } + + if len(sources) > 0 && len(targets) > 0 { + firstSource := sources[0] + if firstSource.ProductWarehouseId == nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID)) + } + + sourceWarehouse, err := productWarehouseRepoTx.GetByID(c.Context(), *firstSource.ProductWarehouseId, nil) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse") + } + + for _, target := range targets { + + targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), target.TargetProjectFlockKandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + } + + targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") + } + + if _, err := s.getOrCreateProductWarehouse( + c.Context(), + dbTransaction, + sourceWarehouse.ProductId, + targetWarehouse.Id, + target.Qty, + ); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse") + } + } + } + + usageQty := *transfer.PendingUsageQty + updateData := map[string]any{ + "usage_qty": usageQty, + "pending_usage_qty": nil, + } + if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), approvableID, updateData, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status") + } + } + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + updated := make([]entity.LayingTransfer, 0, len(approvableIDs)) + for _, approvableID := range approvableIDs { + transfer, err := s.GetOne(c, approvableID) + if err != nil { + return nil, err + } + updated = append(updated, *transfer) + } + + return updated, nil +} + +func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayingID uint, actorID uint) error { + if transferLayingID == 0 || actorID == 0 { + return nil + } + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + action := entity.ApprovalActionCreated + + _, err := approvalSvc.CreateApproval( + ctx, + utils.ApprovalWorkflowTransferToLaying, + transferLayingID, + utils.TransferToLayingStepPengajuan, + &action, + actorID, + nil, + ) + return err +} + +func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64) (*entity.ProductWarehouse, error) { + + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(tx) + + existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID) + if err == nil && existing != nil { + + if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"quantity": gorm.Expr("quantity + ?", quantity)}, nil); err != nil { + return nil, err + } + return existing, nil + } + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + newWarehouse := &entity.ProductWarehouse{ + ProductId: productID, + WarehouseId: warehouseID, + Quantity: quantity, + } + + if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil { + return nil, err + } + + return newWarehouse, nil +} + +func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToReduce float64) error { + + populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) + if err != nil { + return err + } + + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "No populations found for reduction") + } + + remainingToReduce := quantityToReduce + + for i := len(populations) - 1; i >= 0; i-- { + if remainingToReduce <= 0 { + break + } + + pop := populations[i] + reductionAmount := remainingToReduce + if pop.TotalQty < remainingToReduce { + reductionAmount = pop.TotalQty + } + + newQty := pop.TotalQty - reductionAmount + if err := populationRepo.PatchOne(ctx, pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil { + return err + } + + remainingToReduce -= reductionAmount + } + + if remainingToReduce > 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient population to reduce. Still need to reduce: %.0f", remainingToReduce)) + } + return nil } diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 1d291ac5..e35ba4f3 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -31,4 +31,14 @@ type Query struct { TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"` TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"` TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"` + ApprovalStatus string `query:"approval_status" validate:"omitempty,oneof=PENDING APPROVED REJECTED"` // Filter by latest approval status + TransferNumber string `query:"transfer_number" validate:"omitempty"` // Search by transfer number + Sort string `query:"sort" validate:"omitempty,oneof=created_at transfer_date"` // Sort by field + Order string `query:"order" validate:"omitempty,oneof=asc desc"` // Sort order +} + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index ef08eaed..4098c91c 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -176,6 +176,22 @@ var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{ ProjectFlockKandangStepDisetujui: "Disetujui", } +// ------------------------------------------------------------------- +// Transfer To laying Approval +// ------------------------------------------------------------------- +const ( + ApprovalWorkflowTransferToLaying approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("TRANSFER_TO_LAYINGS") + TransferToLayingStepPengajuan approvalutils.ApprovalStep = 1 + TransferToLayingStepDisetujui approvalutils.ApprovalStep = 2 +) + +var TransferToLayingApprovalSteps = map[approvalutils.ApprovalStep]string{ + TransferToLayingStepPengajuan: "Pengajuan", + TransferToLayingStepDisetujui: "Disetujui", +} + +// ------------------------------------------------------------------- + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- From 8f74391f1e25c84278a4d6d36b09d309b350c8f1 Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 5 Nov 2025 18:58:06 +0700 Subject: [PATCH 14/59] unfinished purchase --- .../20251104084540_purchase-items.down.sql | 1 + .../20251104084540_purchase-items.up.sql | 59 +++ .../20251104084555_purchases.down.sql | 14 + .../20251104084555_purchases.up.sql | 64 ++++ internal/entities/purchase.go | 26 ++ internal/entities/purchase_item.go | 31 ++ .../modules/approvals/dto/approval.dto.go | 2 + .../product_warehouse.repository.go | 15 + .../repositories/warehouse.repository.go | 13 + .../production/project_flocks/route.go | 4 +- .../repositories/recording.repository.go | 20 +- .../recordings/services/recording.service.go | 147 ++++++-- .../validations/recording.validation.go | 6 +- .../controllers/purchase.controller.go | 67 ++++ .../modules/purchases/dto/purchase.dto.go | 155 ++++++++ internal/modules/purchases/module.go | 13 + .../repositories/purchase.repository.go | 113 ++++++ internal/modules/purchases/route.go | 59 +++ .../purchases/services/purchase.service.go | 337 ++++++++++++++++++ .../validations/purchase.validation.go | 27 ++ internal/route/route.go | 10 +- internal/utils/constant.go | 19 +- internal/utils/recording/util.recording.go | 5 +- 23 files changed, 1155 insertions(+), 52 deletions(-) create mode 100644 internal/database/migrations/20251104084540_purchase-items.down.sql create mode 100644 internal/database/migrations/20251104084540_purchase-items.up.sql create mode 100644 internal/database/migrations/20251104084555_purchases.down.sql create mode 100644 internal/database/migrations/20251104084555_purchases.up.sql create mode 100644 internal/entities/purchase.go create mode 100644 internal/entities/purchase_item.go create mode 100644 internal/modules/purchases/controllers/purchase.controller.go create mode 100644 internal/modules/purchases/dto/purchase.dto.go create mode 100644 internal/modules/purchases/module.go create mode 100644 internal/modules/purchases/repositories/purchase.repository.go create mode 100644 internal/modules/purchases/route.go create mode 100644 internal/modules/purchases/services/purchase.service.go create mode 100644 internal/modules/purchases/validations/purchase.validation.go diff --git a/internal/database/migrations/20251104084540_purchase-items.down.sql b/internal/database/migrations/20251104084540_purchase-items.down.sql new file mode 100644 index 00000000..46d2b5eb --- /dev/null +++ b/internal/database/migrations/20251104084540_purchase-items.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS purchase_items; diff --git a/internal/database/migrations/20251104084540_purchase-items.up.sql b/internal/database/migrations/20251104084540_purchase-items.up.sql new file mode 100644 index 00000000..a09b1d15 --- /dev/null +++ b/internal/database/migrations/20251104084540_purchase-items.up.sql @@ -0,0 +1,59 @@ +CREATE TABLE IF NOT EXISTS purchase_items ( + id BIGSERIAL PRIMARY KEY, + purchase_id BIGINT NOT NULL, + product_id BIGINT NOT NULL, + warehouse_id BIGINT NOT NULL, + product_warehouse_id BIGINT, + received_date TIMESTAMPTZ, + travel_number VARCHAR, + travel_number_docs VARCHAR, + vehicle_number VARCHAR, + sub_qty NUMERIC(15, 3) NOT NULL, + total_qty NUMERIC(15, 3) DEFAULT 0, + total_used NUMERIC(15, 3) DEFAULT 0, + price NUMERIC(15, 3) DEFAULT 0, + total_price NUMERIC(15, 3) DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN + EXECUTE + 'ALTER TABLE purchase_items + ADD CONSTRAINT fk_purchase_items_product + FOREIGN KEY (product_id) + REFERENCES products(id) + ON DELETE RESTRICT ON UPDATE CASCADE'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN + EXECUTE + 'ALTER TABLE purchase_items + ADD CONSTRAINT fk_purchase_items_warehouse + FOREIGN KEY (warehouse_id) + REFERENCES warehouses(id) + ON DELETE RESTRICT ON UPDATE CASCADE'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + EXECUTE + 'ALTER TABLE purchase_items + ADD CONSTRAINT fk_purchase_items_product_warehouse + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE SET NULL ON UPDATE CASCADE'; + END IF; +END $$; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_purchase_items_unique_allocation + ON purchase_items (purchase_id, product_id, warehouse_id) + WHERE deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id); +CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id); +CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id); +CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id); +CREATE INDEX IF NOT EXISTS idx_purchase_items_deleted_at ON purchase_items (deleted_at); diff --git a/internal/database/migrations/20251104084555_purchases.down.sql b/internal/database/migrations/20251104084555_purchases.down.sql new file mode 100644 index 00000000..f3900bbf --- /dev/null +++ b/internal/database/migrations/20251104084555_purchases.down.sql @@ -0,0 +1,14 @@ +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'fk_purchase_items_purchase' + AND conrelid = 'purchase_items'::regclass + ) THEN + ALTER TABLE purchase_items + DROP CONSTRAINT fk_purchase_items_purchase; + END IF; +END $$; + +DROP TABLE IF EXISTS purchases; diff --git a/internal/database/migrations/20251104084555_purchases.up.sql b/internal/database/migrations/20251104084555_purchases.up.sql new file mode 100644 index 00000000..e42f1606 --- /dev/null +++ b/internal/database/migrations/20251104084555_purchases.up.sql @@ -0,0 +1,64 @@ +CREATE TABLE IF NOT EXISTS purchases ( + id BIGSERIAL PRIMARY KEY, + pr_number VARCHAR NOT NULL, + po_number VARCHAR, + po_date TIMESTAMPTZ, + supplier_id BIGINT NOT NULL, + credit_term INT, + due_date TIMESTAMPTZ, + grand_total NUMERIC(15, 3) DEFAULT 0, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + created_by BIGINT NOT NULL +); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN + EXECUTE + 'ALTER TABLE purchases + ADD CONSTRAINT fk_purchases_supplier + FOREIGN KEY (supplier_id) + REFERENCES suppliers(id) + ON DELETE RESTRICT ON UPDATE CASCADE'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + EXECUTE + 'ALTER TABLE purchases + ADD CONSTRAINT fk_purchases_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE'; + END IF; + + IF EXISTS ( + SELECT 1 FROM pg_tables WHERE tablename = 'purchase_items' + ) AND NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'fk_purchase_items_purchase' + ) THEN + EXECUTE + 'ALTER TABLE purchase_items + ADD CONSTRAINT fk_purchase_items_purchase + FOREIGN KEY (purchase_id) + REFERENCES purchases(id) + ON DELETE CASCADE ON UPDATE CASCADE'; + END IF; +END $$; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_pr_number_unique + ON purchases (pr_number) + WHERE deleted_at IS NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_po_number_unique + ON purchases (po_number) + WHERE deleted_at IS NULL AND po_number IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id); +CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by); +CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date); +CREATE INDEX IF NOT EXISTS idx_purchases_deleted_at ON purchases (deleted_at); diff --git a/internal/entities/purchase.go b/internal/entities/purchase.go new file mode 100644 index 00000000..1a57090a --- /dev/null +++ b/internal/entities/purchase.go @@ -0,0 +1,26 @@ +package entities + +import ( + "time" +) + +type Purchase struct { + Id uint64 `gorm:"primaryKey;autoIncrement"` + PrNumber string `gorm:"not null"` + PoNumber *string + PoDate *time.Time + SupplierId uint64 `gorm:"not null"` + CreditTerm *int + DueDate *time.Time + GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` + Notes *string + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt *time.Time `gorm:"index"` + CreatedBy uint64 `gorm:"not null"` + + // Relations + Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"` + Items []PurchaseItem `gorm:"foreignKey:PurchaseId"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/entities/purchase_item.go b/internal/entities/purchase_item.go new file mode 100644 index 00000000..b092b647 --- /dev/null +++ b/internal/entities/purchase_item.go @@ -0,0 +1,31 @@ +package entities + +import ( + "time" +) + +type PurchaseItem struct { + Id uint64 `gorm:"primaryKey;autoIncrement"` + PurchaseId uint64 `gorm:"not null"` + ProductId uint64 `gorm:"not null"` + WarehouseId uint64 `gorm:"not null"` + ProductWarehouseId *uint64 + ReceivedDate *time.Time + TravelNumber *string + TravelNumberDocs *string + VehicleNumber *string + SubQty float64 `gorm:"type:numeric(15,3);not null"` + TotalQty float64 `gorm:"type:numeric(15,3);default:0"` + TotalUsed float64 `gorm:"type:numeric(15,3);default:0"` + Price float64 `gorm:"type:numeric(15,3);default:0"` + TotalPrice float64 `gorm:"type:numeric(15,3);default:0"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt *time.Time `gorm:"index"` + + // Relations + Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"` + Product *Product `gorm:"foreignKey:ProductId;references:Id"` + Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` +} diff --git a/internal/modules/approvals/dto/approval.dto.go b/internal/modules/approvals/dto/approval.dto.go index 085c367c..52b99fc6 100644 --- a/internal/modules/approvals/dto/approval.dto.go +++ b/internal/modules/approvals/dto/approval.dto.go @@ -11,6 +11,7 @@ import ( ) type ApprovalBaseDTO struct { + Id uint `json:"id"` StepNumber uint16 `json:"step_number"` StepName string `json:"step_name"` Action *string `json:"action"` @@ -27,6 +28,7 @@ type ApprovalGroupDTO struct { func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO { dto := ApprovalBaseDTO{ + Id: e.Id, Notes: e.Notes, } diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index d95c298b..599e9bc7 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -23,6 +23,7 @@ type ProductWarehouseRepository interface { GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error + GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) } type ProductWarehouseRepositoryImpl struct { @@ -149,6 +150,20 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d return nil } +func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) { + var productWarehouse entity.ProductWarehouse + err := r.DB().WithContext(ctx). + Preload("Product"). + Preload("Warehouse"). + Preload("Warehouse.Area"). + Preload("Warehouse.Location"). + First(&productWarehouse, id).Error + if err != nil { + return nil, err + } + return &productWarehouse, nil +} + func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) { var product entity.Product err := r.DB().WithContext(ctx). diff --git a/internal/modules/master/warehouses/repositories/warehouse.repository.go b/internal/modules/master/warehouses/repositories/warehouse.repository.go index e879e01a..ff05b3a1 100644 --- a/internal/modules/master/warehouses/repositories/warehouse.repository.go +++ b/internal/modules/master/warehouses/repositories/warehouse.repository.go @@ -17,6 +17,7 @@ type WarehouseRepository interface { IdExists(ctx context.Context, id uint) (bool, error) GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) + GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error) } type WarehouseRepositoryImpl struct { @@ -62,6 +63,18 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId return &warehouse, nil } +func (r *WarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error) { + var warehouse entity.Warehouse + err := r.db.WithContext(ctx). + Preload("Area"). + Preload("Location"). + First(&warehouse, id).Error + if err != nil { + return nil, err + } + return &warehouse, nil +} + func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) { var warehouse entity.Warehouse err := r.db.WithContext(ctx). diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index 7642b90c..70a22bad 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -12,7 +12,7 @@ import ( func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) { ctrl := controller.NewProjectflockController(s) - route := v1.Group("/project_flocks") + route := v1.Group("/project-flocks") // route.Get("/", m.Auth(u), ctrl.GetAll) // route.Post("/", m.Auth(u), ctrl.CreateOne) @@ -27,6 +27,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Delete("/:id", ctrl.DeleteOne) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) - route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary) + route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary) } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index aa525951..d9512edd 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -254,18 +254,22 @@ func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFloc } func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) { - var population entity.ProjectFlockPopulation + var total float64 err := tx. - Where("project_flock_kandang_id = ?", projectFlockKandangId). - Order("created_at DESC"). - First(&population).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, nil - } + Table("project_flock_populations"). + Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty"). + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId). + Scan(&total).Error if err != nil { return 0, err } - return int64(math.Round(population.TotalQty)), nil + + if total < 0 { + total = 0 + } + + return int64(math.Round(total)), nil } func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) { diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index e8836590..b31a90c0 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -261,6 +261,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return nil, err } + if req.BodyWeights == nil && req.Stocks == nil && req.Depletions == nil && req.Eggs == nil { + return s.GetOne(c, id) + } + ctx := c.Context() var recordingEntity *entity.Recording @@ -277,12 +281,21 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } recordingEntity = recording + hasBodyChanges := req.BodyWeights != nil + hasStockChanges := req.Stocks != nil + hasDepletionChanges := req.Depletions != nil + hasEggChanges := req.Eggs != nil + + if !hasBodyChanges && !hasStockChanges && !hasDepletionChanges && !hasEggChanges { + return nil + } + var category string if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 { category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category) } isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) - if req.Eggs != nil { + if hasEggChanges { if !isLaying && len(req.Eggs) > 0 { return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") } @@ -291,7 +304,29 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } } - if req.BodyWeights != nil { + if hasStockChanges { + if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil { + return err + } + } + + if hasDepletionChanges || hasEggChanges { + if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil { + return err + } + } + + hasExistingGradings := false + for _, egg := range recordingEntity.Eggs { + if len(egg.GradingEggs) > 0 { + hasExistingGradings = true + break + } + } + + hasEggsAfterUpdate := len(recordingEntity.Eggs) > 0 + + if hasBodyChanges { if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil { s.Log.Errorf("Failed to clear body weights: %+v", err) return err @@ -302,11 +337,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } } - if req.Stocks != nil { - if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil { - return err - } - + if hasStockChanges { existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id) if err != nil { s.Log.Errorf("Failed to list existing stocks: %+v", err) @@ -330,17 +361,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } } - if req.Eggs != nil && req.Depletions == nil { - if err := s.ensureProductWarehousesExist(c, nil, nil, req.Eggs); err != nil { - return err - } - } - - if req.Depletions != nil { - if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil { - return err - } - + if hasDepletionChanges { existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id) if err != nil { s.Log.Errorf("Failed to list existing depletions: %+v", err) @@ -364,7 +385,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } } - if req.Eggs != nil { + if hasEggChanges { existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id) if err != nil { s.Log.Errorf("Failed to list existing eggs: %+v", err) @@ -386,17 +407,71 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err) return err } + + hasExistingGradings = false + hasEggsAfterUpdate = len(req.Eggs) > 0 } - if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil { - s.Log.Errorf("Failed to recompute recording metrics: %+v", err) - return err + if hasBodyChanges || hasStockChanges || hasDepletionChanges { + if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil { + s.Log.Errorf("Failed to recompute recording metrics: %+v", err) + return err + } } action := entity.ApprovalActionUpdated - if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, utils.RecordingStepPengajuan, action, recordingEntity.CreatedBy, nil); err != nil { - s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err) - return err + actorID := recordingEntity.CreatedBy + if actorID == 0 { + actorID = 1 + } + + var step approvalutils.ApprovalStep + if isLaying { + if !hasEggsAfterUpdate { + step = utils.RecordingStepGradingTelur + } else if hasEggChanges { + step = utils.RecordingStepGradingTelur + } else if hasExistingGradings { + step = utils.RecordingStepPengajuan + } else { + step = utils.RecordingStepGradingTelur + } + } else { + step = utils.RecordingStepPengajuan + } + + latestApproval := recordingEntity.LatestApproval + if latestApproval == nil { + if s.ApprovalSvc != nil { + if fetched, fetchErr := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, recordingEntity.Id, nil); fetchErr != nil { + s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr) + return fetchErr + } else { + latestApproval = fetched + } + } else if s.ApprovalRepo != nil { + if fetched, fetchErr := s.ApprovalRepo.LatestByTarget(ctx, utils.ApprovalWorkflowRecording.String(), recordingEntity.Id, nil); fetchErr != nil { + s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr) + return fetchErr + } else { + latestApproval = fetched + } + } + } + + shouldCreateApproval := true + if latestApproval != nil && + latestApproval.StepNumber == uint16(step) && + latestApproval.Action != nil && + *latestApproval.Action == action { + shouldCreateApproval = false + } + + if shouldCreateApproval { + if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, step, action, actorID, nil); err != nil { + s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err) + return err + } } return nil @@ -1015,13 +1090,21 @@ func (s *recordingService) ensureChickInExists(ctx context.Context, projectFlock return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid") } - _, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) - if err == nil { - return nil + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) + if err != nil { + s.Log.Errorf("Failed to check project flock population for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa data chick in") } - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusBadRequest, "Project flock belum melakukan chick in sehingga belum dapat membuat recording") + + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Project flock belum memiliki chick in yang disetujui sehingga belum dapat membuat recording") } - s.Log.Errorf("Failed to check project flock population for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err) - return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa data chick in") + + for _, population := range populations { + if population.TotalQty > 0 { + return nil + } + } + + return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording") } diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go index 62e5f8df..28ea8a9f 100644 --- a/internal/modules/production/recordings/validations/recording.validation.go +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -2,9 +2,9 @@ package validation type ( BodyWeight struct { - AvgWeight float64 `json:"avg_weight" validate:"required"` - Qty float64 `json:"qty" validate:"required,gt=0"` - TotalWeight float64 `json:"total_weight" validate:"required,gte=0"` + AvgWeight float64 `json:"avg_weight" validate:"required"` + Qty float64 `json:"qty" validate:"required,gt=0"` + TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gte=0"` } Stock struct { diff --git a/internal/modules/purchases/controllers/purchase.controller.go b/internal/modules/purchases/controllers/purchase.controller.go new file mode 100644 index 00000000..ffef2f5d --- /dev/null +++ b/internal/modules/purchases/controllers/purchase.controller.go @@ -0,0 +1,67 @@ +package controller + +import ( + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type PurchaseController struct { + service service.PurchaseService +} + +func NewPurchaseController(s service.PurchaseService) *PurchaseController { + return &PurchaseController{service: s} +} + +func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error { + req := new(validation.CreatePurchaseRequest) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := ctrl.service.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Purchase requisition created successfully", + Data: dto.ToPurchaseDetailDTO(*result), + }) +} + +func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error { + param := c.Params("id") + id, err := strconv.ParseUint(param, 10, 64) + if err != nil || id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase requisition id") + } + + req := new(validation.ApproveStaffPurchaseRequest) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := ctrl.service.ApproveStaffPurchase(c, id, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Staff purchase approval recorded successfully", + Data: dto.ToPurchaseDetailDTO(*result), + }) +} diff --git a/internal/modules/purchases/dto/purchase.dto.go b/internal/modules/purchases/dto/purchase.dto.go new file mode 100644 index 00000000..381115a6 --- /dev/null +++ b/internal/modules/purchases/dto/purchase.dto.go @@ -0,0 +1,155 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +type SupplierBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Alias string `json:"alias"` + Type string `json:"type"` + Category string `json:"category"` +} + +type AreaBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type LocationBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type WarehouseBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Area *AreaBaseDTO `json:"area,omitempty"` + Location *LocationBaseDTO `json:"location,omitempty"` +} + +type ProductBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + SKU *string `json:"sku,omitempty"` +} + +type PurchaseItemDTO struct { + Id uint64 `json:"id"` + Product *ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` + ProductWarehouseID *uint64 `json:"product_warehouse_id,omitempty"` + SubQty float64 `json:"sub_qty"` + TotalQty float64 `json:"total_qty"` + TotalUsed float64 `json:"total_used"` + Price float64 `json:"price"` + TotalPrice float64 `json:"total_price"` +} + +type PurchaseDetailDTO struct { + Id uint64 `json:"id"` + PrNumber string `json:"pr_number"` + Supplier *SupplierBaseDTO `json:"supplier,omitempty"` + CreditTerm *int `json:"credit_term,omitempty"` + DueDate *time.Time `json:"due_date,omitempty"` + GrandTotal float64 `json:"grand_total"` + Notes *string `json:"notes,omitempty"` + Items []PurchaseItemDTO `json:"items"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func toSupplierBaseDTO(s entity.Supplier) *SupplierBaseDTO { + if s.Id == 0 { + return nil + } + return &SupplierBaseDTO{ + Id: s.Id, + Name: s.Name, + Alias: s.Alias, + Type: s.Type, + Category: s.Category, + } +} + +func toWarehouseBaseDTO(w *entity.Warehouse) *WarehouseBaseDTO { + if w == nil || w.Id == 0 { + return nil + } + dto := &WarehouseBaseDTO{ + Id: w.Id, + Name: w.Name, + } + if w.Area.Id != 0 { + dto.Area = &AreaBaseDTO{ + Id: w.Area.Id, + Name: w.Area.Name, + } + } + if w.Location != nil && w.Location.Id != 0 { + dto.Location = &LocationBaseDTO{ + Id: w.Location.Id, + Name: w.Location.Name, + } + } + return dto +} + +func toProductBaseDTO(p *entity.Product) *ProductBaseDTO { + if p == nil || p.Id == 0 { + return nil + } + dto := &ProductBaseDTO{ + Id: p.Id, + Name: p.Name, + } + if p.Sku != nil { + dto.SKU = p.Sku + } + return dto +} + +func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO { + dto := PurchaseItemDTO{ + Id: item.Id, + ProductWarehouseID: item.ProductWarehouseId, + SubQty: item.SubQty, + TotalQty: item.TotalQty, + TotalUsed: item.TotalUsed, + Price: item.Price, + TotalPrice: item.TotalPrice, + } + if item.Product != nil { + dto.Product = toProductBaseDTO(item.Product) + } + if item.Warehouse != nil { + dto.Warehouse = toWarehouseBaseDTO(item.Warehouse) + } + return dto +} + +func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO { + result := make([]PurchaseItemDTO, len(items)) + for i, item := range items { + result[i] = ToPurchaseItemDTO(item) + } + return result +} + +func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO { + return PurchaseDetailDTO{ + Id: p.Id, + PrNumber: p.PrNumber, + Supplier: toSupplierBaseDTO(p.Supplier), + CreditTerm: p.CreditTerm, + DueDate: p.DueDate, + GrandTotal: p.GrandTotal, + Notes: p.Notes, + Items: ToPurchaseItemDTOs(p.Items), + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + } +} diff --git a/internal/modules/purchases/module.go b/internal/modules/purchases/module.go new file mode 100644 index 00000000..1397f27e --- /dev/null +++ b/internal/modules/purchases/module.go @@ -0,0 +1,13 @@ +package purchases + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +type PurchaseModule struct{} + +func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + RegisterRoutes(router, db, validate) +} diff --git a/internal/modules/purchases/repositories/purchase.repository.go b/internal/modules/purchases/repositories/purchase.repository.go new file mode 100644 index 00000000..398fcea1 --- /dev/null +++ b/internal/modules/purchases/repositories/purchase.repository.go @@ -0,0 +1,113 @@ +package repositories + +import ( + "context" + "errors" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type PurchaseRepository interface { + repository.BaseRepository[entity.Purchase] + CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error + GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) + UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error +} + +type PurchaseRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.Purchase] +} + +func NewPurchaseRepository(db *gorm.DB) PurchaseRepository { + return &PurchaseRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db), + } +} + +func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error { + db := r.DB().WithContext(ctx) + + if err := db.Create(purchase).Error; err != nil { + return err + } + + if len(items) == 0 { + return nil + } + + for _, item := range items { + item.PurchaseId = purchase.Id + } + + if err := db.Create(&items).Error; err != nil { + return err + } + return nil +} + +func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) { + var purchase entity.Purchase + err := r.DB().WithContext(ctx). + Preload("Supplier"). + Preload("Items", func(db *gorm.DB) *gorm.DB { + return db.Order("id ASC") + }). + Preload("Items.Product"). + Preload("Items.Warehouse"). + Preload("Items.Warehouse.Area"). + Preload("Items.Warehouse.Location"). + Preload("Items.ProductWarehouse"). + First(&purchase, id).Error + if err != nil { + return nil, err + } + return &purchase, nil +} + +type PurchasePricingUpdate struct { + ItemID uint64 + Price float64 + TotalPrice float64 +} + +func (r *PurchaseRepositoryImpl) UpdatePricing( + ctx context.Context, + purchaseID uint64, + updates []PurchasePricingUpdate, + grandTotal float64, +) error { + if len(updates) == 0 { + return errors.New("pricing updates cannot be empty") + } + + db := r.DB().WithContext(ctx) + + for _, upd := range updates { + result := db.Model(&entity.PurchaseItem{}). + Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID). + Updates(map[string]interface{}{ + "price": upd.Price, + "total_price": upd.TotalPrice, + "updated_at": gorm.Expr("NOW()"), + }) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + } + + if err := db.Model(&entity.Purchase{}). + Where("id = ?", purchaseID). + Updates(map[string]interface{}{ + "grand_total": grandTotal, + "updated_at": gorm.Expr("NOW()"), + }).Error; err != nil { + return err + } + + return nil +} diff --git a/internal/modules/purchases/route.go b/internal/modules/purchases/route.go new file mode 100644 index 00000000..df3ea1a1 --- /dev/null +++ b/internal/modules/purchases/route.go @@ -0,0 +1,59 @@ +package purchases + +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" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers" + rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + utils "gitlab.com/mbugroup/lti-api.git/internal/utils" + "gorm.io/gorm" +) + +func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + group := router.Group("/purchases") + + purchaseRepo := rPurchase.NewPurchaseRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) + supplierRepo := rSupplier.NewSupplierRepository(db) + userRepo := rUser.NewUserRepository(db) + + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err)) + } + + purchaseService := service.NewPurchaseService( + validate, + purchaseRepo, + productWarehouseRepo, + warehouseRepo, + supplierRepo, + approvalRepo, + ) + userService := sUser.NewUserService(userRepo, validate) + + PurchaseRoutes(group, userService, purchaseService) +} + +func PurchaseRoutes(v1 fiber.Router, u sUser.UserService, s service.PurchaseService) { + ctrl := controller.NewPurchaseController(s) + + route := v1.Group("/requisitions") + + // route.Post("/", m.Auth(u), ctrl.CreateOne) + + route.Post("/", ctrl.CreateOne) + route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase) +} diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go new file mode 100644 index 00000000..a7419fc5 --- /dev/null +++ b/internal/modules/purchases/services/purchase.service.go @@ -0,0 +1,337 @@ +package service + +import ( + "errors" + "fmt" + "time" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type PurchaseService interface { + CreateOne(ctx *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) + ApproveStaffPurchase(ctx *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error) +} + +type purchaseService struct { + Log *logrus.Logger + Validate *validator.Validate + PurchaseRepo rPurchase.PurchaseRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + WarehouseRepo rWarehouse.WarehouseRepository + SupplierRepo rSupplier.SupplierRepository + ApprovalRepo commonRepo.ApprovalRepository +} + +func NewPurchaseService( + validate *validator.Validate, + purchaseRepo rPurchase.PurchaseRepository, + productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, + warehouseRepo rWarehouse.WarehouseRepository, + supplierRepo rSupplier.SupplierRepository, + approvalRepo commonRepo.ApprovalRepository, +) PurchaseService { + return &purchaseService{ + Log: utils.Log, + Validate: validate, + PurchaseRepo: purchaseRepo, + ProductWarehouseRepo: productWarehouseRepo, + WarehouseRepo: warehouseRepo, + SupplierRepo: supplierRepo, + ApprovalRepo: approvalRepo, + } +} + +func uint64Ptr(v uint64) *uint64 { + return &v +} + +func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + supplier, err := s.SupplierRepo.GetByID(c.Context(), req.SupplierID, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found") + } + s.Log.Errorf("Failed to get supplier: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier") + } + + warehouse, err := s.WarehouseRepo.GetDetailByID(c.Context(), req.WarehouseID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") + } + s.Log.Errorf("Failed to get warehouse: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") + } + + if warehouse.AreaId != req.AreaID { + return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided area") + } + if warehouse.LocationId == nil || *warehouse.LocationId != req.LocationID { + return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided location") + } + + type aggregatedItem struct { + productId uint64 + warehouseId uint64 + productWarehouseId *uint64 + subQty float64 + } + + if len(req.Items) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty") + } + + aggregated := make([]*aggregatedItem, 0, len(req.Items)) + indexMap := make(map[string]int) + + for _, item := range req.Items { + var ( + productId = uint64(item.ProductID) + warehouseId = uint64(req.WarehouseID) + productWarehouseId *uint64 + ) + + if item.ProductWarehouseID != nil { + productWarehouse, err := s.ProductWarehouseRepo.GetDetailByID(c.Context(), *item.ProductWarehouseID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", *item.ProductWarehouseID)) + } + s.Log.Errorf("Failed to get product warehouse %d: %+v", *item.ProductWarehouseID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") + } + + if productWarehouse.WarehouseId != req.WarehouseID { + return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse does not match selected warehouse") + } + + productId = uint64(productWarehouse.ProductId) + warehouseId = uint64(productWarehouse.WarehouseId) + idCopy := uint64(productWarehouse.Id) + productWarehouseId = &idCopy + } else { + productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), item.ProductID, req.WarehouseID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to get product warehouse for product %d and warehouse %d: %+v", item.ProductID, req.WarehouseID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") + } + if err == nil { + idCopy := uint64(productWarehouse.Id) + productWarehouseId = &idCopy + } + } + + key := fmt.Sprintf("%d:%d", productId, warehouseId) + if idx, ok := indexMap[key]; ok { + aggregated[idx].subQty += item.Quantity + continue + } + + entry := &aggregatedItem{ + productId: productId, + warehouseId: warehouseId, + productWarehouseId: productWarehouseId, + subQty: item.Quantity, + } + aggregated = append(aggregated, entry) + indexMap[key] = len(aggregated) - 1 + } + + prNumber := fmt.Sprintf("PR-%s-%s", time.Now().Format("20060102"), uuid.NewString()[:8]) + + var creditTerm *int + var dueDate *time.Time + + if supplier.DueDate > 0 { + ct := supplier.DueDate + creditTerm = &ct + d := time.Now().UTC().AddDate(0, 0, ct) + dueDate = &d + } + + purchase := &entity.Purchase{ + PrNumber: prNumber, + SupplierId: uint64(req.SupplierID), + CreditTerm: creditTerm, + DueDate: dueDate, + GrandTotal: 0, + Notes: req.Notes, + CreatedBy: 1, // TODO: replace with authenticated user id once available + } + + items := make([]*entity.PurchaseItem, 0, len(aggregated)) + for _, item := range aggregated { + items = append(items, &entity.PurchaseItem{ + ProductId: item.productId, + WarehouseId: item.warehouseId, + ProductWarehouseId: item.productWarehouseId, + SubQty: item.subQty, + TotalQty: item.subQty, + TotalUsed: 0, + Price: 0, + TotalPrice: 0, + }) + } + + ctx := c.Context() + transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + purchaseRepoTx := rPurchase.NewPurchaseRepository(tx) + if err := purchaseRepoTx.CreateWithItems(ctx, purchase, items); err != nil { + return err + } + + actorID := uint(purchase.CreatedBy) + if actorID == 0 { + actorID = 1 + } + action := entity.ApprovalActionCreated + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + if _, err := approvalSvc.CreateApproval( + ctx, + utils.ApprovalWorkflowPurchase, + uint(purchase.Id), + utils.PurchaseStepPengajuan, + &action, + actorID, + nil, + ); err != nil { + return err + } + + return nil + }) + if transactionErr != nil { + s.Log.Errorf("Failed to create purchase requisition: %+v", transactionErr) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create purchase requisition") + } + + created, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id) + if err != nil { + s.Log.Errorf("Failed to load created purchase requisition: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition") + } + + return created, nil +} + +func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + ctx := c.Context() + + purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Purchase requisition not found") + } + s.Log.Errorf("Failed to get purchase requisition: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase requisition") + } + + if len(purchase.Items) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase requisition has no items to approve") + } + + requestItems := make(map[uint64]validation.StaffPurchaseApprovalItem, len(req.Items)) + for _, item := range req.Items { + requestItems[item.PurchaseItemID] = item + } + + updates := make([]rPurchase.PurchasePricingUpdate, 0, len(purchase.Items)) + var grandTotal float64 + + for _, item := range purchase.Items { + data, ok := requestItems[item.Id] + if !ok { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id)) + } + delete(requestItems, item.Id) + + if data.Price <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Price for item %d must be greater than 0", item.Id)) + } + + totalPrice := data.TotalPrice + if totalPrice == nil { + calculated := data.Price * item.TotalQty + totalPrice = &calculated + } + if *totalPrice <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for item %d must be greater than 0", item.Id)) + } + + updates = append(updates, rPurchase.PurchasePricingUpdate{ + ItemID: item.Id, + Price: data.Price, + TotalPrice: *totalPrice, + }) + grandTotal += *totalPrice + } + + if len(requestItems) > 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase requisition") + } + + action := entity.ApprovalActionApproved + actorID := uint(1) // TODO: replace with authenticated user id once available + + transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + purchaseRepoTx := rPurchase.NewPurchaseRepository(tx) + if err := purchaseRepoTx.UpdatePricing(ctx, purchase.Id, updates, grandTotal); err != nil { + return err + } + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + if _, err := approvalSvc.CreateApproval( + ctx, + utils.ApprovalWorkflowPurchase, + uint(purchase.Id), + utils.PurchaseStepStaffPurchase, + &action, + actorID, + req.Notes, + ); err != nil { + return err + } + + return nil + }) + if transactionErr != nil { + if errors.Is(transactionErr, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found") + } + s.Log.Errorf("Failed to approve purchase requisition %d: %+v", purchase.Id, transactionErr) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to approve purchase requisition") + } + + updated, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id) + if err != nil { + s.Log.Errorf("Failed to load purchase requisition after approval: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition") + } + + return updated, nil +} diff --git a/internal/modules/purchases/validations/purchase.validation.go b/internal/modules/purchases/validations/purchase.validation.go new file mode 100644 index 00000000..8791fc1c --- /dev/null +++ b/internal/modules/purchases/validations/purchase.validation.go @@ -0,0 +1,27 @@ +package validation + +type PurchaseItemPayload struct { + ProductID uint `json:"product_id" validate:"required"` + ProductWarehouseID *uint `json:"product_warehouse_id,omitempty" validate:"omitempty,gt=0"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` +} + +type CreatePurchaseRequest struct { + SupplierID uint `json:"supplier_id" validate:"required"` + AreaID uint `json:"area_id" validate:"required"` + LocationID uint `json:"location_id" validate:"required"` + WarehouseID uint `json:"warehouse_id" validate:"required"` + Notes *string `json:"notes" validate:"omitempty,max=500"` + Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"` +} + +type StaffPurchaseApprovalItem struct { + PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"` + Price float64 `json:"price" validate:"required,gt=0"` + TotalPrice *float64 `json:"total_price,omitempty" validate:"omitempty,gt=0"` +} + +type ApproveStaffPurchaseRequest struct { + Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/route/route.go b/internal/route/route.go index 60f0fe26..d6277549 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -8,12 +8,13 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals" constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants" inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory" master "gitlab.com/mbugroup/lti-api.git/internal/modules/master" + production "gitlab.com/mbugroup/lti-api.git/internal/modules/production" + purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases" 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,8 +29,9 @@ func Routes(app *fiber.App, db *gorm.DB) { master.MasterModule{}, constants.ConstantModule{}, inventory.InventoryModule{}, - production.ProductionModule{}, - approvals.ApprovalModule{}, + production.ProductionModule{}, + approvals.ApprovalModule{}, + purchases.PurchaseModule{}, // MODULE REGISTRY } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index ec98bce4..099b6510 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -205,8 +205,23 @@ const ( var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{ RecordingStepGradingTelur: "Grading-Telur", - RecordingStepPengajuan: "Pengajuan", - RecordingStepDisetujui: "Disetujui", + RecordingStepPengajuan: "Pengajuan", + RecordingStepDisetujui: "Disetujui", +} + +// ------------------------------------------------------------------- +// Purchase Approval +// ------------------------------------------------------------------- + +const ( + ApprovalWorkflowPurchase approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PURCHASES") + PurchaseStepPengajuan approvalutils.ApprovalStep = 1 + PurchaseStepStaffPurchase approvalutils.ApprovalStep = 2 +) + +var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{ + PurchaseStepPengajuan: "Pengajuan", + PurchaseStepStaffPurchase: "Staff Purchase", } // ------------------------------------------------------------------- diff --git a/internal/utils/recording/util.recording.go b/internal/utils/recording/util.recording.go index e9ae371c..8f0fe81f 100644 --- a/internal/utils/recording/util.recording.go +++ b/internal/utils/recording/util.recording.go @@ -12,7 +12,10 @@ func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.Re result := make([]entity.RecordingBW, 0, len(items)) for _, item := range items { - totalWeight := item.TotalWeight + var totalWeight float64 + if item.TotalWeight != nil { + totalWeight = *item.TotalWeight + } if totalWeight <= 0 { totalWeight = item.AvgWeight * item.Qty } From 1c99093ff8f7ecd29a501e449c64933ffd3d736f Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 10:31:35 +0700 Subject: [PATCH 15/59] feat[BE-127]; creating correct logic update and delete transfer laying --- .../services/transfer_laying.service.go | 172 ++++++++++++++++-- .../validations/transfer_laying.validation.go | 8 +- 2 files changed, 161 insertions(+), 19 deletions(-) diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 92965de0..06c297bc 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -361,7 +361,9 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return nil, err } - _, err := s.Repository.GetByID(c.Context(), id, nil) + existingTransfer, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return db.Preload("Sources.ProductWarehouse").Preload("Targets") + }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") @@ -382,26 +384,140 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i } } - updateBody := make(map[string]any) - - if req.TransferDate != nil { - updateBody["transfer_date"] = *req.TransferDate + if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Source project flock not found") } - if req.Reason != nil { - updateBody["notes"] = *req.Reason + if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Target project flock not found") } - if len(updateBody) == 0 { - return s.GetOne(c, id) + transferDate, err := time.Parse("2006-01-02", req.TransferDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") } - if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) + + for _, oldSource := range existingTransfer.Sources { + if oldSource.ProductWarehouseId != nil && oldSource.Qty > 0 { + + if err := productWarehouseRepoTx.PatchOne(c.Context(), *oldSource.ProductWarehouseId, map[string]any{ + "quantity": gorm.Expr("quantity + ?", oldSource.Qty), + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore warehouse quantity") + } + + if err := s.restoreProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, oldSource.SourceProjectFlockKandangId, oldSource.Qty); err != nil { + return err + } + } } - s.Log.Errorf("Failed to update transferLaying: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying") + + for _, oldSource := range existingTransfer.Sources { + if err := dbTransaction.Delete(&oldSource).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old source") + } + } + + for _, oldTarget := range existingTransfer.Targets { + if err := dbTransaction.Delete(&oldTarget).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old target") + } + } + + totalSourceQty := 0.0 + for _, source := range req.SourceKandangs { + totalSourceQty += source.Quantity + } + + if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), id, map[string]any{ + "transfer_date": transferDate, + "notes": req.Reason, + "pending_usage_qty": &totalSourceQty, + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer header") + } + + sourceWarehouseMap := make(map[uint]uint) + for _, sourceDetail := range req.SourceKandangs { + + populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations") + } + + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available", sourceDetail.ProjectFlockKandangId)) + } + + var totalPopulation float64 + var productWarehouseId uint + + for _, pop := range populations { + totalPopulation += pop.TotalQty + if pop.ProductWarehouseId > 0 { + productWarehouseId = pop.ProductWarehouseId + } + } + + if totalPopulation < sourceDetail.Quantity { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity)) + } + + sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId + + source := entity.LayingTransferSource{ + LayingTransferId: id, + SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, + Qty: sourceDetail.Quantity, + ProductWarehouseId: &productWarehouseId, + } + if err := dbTransaction.Create(&source).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source") + } + + if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil { + return err + } + + if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("quantity - ?", sourceDetail.Quantity)}, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity") + } + } + + for _, targetDetail := range req.TargetKandangs { + targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + } + + targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") + } + + target := entity.LayingTransferTarget{ + LayingTransferId: id, + TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId, + Qty: targetDetail.Quantity, + ProductWarehouseId: &targetWarehouse.Id, + } + if err := dbTransaction.Create(&target).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target") + } + } + + return nil + }) + + if err != nil { + return nil, err } return s.GetOne(c, id) @@ -464,9 +580,8 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- { pop := populations[i] restoreAmount := remainingToRestore - if remainingToRestore < pop.TotalQty { - - restoreAmount = remainingToRestore + if pop.TotalQty < remainingToRestore { + restoreAmount = pop.TotalQty } newQty := pop.TotalQty + restoreAmount @@ -725,3 +840,26 @@ func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context return nil } + +func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToRestore float64) error { + populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) + if err != nil { + return err + } + + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration") + } + + // Restore in LIFO order (from newest to oldest) + // Add all quantity back to the last (newest) population + if len(populations) > 0 { + lastPop := populations[len(populations)-1] + newQty := lastPop.TotalQty + quantityToRestore + if err := populationRepo.PatchOne(ctx, lastPop.Id, map[string]any{"total_qty": newQty}, nil); err != nil { + return err + } + } + + return nil +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index e35ba4f3..bd117bd9 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -20,8 +20,12 @@ type Create struct { } type Update struct { - TransferDate *string `json:"transfer_date,omitempty" validate:"omitempty,datetime=2006-01-02"` - Reason *string `json:"reason,omitempty" validate:"omitempty,max=1000"` + TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"` + SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"` + TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"` + SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,dive,required"` + TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"` + Reason string `json:"reason" validate:"omitempty,max=1000"` } type Query struct { From 60fe553f636e8e0ce73cd1a14f8062422e719399 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 10:51:32 +0700 Subject: [PATCH 16/59] FIX[BE]: getting available qty from Flags instead of Product.Category --- .../repositories/product_warehouse.repository.go | 4 ++-- .../modules/production/chickins/services/chickin.service.go | 2 +- .../services/project_flock_kandang.service.go | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index d95c298b..fbda4718 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -165,7 +165,7 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con var productWarehouses []entity.ProductWarehouse err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}). Joins("JOIN products ON products.id = product_warehouses.product_id"). - Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products"). + Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'"). Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId). Order("product_warehouses.created_at DESC"). Preload("Product").Preload("Warehouse"). @@ -179,7 +179,7 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) { var product entity.Product err := r.DB().WithContext(ctx). - Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products"). + Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'"). Where("flags.name = ?", flagName). First(&product).Error if err != nil { diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index d78a9a00..974b5c9f 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -136,7 +136,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category)) } - productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) + productWarehouses, err = s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) if err != nil || len(productWarehouses) == 0 { return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category))) } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index ec292d90..52b2570a 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -105,11 +105,10 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project } else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) { productCategoryCode = "PULLET" } else { - return nil, nil } - products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) + products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) if err != nil || len(products) == 0 { return nil, nil } @@ -121,7 +120,6 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) } - // Only include product warehouse if available_qty > 0 if availableQty <= 0 { continue } From 028d5f6f913f109cc53a79d7a629d75a99dce5e9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 11:13:56 +0700 Subject: [PATCH 17/59] FEAT[BE-127]: add flockName to getone projectflockkandang API --- .../project-flock-kandangs/dto/project_flock_kandang.dto.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 92db5b23..163de6a3 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -24,6 +24,7 @@ type ProjectFlockKandangBaseDTO struct { type ProjectFlockDTO struct { Id uint `json:"id"` + Name string `json:"name,omitempty"` Period int `json:"period"` Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` @@ -82,6 +83,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO return &ProjectFlockDTO{ Id: pf.Id, + Name: pf.FlockName, Period: pf.Period, Area: pf.Area, Category: pf.Category, From 5cfa97dd031561632f6317b253bb7e1fab556e83 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 13:41:16 +0700 Subject: [PATCH 18/59] Feat[BE]: adding project flock kandang id into project flock get one projecct flock api --- .../project_flocks/dto/projectflock.dto.go | 47 +++++++++++-------- .../repositories/projectflock.repository.go | 4 +- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index 977aeb40..ce9be3d3 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -23,25 +23,23 @@ type ProjectFlockBaseDTO struct { FlockName string `json:"flock_name"` } -type KandangWithProjectFlockIdDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` -} - type ProjectFlockListDTO struct { ProjectFlockBaseDTO // 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"` + Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` + Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` + Kandangs []KandangWithProjectFlockIdDTO `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 KandangWithProjectFlockIdDTO struct { + kandangDTO.KandangBaseDTO + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` } type ProjectFlockDetailDTO struct { @@ -60,11 +58,22 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { createdUser = &mapped } - var kandangSummaries []kandangDTO.KandangBaseDTO + var kandangSummaries []KandangWithProjectFlockIdDTO if len(e.Kandangs) > 0 { - kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs)) + kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs)) for i, kandang := range e.Kandangs { - kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang) + // Find project_flock_kandang_id dari KandangHistory + var pfkId uint + for _, kh := range e.KandangHistory { + if kh.KandangId == kandang.Id { + pfkId = kh.Id + break + } + } + kandangSummaries[i] = KandangWithProjectFlockIdDTO{ + KandangBaseDTO: kandangDTO.ToKandangBaseDTO(kandang), + ProjectFlockKandangId: pfkId, + } } } diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index bb653fe9..a8fab919 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -117,7 +117,9 @@ func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB Preload("Area"). Preload("Fcr"). Preload("Location"). - Preload("Kandangs") + Preload("Kandangs"). + Preload("KandangHistory"). + Preload("KandangHistory.Kandang") } func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB { From 4b69afe4facf29ac2bd8eace1b97948b44979676 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 14:03:47 +0700 Subject: [PATCH 19/59] Feat[BE-127] create get all with param on project flock kandang --- .../project_flock_kandang.controller.go | 9 +- .../dto/project_flock_kandang.dto.go | 6 ++ .../services/project_flock_kandang.service.go | 92 +++++++++++++++++-- .../project_flock_kandang.validation.go | 12 ++- .../projectflock_kandang.repository.go | 39 ++++---- 5 files changed, 129 insertions(+), 29 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 92743035..8e4c23a5 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -33,11 +33,16 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } - result, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) + results, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) if err != nil { return err } + var data []dto.ProjectFlockKandangListDTO + for _, result := range results { + data = append(data, dto.ToProjectFlockKandangListDTO(result.Entity)) + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangListDTO]{ Code: fiber.StatusOK, @@ -49,7 +54,7 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToProjectFlockKandangListDTOs(result), + Data: data, }) } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 163de6a3..de74723f 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -68,6 +68,12 @@ type ProjectFlockKandangDetailDTO struct { ProjectFlockKandangListDTO } +// Wrapper untuk GetAll dengan available quantities sudah included +type ProjectFlockKandangWithAvailableQtysDTO struct { + Entity entity.ProjectFlockKandang + AvailableQtys []map[string]interface{} +} + // === Mapper Functions (ordered) === func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO { diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 52b2570a..ee6687c0 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -7,6 +7,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + dto "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -18,7 +19,7 @@ import ( ) type ProjectFlockKandangService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) } @@ -44,19 +45,99 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository } } -func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) { +func (s projectFlockKandangService) withRelations(db *gorm.DB) *gorm.DB { + return db. + Preload("ProjectFlock"). + Preload("ProjectFlock.Fcr"). + Preload("ProjectFlock.Area"). + Preload("ProjectFlock.Location"). + Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.Kandangs"). + Preload("ProjectFlock.KandangHistory"). + Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse") + +} + +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } - projectFlockKandangs, err := s.Repository.GetAll(c.Context()) + if params.Page <= 0 { + params.Page = 1 + } + if params.Limit <= 0 { + params.Limit = 10 + } + + offset := (params.Page - 1) * params.Limit + + projectFlockKandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + + if params.Search != "" { + db = db.Where( + db.Where("LOWER(kandang.name) LIKE LOWER(?)", "%"+params.Search+"%"). + Or("LOWER(project_flock.flock_name) LIKE LOWER(?)", "%"+params.Search+"%"), + ) + } + + if params.ProjectFlockId > 0 { + db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockId) + } + + if params.KandangId > 0 { + db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangId) + } + + if params.Category != "" { + db = db.Where("project_flock.category = ?", params.Category) + } + if params.AreaId > 0 { + db = db.Where("project_flock.area_id = ?", params.AreaId) + } + + sortBy := "project_flock_id ASC, created_at ASC" + if params.SortBy != "" { + sortOrder := "ASC" + if params.SortOrder == "DESC" { + sortOrder = "DESC" + } + + switch params.SortBy { + case "created_at": + sortBy = "project_flock_kandangs.created_at " + sortOrder + case "period": + sortBy = "project_flock.period " + sortOrder + } + } + + return db.Order(sortBy) + }) + if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) return nil, 0, err } - total := int64(len(projectFlockKandangs)) - return projectFlockKandangs, total, nil + var results []dto.ProjectFlockKandangWithAvailableQtysDTO + for i := range projectFlockKandangs { + availableQtys, err := s.getAvailableQuantities(c, &projectFlockKandangs[i]) + if err != nil { + s.Log.Warnf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandangs[i].Kandang.Id, err) + availableQtys = nil + } + + results = append(results, dto.ProjectFlockKandangWithAvailableQtysDTO{ + Entity: projectFlockKandangs[i], + AvailableQtys: availableQtys, + }) + } + + return results, total, nil } func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) { @@ -65,7 +146,6 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } if err != nil { - s.Log.Errorf("Failed get projectFlockKandang by id: %+v", err) return nil, nil, err } diff --git a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go index 1e95fd2c..e635ff92 100644 --- a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go +++ b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go @@ -11,7 +11,13 @@ type Update struct { } type Query struct { - 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"` + 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"` + ProjectFlockId uint `query:"project_flock_id" validate:"omitempty"` + KandangId uint `query:"kandang_id" validate:"omitempty"` + Category string `query:"category" validate:"omitempty,oneof=Growing Laying"` + AreaId uint `query:"area_id" validate:"omitempty"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at period"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"` } 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 101d01ab..6c39464b 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -14,7 +14,7 @@ type ProjectFlockKandangRepository interface { GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID 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) + GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) @@ -50,25 +50,28 @@ func (r *projectFlockKandangRepositoryImpl) DeleteMany(ctx context.Context, proj Delete(&entity.ProjectFlockKandang{}).Error } -func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) { +func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) { var records []entity.ProjectFlockKandang - if err := r.db.WithContext(ctx). - Preload("ProjectFlock"). - Preload("ProjectFlock.Fcr"). - Preload("ProjectFlock.Area"). - Preload("ProjectFlock.Location"). - Preload("ProjectFlock.CreatedUser"). - Preload("ProjectFlock.Kandangs"). - Preload("ProjectFlock.KandangHistory"). - Preload("Kandang"). - Preload("Chickins"). - Preload("Chickins.CreatedUser"). - Preload("Chickins.ProductWarehouse"). - Order("project_flock_id ASC, created_at ASC"). - Find(&records).Error; err != nil { - return nil, err + var total int64 + + q := r.db.WithContext(ctx) + + // Apply modifier function + if modifier != nil { + q = modifier(q) } - return records, nil + + // Count total + if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + // Apply pagination and fetch + if err := q.Offset(offset).Limit(limit).Find(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil } func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { From a587584156da887bf81b4876449393cbaaaac063 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 14:04:20 +0700 Subject: [PATCH 20/59] Feat[BE]: change response flock name on project flock kandang --- .../project-flock-kandangs/dto/project_flock_kandang.dto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index de74723f..8e9d3672 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -24,7 +24,7 @@ type ProjectFlockKandangBaseDTO struct { type ProjectFlockDTO struct { Id uint `json:"id"` - Name string `json:"name,omitempty"` + Name string `json:"flock_name,omitempty"` Period int `json:"period"` Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` From 663d5129bb4532b2b9bb273e428c6e2acaf8456d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 17:57:10 +0700 Subject: [PATCH 21/59] Feat[BE-127] Creating project flock kandang get all with soma query param --- .../project_flock_kandang.controller.go | 15 ++- .../services/project_flock_kandang.service.go | 80 ++++------------ .../project_flock_kandang.validation.go | 1 + .../projectflock_kandang.repository.go | 96 ++++++++++++++++++- 4 files changed, 126 insertions(+), 66 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 8e4c23a5..383006bf 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -24,9 +24,16 @@ func NewProjectFlockKandangController(projectFlockKandangService service.Project func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + ProjectFlockId: uint(c.QueryInt("project_flock_id", 0)), + KandangId: uint(c.QueryInt("kandang_id", 0)), + Category: c.Query("category", ""), + AreaId: uint(c.QueryInt("area_id", 0)), + SortBy: c.Query("sort_by", ""), + SortOrder: c.Query("sort_order", ""), + StepName: c.Query("step_name", ""), } if query.Page < 1 || query.Limit < 1 { @@ -38,7 +45,7 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { return err } - var data []dto.ProjectFlockKandangListDTO + data := make([]dto.ProjectFlockKandangListDTO, 0) for _, result := range results { data = append(data, dto.ToProjectFlockKandangListDTO(result.Entity)) } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index ee6687c0..90e85882 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -45,22 +45,6 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository } } -func (s projectFlockKandangService) withRelations(db *gorm.DB) *gorm.DB { - return db. - Preload("ProjectFlock"). - Preload("ProjectFlock.Fcr"). - Preload("ProjectFlock.Area"). - Preload("ProjectFlock.Location"). - Preload("ProjectFlock.CreatedUser"). - Preload("ProjectFlock.Kandangs"). - Preload("ProjectFlock.KandangHistory"). - Preload("Kandang"). - Preload("Chickins"). - Preload("Chickins.CreatedUser"). - Preload("Chickins.ProductWarehouse") - -} - func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -75,55 +59,33 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer offset := (params.Page - 1) * params.Limit - projectFlockKandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) - - if params.Search != "" { - db = db.Where( - db.Where("LOWER(kandang.name) LIKE LOWER(?)", "%"+params.Search+"%"). - Or("LOWER(project_flock.flock_name) LIKE LOWER(?)", "%"+params.Search+"%"), - ) - } - - if params.ProjectFlockId > 0 { - db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockId) - } - - if params.KandangId > 0 { - db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangId) - } - - if params.Category != "" { - db = db.Where("project_flock.category = ?", params.Category) - } - if params.AreaId > 0 { - db = db.Where("project_flock.area_id = ?", params.AreaId) - } - - sortBy := "project_flock_id ASC, created_at ASC" - if params.SortBy != "" { - sortOrder := "ASC" - if params.SortOrder == "DESC" { - sortOrder = "DESC" - } - - switch params.SortBy { - case "created_at": - sortBy = "project_flock_kandangs.created_at " + sortOrder - case "period": - sortBy = "project_flock.period " + sortOrder - } - } - - return db.Order(sortBy) - }) + projectFlockKandangs, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) return nil, 0, err } - var results []dto.ProjectFlockKandangWithAvailableQtysDTO + if s.ApprovalSvc != nil { + projectFlockKandangIDs := make([]uint, len(projectFlockKandangs)) + for i, pfk := range projectFlockKandangs { + projectFlockKandangIDs[i] = pfk.Id + } + + approvalMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandangIDs, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Failed to fetch approvals for projectFlockKandangs: %+v", err) + } else { + for i := range projectFlockKandangs { + if approval, ok := approvalMap[projectFlockKandangs[i].Id]; ok { + projectFlockKandangs[i].LatestApproval = approval + } + } + } + } + results := make([]dto.ProjectFlockKandangWithAvailableQtysDTO, 0) for i := range projectFlockKandangs { availableQtys, err := s.getAvailableQuantities(c, &projectFlockKandangs[i]) if err != nil { diff --git a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go index e635ff92..93e0256a 100644 --- a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go +++ b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go @@ -20,4 +20,5 @@ type Query struct { AreaId uint `query:"area_id" validate:"omitempty"` SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at period"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"` + StepName string `query:"step_name" validate:"omitempty,max=50"` } 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 6c39464b..1a05acff 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -6,6 +6,7 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" "gorm.io/gorm" ) @@ -15,6 +16,7 @@ type ProjectFlockKandangRepository interface { CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) + GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) @@ -56,17 +58,14 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset i q := r.db.WithContext(ctx) - // Apply modifier function if modifier != nil { q = modifier(q) } - // Count total if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { return nil, 0, err } - // Apply pagination and fetch if err := q.Offset(offset).Limit(limit).Find(&records).Error; err != nil { return nil, 0, err } @@ -74,6 +73,97 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset i return records, total, nil } +func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) { + var records []entity.ProjectFlockKandang + var total int64 + + query, ok := params.(*validation.Query) + + q := r.db.WithContext(ctx). + Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). + Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). + Preload("ProjectFlock"). + Preload("ProjectFlock.Fcr"). + Preload("ProjectFlock.Area"). + Preload("ProjectFlock.Location"). + Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.Kandangs"). + Preload("ProjectFlock.KandangHistory"). + Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse") + + if ok && query != nil && query.StepName != "" { + q = q.Where(` + EXISTS ( + SELECT 1 FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + AND LOWER("approvals"."step_name") = LOWER(?) + AND "approvals"."id" IN ( + SELECT "id" FROM "approvals" + WHERE "approvable_id" = "project_flock_kandangs"."id" + AND "approvable_type" = ? + ORDER BY "action_at" DESC + LIMIT 1 + ) + ) + `, "PROJECT_FLOCK_KANDANGS", query.StepName, "PROJECT_FLOCK_KANDANGS") + } + + if ok && query != nil { + if query.Search != "" { + escapedSearch := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(query.Search) + q = q.Where( + r.db.Where("LOWER(\"kandangs\".\"name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"). + Or("LOWER(\"project_flocks\".\"flock_name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"), + ) + } + + if query.ProjectFlockId > 0 { + q = q.Where("\"project_flock_kandangs\".\"project_flock_id\" = ?", query.ProjectFlockId) + } + + if query.KandangId > 0 { + q = q.Where("\"project_flock_kandangs\".\"kandang_id\" = ?", query.KandangId) + } + + if query.Category != "" { + q = q.Where("\"project_flocks\".\"category\" = ?", query.Category) + } + + if query.AreaId > 0 { + q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId) + } + } + + if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + sortBy := "\"project_flock_kandangs\".\"created_at\" DESC" + if ok && query != nil && query.SortBy != "" { + sortOrder := "DESC" + if query.SortOrder == "ASC" { + sortOrder = "ASC" + } + + switch query.SortBy { + case "created_at": + sortBy = "\"project_flock_kandangs\".\"created_at\" " + sortOrder + case "period": + sortBy = "\"project_flocks\".\"period\" " + sortOrder + } + } + + if err := q.Order(sortBy).Offset(offset).Limit(limit).Find(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil +} + func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { return &projectFlockKandangRepositoryImpl{db: tx} } From 954cccd564231f4531454fa1e3e59577aae5f162 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 21:25:15 +0700 Subject: [PATCH 22/59] Fix[BE]: make projectflock kandang API and dto clean --- .../production/chickins/dto/chickin.dto.go | 52 ++++- .../project_flock_kandang.controller.go | 4 +- .../dto/project_flock_kandang.dto.go | 200 +++++------------- .../services/project_flock_kandang.service.go | 69 ++---- .../projectflock_kandang.repository.go | 4 + 5 files changed, 115 insertions(+), 214 deletions(-) diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 7271c310..ad6d10f8 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -9,20 +9,29 @@ import ( 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" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs (ordered) === +type ProductWarehouseDTO struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` +} + type ChickinBaseDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ChickInDate time.Time `json:"chick_in_date"` - ProductWarehouseId uint `json:"product_warehouse_id"` - UsageQty float64 `json:"usage_qty"` - PendingUsageQty float64 `json:"pending_usage_qty"` - Notes string `json:"notes"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + ProductWarehouseId uint `json:"product_warehouse_id"` + ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` } type ProjectFlockDTO struct { @@ -159,11 +168,18 @@ func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { // If relation is not loaded but ID is available, use the ID projectFlockKandangId = e.ProjectFlockKandangId } + + var productWarehouse *ProductWarehouseDTO + if e.ProductWarehouse != nil && e.ProductWarehouse.Id != 0 { + productWarehouse = toProductWarehouseDTO(e.ProductWarehouse) + } + return ChickinBaseDTO{ Id: e.Id, ProjectFlockKandangId: projectFlockKandangId, ChickInDate: e.ChickInDate, ProductWarehouseId: e.ProductWarehouseId, + ProductWarehouse: productWarehouse, UsageQty: e.UsageQty, PendingUsageQty: e.PendingUsageQty, Notes: e.Notes, @@ -242,3 +258,25 @@ func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO { } return result } + +// === Helper Functions === + +// ToProductWarehouseDTO adalah exported helper untuk mapping ProductWarehouse (shared logic) +func ToProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO { + if pw == nil { + return nil + } + + product := productDTO.ToProductBaseDTO(pw.Product) + warehouse := warehouseDTO.ToWarehouseBaseDTO(pw.Warehouse) + + return &ProductWarehouseDTO{ + Id: pw.Id, + Product: &product, + Warehouse: &warehouse, + } +} + +func toProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO { + return ToProductWarehouseDTO(pw) +} diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 383006bf..21701826 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -47,7 +47,7 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { data := make([]dto.ProjectFlockKandangListDTO, 0) for _, result := range results { - data = append(data, dto.ToProjectFlockKandangListDTO(result.Entity)) + data = append(data, dto.ToProjectFlockKandangListDTO(result)) } return c.Status(fiber.StatusOK). @@ -83,6 +83,6 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get projectFlockKandang successfully", - Data: dto.ToProjectFlockKandangListDTOWithAvailableQty(*result, availableQtys), + Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys), }) } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 8e9d3672..e80e3220 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -19,7 +19,9 @@ import ( // === DTO Structs (ordered) === type ProjectFlockKandangBaseDTO struct { - Id uint `json:"id"` + Id uint `json:"id"` + KandangId uint `json:"kandang_id"` + ProjectFlockId uint `json:"project_flock_id"` } type ProjectFlockDTO struct { @@ -55,23 +57,17 @@ type AvailableQtyDTO struct { type ProjectFlockKandangListDTO struct { ProjectFlockKandangBaseDTO - ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` - Kandang *KandangDTO `json:"kandang,omitempty"` - Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"` - AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` + ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` + Kandang *KandangDTO `json:"kandang,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } type ProjectFlockKandangDetailDTO struct { ProjectFlockKandangListDTO -} - -// Wrapper untuk GetAll dengan available quantities sudah included -type ProjectFlockKandangWithAvailableQtysDTO struct { - Entity entity.ProjectFlockKandang - AvailableQtys []map[string]interface{} + Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"` + AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` } // === Mapper Functions (ordered) === @@ -101,23 +97,27 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO } } -func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO { +func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64) ProjectFlockKandangDetailDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) projectFlockSummary = &mapped } - return ProjectFlockKandangListDTO{ + listDTO := ProjectFlockKandangListDTO{ ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), ProjectFlock: toProjectFlockDTO(projectFlockSummary), Kandang: toKandangDTO(e.Kandang), - Chickins: toChickinDTOs(e.Chickins), - AvailableQtys: toAvailableQtyDTOsFromRaw(availableQtysRaw), CreatedAt: e.CreatedAt, CreatedUser: toCreatedUserDTO(e.ProjectFlock), Approval: toApprovalDTO(e), } + + return ProjectFlockKandangDetailDTO{ + ProjectFlockKandangListDTO: listDTO, + Chickins: toChickinDTOs(e.Chickins), + AvailableQtys: toAvailableQtyDTOsFromMap(e.Chickins, availableQtyMap), + } } func toKandangDTO(kandang entity.Kandang) *KandangDTO { @@ -151,8 +151,6 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), ProjectFlock: toProjectFlockDTO(projectFlockSummary), Kandang: toKandangDTO(e.Kandang), - Chickins: toChickinDTOs(e.Chickins), - AvailableQtys: toAvailableQtyDTOs(e.Chickins), CreatedAt: e.CreatedAt, CreatedUser: toCreatedUserDTO(e.ProjectFlock), Approval: toApprovalDTO(e), @@ -167,75 +165,6 @@ func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFloc return result } -func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDetailDTO { - return ProjectFlockKandangDetailDTO{ - ProjectFlockKandangListDTO: ToProjectFlockKandangListDTO(e), - } -} - -// === Helper Functions (ordered) === - -func toProductWarehouseDTO(pwData map[string]interface{}) *ProductWarehouseDTO { - if pwData == nil { - return nil - } - - dto := &ProductWarehouseDTO{} - - if id, ok := pwData["id"].(float64); ok { - dto.Id = uint(id) - } else if id, ok := pwData["id"].(uint); ok { - dto.Id = id - } - - if pData, ok := pwData["product"].(map[string]interface{}); ok { - dto.Product = toProductDTO(pData) - } - - if wData, ok := pwData["warehouse"].(map[string]interface{}); ok { - dto.Warehouse = toWarehouseDTO(wData) - } - - return dto -} - -func toProductDTO(pData map[string]interface{}) *productDTO.ProductBaseDTO { - if pData == nil { - return nil - } - - product := &productDTO.ProductBaseDTO{} - if id, ok := pData["id"].(float64); ok { - product.Id = uint(id) - } else if id, ok := pData["id"].(uint); ok { - product.Id = id - } - if name, ok := pData["name"].(string); ok { - product.Name = name - } - return product -} - -func toWarehouseDTO(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO { - if wData == nil { - return nil - } - - warehouse := &warehouseDTO.WarehouseBaseDTO{} - if id, ok := wData["id"].(float64); ok { - warehouse.Id = uint(id) - } else if id, ok := wData["id"].(uint); ok { - warehouse.Id = id - } - if name, ok := wData["name"].(string); ok { - warehouse.Name = name - } - if wType, ok := wData["type"].(string); ok { - warehouse.Type = wType - } - return warehouse -} - func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO { if pf.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(pf.CreatedUser) @@ -261,78 +190,49 @@ func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO return result } -func toAvailableQtyDTOs(chickins []entity.ProjectChickin) []AvailableQtyDTO { - if len(chickins) == 0 { - return nil - } - - availableQtyMap := make(map[uint]AvailableQtyDTO) - for _, ch := range chickins { - if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 { - continue - } - - if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists { - continue - } - - pwDTO := &ProductWarehouseDTO{ - Id: ch.ProductWarehouse.Id, - } - - if ch.ProductWarehouse.Product.Id != 0 { - pwDTO.Product = &productDTO.ProductBaseDTO{ - Id: ch.ProductWarehouse.Product.Id, - Name: ch.ProductWarehouse.Product.Name, - } - } - - if ch.ProductWarehouse.Warehouse.Id != 0 { - pwDTO.Warehouse = &warehouseDTO.WarehouseBaseDTO{ - Id: ch.ProductWarehouse.Warehouse.Id, - Name: ch.ProductWarehouse.Warehouse.Name, - Type: ch.ProductWarehouse.Warehouse.Type, - } - } - - availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{ - ProductWarehouse: pwDTO, - } - } - +func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap map[uint]float64) []AvailableQtyDTO { if len(availableQtyMap) == 0 { return nil } + pwMap := make(map[uint]*entity.ProductWarehouse) + for _, chickin := range chickins { + if chickin.ProductWarehouse != nil && chickin.ProductWarehouse.Id != 0 { + pwMap[chickin.ProductWarehouseId] = chickin.ProductWarehouse + } + } + result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) - for _, v := range availableQtyMap { - result = append(result, v) - } - return result -} - -func toAvailableQtyDTOsFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { - if len(availableQtysRaw) == 0 { - return nil - } - - result := make([]AvailableQtyDTO, len(availableQtysRaw)) - for i, v := range availableQtysRaw { - pwData, ok := v["product_warehouse"].(map[string]interface{}) - if !ok { + for pwId, availableQty := range availableQtyMap { + pw, exists := pwMap[pwId] + if !exists || pw == nil { continue } - pwDTO := toProductWarehouseDTO(pwData) - availableQty := 0.0 - if qty, ok := v["available_qty"].(float64); ok { - availableQty = qty - } + pwDTO := ToProductWarehouseDTO(pw) - result[i] = AvailableQtyDTO{ + result = append(result, AvailableQtyDTO{ AvailableQty: availableQty, ProductWarehouse: pwDTO, - } + }) } + return result } + +func ToProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO { + if pw == nil { + return nil + } + + chickinPwDTO := chickinDTO.ToProductWarehouseDTO(pw) + if chickinPwDTO == nil { + return nil + } + + return &ProductWarehouseDTO{ + Id: chickinPwDTO.Id, + Product: chickinPwDTO.Product, + Warehouse: chickinPwDTO.Warehouse, + } +} diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 90e85882..fcdb31f4 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -7,7 +7,6 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" - dto "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -19,10 +18,12 @@ import ( ) type ProjectFlockKandangService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, error) } +// Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity + type projectFlockKandangService struct { Log *logrus.Logger Validate *validator.Validate @@ -45,18 +46,11 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository } } -func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) { +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } - if params.Page <= 0 { - params.Page = 1 - } - if params.Limit <= 0 { - params.Limit = 10 - } - offset := (params.Page - 1) * params.Limit projectFlockKandangs, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) @@ -85,24 +79,11 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } } } - results := make([]dto.ProjectFlockKandangWithAvailableQtysDTO, 0) - for i := range projectFlockKandangs { - availableQtys, err := s.getAvailableQuantities(c, &projectFlockKandangs[i]) - if err != nil { - s.Log.Warnf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandangs[i].Kandang.Id, err) - availableQtys = nil - } - results = append(results, dto.ProjectFlockKandangWithAvailableQtysDTO{ - Entity: projectFlockKandangs[i], - AvailableQtys: availableQtys, - }) - } - - return results, total, nil + return projectFlockKandangs, total, nil } -func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) { +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, error) { projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") @@ -122,16 +103,16 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje } } - availableQtys, err := s.getAvailableQuantities(c, projectFlockKandang) + availableQtyMap, err := s.getAvailableQuantities(c, projectFlockKandang) if err != nil { s.Log.Errorf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandang.Kandang.Id, err) - availableQtys = nil + availableQtyMap = nil } - return projectFlockKandang, availableQtys, nil + return projectFlockKandang, availableQtyMap, nil } -func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) ([]map[string]interface{}, error) { +func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) (map[uint]float64, error) { if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil { return nil, nil } @@ -155,38 +136,16 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project return nil, nil } - var result []map[string]interface{} + result := make(map[uint]float64) for _, pw := range products { availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) if err != nil { s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) } - if availableQty <= 0 { - continue + if availableQty > 0 { + result[pw.Id] = availableQty } - - productData := map[string]interface{}{ - "id": pw.Product.Id, - "name": pw.Product.Name, - } - - warehouseData := map[string]interface{}{ - "id": pw.Warehouse.Id, - "name": pw.Warehouse.Name, - "type": pw.Warehouse.Type, - } - - productWarehouseData := map[string]interface{}{ - "id": pw.Id, - "product": productData, - "warehouse": warehouseData, - } - - result = append(result, map[string]interface{}{ - "available_qty": availableQty, - "product_warehouse": productWarehouseData, - }) } return result, 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 1a05acff..a2ab8ebe 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -189,6 +189,8 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint Preload("Chickins"). Preload("Chickins.CreatedUser"). Preload("Chickins.ProductWarehouse"). + Preload("Chickins.ProductWarehouse.Product"). + Preload("Chickins.ProductWarehouse.Warehouse"). First(record, id).Error; err != nil { return nil, err } @@ -210,6 +212,8 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont Preload("Chickins"). Preload("Chickins.CreatedUser"). Preload("Chickins.ProductWarehouse"). + Preload("Chickins.ProductWarehouse.Product"). + Preload("Chickins.ProductWarehouse.Warehouse"). First(record).Error; err != nil { return nil, err } From d21aaead7babff8c0c91b7b5ed0f8fa13854b622 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 07:58:00 +0700 Subject: [PATCH 23/59] Fix[BE]: use new request body for frontend requrement on chickin create API --- .../chickins/services/chickin.service.go | 49 +++++++++---------- .../validations/chickin.validation.go | 11 +++-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 974b5c9f..a130740a 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -123,39 +123,34 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found") } - var productWarehouses []entity.ProductWarehouse category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) - var productCategoryCode string - switch category { - case string(utils.ProjectFlockCategoryGrowing): - productCategoryCode = "DOC" - case string(utils.ProjectFlockCategoryLaying): - productCategoryCode = "PULLET" - default: - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category)) - } - - productWarehouses, err = s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) - if err != nil || len(productWarehouses) == 0 { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category))) - } - - chickinDate, err := utils.ParseDateString(req.ChickInDate) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format") - } - actorID := uint(1) // todo nanti ambil dari auth context newChikins := make([]*entity.ProjectChickin, 0) - for _, productWarehouse := range productWarehouses { - availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category) + + for _, chickinReq := range req.ChickinRequests { + + productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, nil) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", productWarehouse.Id)) + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickinReq.ProductWarehouseId)) + } + + if productWarehouse.WarehouseId != warehouse.Id { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId)) + } + + chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId)) + } + + availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, productWarehouse, category) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", chickinReq.ProductWarehouseId)) } if availableQty <= 0 { - continue + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available quantity for product warehouse %d", chickinReq.ProductWarehouseId)) } newChickin := &entity.ProjectChickin{ @@ -163,8 +158,8 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti ChickInDate: chickinDate, UsageQty: 0, PendingUsageQty: availableQty, - ProductWarehouseId: productWarehouse.Id, - Notes: req.Note, + ProductWarehouseId: chickinReq.ProductWarehouseId, + Notes: chickinReq.Note, CreatedBy: actorID, } diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go index c2676130..ebc6487f 100644 --- a/internal/modules/production/chickins/validations/chickin.validation.go +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -1,9 +1,14 @@ 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"` - Note string `json:"note" validate:"omitempty"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` + ChickinRequests []ChickinRequestItem `json:"chickin_requests" validate:"required,min=1,dive"` +} + +type ChickinRequestItem struct { + ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` + ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` + Note string `json:"note" validate:"omitempty"` } type Update struct { From ba12320d12b86c4d7aa06c261781945e6b001b80 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 09:01:37 +0700 Subject: [PATCH 24/59] Feat[BE-221]: create So DO migration --- .../20251107013921_create_marketings.down.sql | 5 +++ .../20251107013921_create_marketings.up.sql | 44 +++++++++++++++++++ ...7015122_create_marketing_products.down.sql | 1 + ...107015122_create_marketing_products.up.sql | 34 ++++++++++++++ ...eate_marketing_product_deliveries.down.sql | 2 + ...create_marketing_product_deliveries.up.sql | 29 ++++++++++++ 6 files changed, 115 insertions(+) create mode 100644 internal/database/migrations/20251107013921_create_marketings.down.sql create mode 100644 internal/database/migrations/20251107013921_create_marketings.up.sql create mode 100644 internal/database/migrations/20251107015122_create_marketing_products.down.sql create mode 100644 internal/database/migrations/20251107015122_create_marketing_products.up.sql create mode 100644 internal/database/migrations/20251107015128_create_marketing_product_deliveries.down.sql create mode 100644 internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql diff --git a/internal/database/migrations/20251107013921_create_marketings.down.sql b/internal/database/migrations/20251107013921_create_marketings.down.sql new file mode 100644 index 00000000..df4a0db9 --- /dev/null +++ b/internal/database/migrations/20251107013921_create_marketings.down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS marketing_delivery_products CASCADE; + +DROP TABLE IF EXISTS marketing_products CASCADE; + +DROP TABLE IF EXISTS marketings CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251107013921_create_marketings.up.sql b/internal/database/migrations/20251107013921_create_marketings.up.sql new file mode 100644 index 00000000..d2a3e24f --- /dev/null +++ b/internal/database/migrations/20251107013921_create_marketings.up.sql @@ -0,0 +1,44 @@ +CREATE TABLE marketings ( + id BIGSERIAL PRIMARY KEY, + so_number VARCHAR(255) UNIQUE NOT NULL, + customer_id BIGINT NOT NULL, + so_docs VARCHAR(20), + so_date DATE NOT NULL, + sales_person_id BIGINT NOT NULL, + notes TEXT, + created_by BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'customers') THEN + ALTER TABLE marketings + ADD CONSTRAINT fk_marketings_customer_id + FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE marketings + ADD CONSTRAINT fk_marketings_sales_person_id + FOREIGN KEY (sales_person_id) REFERENCES users(id) ON DELETE RESTRICT; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE marketings + ADD CONSTRAINT fk_marketings_created_by + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; + END IF; +END $$; + +CREATE INDEX idx_marketings_customer_id ON marketings (customer_id); + +CREATE INDEX idx_marketings_sales_person_id ON marketings (sales_person_id); + +CREATE INDEX idx_marketings_created_by ON marketings (created_by); + +CREATE INDEX idx_marketings_so_date ON marketings (so_date); + +CREATE INDEX idx_marketings_deleted_at ON marketings (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251107015122_create_marketing_products.down.sql b/internal/database/migrations/20251107015122_create_marketing_products.down.sql new file mode 100644 index 00000000..c40efdb3 --- /dev/null +++ b/internal/database/migrations/20251107015122_create_marketing_products.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS marketing_products CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251107015122_create_marketing_products.up.sql b/internal/database/migrations/20251107015122_create_marketing_products.up.sql new file mode 100644 index 00000000..5490f931 --- /dev/null +++ b/internal/database/migrations/20251107015122_create_marketing_products.up.sql @@ -0,0 +1,34 @@ +CREATE TABLE marketing_products ( + id BIGSERIAL PRIMARY KEY, + marketing_id BIGINT NOT NULL, + product_warehouse_id BIGINT NOT NULL, + qty NUMERIC(15, 3) NOT NULL, + unit_price NUMERIC(15, 3) NOT NULL, + avg_weight NUMERIC(15, 3) NOT NULL, + total_weight NUMERIC(15, 3) NOT NULL, + total_price NUMERIC(15, 3) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketings') THEN + ALTER TABLE marketing_products + ADD CONSTRAINT fk_marketing_products_marketing_id + FOREIGN KEY (marketing_id) REFERENCES marketings(id) ON DELETE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE marketing_products + ADD CONSTRAINT fk_marketing_products_product_warehouse_id + FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE RESTRICT; + END IF; +END $$; + +CREATE INDEX idx_marketing_products_marketing_id ON marketing_products (marketing_id); + +CREATE INDEX idx_marketing_products_product_warehouse_id ON marketing_products (product_warehouse_id); + +CREATE INDEX idx_marketing_products_deleted_at ON marketing_products (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251107015128_create_marketing_product_deliveries.down.sql b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.down.sql new file mode 100644 index 00000000..20da9516 --- /dev/null +++ b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE IF EXISTS marketing_delivery_products CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql new file mode 100644 index 00000000..09625c16 --- /dev/null +++ b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql @@ -0,0 +1,29 @@ +CREATE TABLE marketing_delivery_products ( + id BIGSERIAL PRIMARY KEY, + marketing_product_id BIGINT UNIQUE NOT NULL, + qty NUMERIC(15, 3) NOT NULL, + unit_price NUMERIC(15, 3) NOT NULL, + total_weight NUMERIC(15, 3) NOT NULL, + avg_weight NUMERIC(15, 3) NOT NULL, + total_price NUMERIC(15, 3) NOT NULL, + delivery_date TIMESTAMPTZ, + vehicle_number VARCHAR(50), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketing_products') THEN + ALTER TABLE marketing_delivery_products + ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id + FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id) ON DELETE CASCADE; + END IF; +END $$; + +CREATE INDEX idx_marketing_delivery_products_marketing_product_id ON marketing_delivery_products (marketing_product_id); + +CREATE INDEX idx_marketing_delivery_products_delivery_date ON marketing_delivery_products (delivery_date); + +CREATE INDEX idx_marketing_delivery_products_deleted_at ON marketing_delivery_products (deleted_at); \ No newline at end of file From 6e69e97d269f857abc19e86615715914df5cdcb5 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 13:24:48 +0700 Subject: [PATCH 25/59] feat[BE-127]: create available qty API and inisiate sales order and delivery order --- internal/entities/marketing.go | 26 ++++ .../entities/marketing_delivery_product.go | 24 +++ internal/entities/marketing_product.go | 24 +++ internal/entities/sales-orders.go | 18 +++ internal/modules/marketing/module.go | 13 ++ internal/modules/marketing/route.go | 25 +++ .../controllers/sales-orders.controller.go | 144 ++++++++++++++++++ .../sales-orders/dto/sales-orders.dto.go | 64 ++++++++ .../modules/marketing/sales-orders/module.go | 26 ++++ .../repositories/sales-orders.repository.go | 21 +++ .../modules/marketing/sales-orders/route.go | 28 ++++ .../services/sales-orders.service.go | 129 ++++++++++++++++ .../validations/sales-orders.validation.go | 15 ++ .../controllers/projectflock.controller.go | 4 + .../project_flock_population_repository.go | 15 ++ .../repositories/projectflock.repository.go | 3 + .../production/project_flocks/route.go | 2 +- .../validations/projectflock.validation.go | 1 + .../controllers/transfer_laying.controller.go | 63 ++++++-- .../dto/transfer_laying.dto.go | 15 +- .../production/transfer_layings/route.go | 1 + .../services/transfer_laying.service.go | 88 ++++++----- .../validations/transfer_laying.validation.go | 12 +- internal/route/route.go | 2 + 24 files changed, 695 insertions(+), 68 deletions(-) create mode 100644 internal/entities/marketing.go create mode 100644 internal/entities/marketing_delivery_product.go create mode 100644 internal/entities/marketing_product.go create mode 100644 internal/entities/sales-orders.go create mode 100644 internal/modules/marketing/module.go create mode 100644 internal/modules/marketing/route.go create mode 100644 internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go create mode 100644 internal/modules/marketing/sales-orders/dto/sales-orders.dto.go create mode 100644 internal/modules/marketing/sales-orders/module.go create mode 100644 internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go create mode 100644 internal/modules/marketing/sales-orders/route.go create mode 100644 internal/modules/marketing/sales-orders/services/sales-orders.service.go create mode 100644 internal/modules/marketing/sales-orders/validations/sales-orders.validation.go diff --git a/internal/entities/marketing.go b/internal/entities/marketing.go new file mode 100644 index 00000000..1ae4d8c3 --- /dev/null +++ b/internal/entities/marketing.go @@ -0,0 +1,26 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type Marketing struct { + Id uint `gorm:"primaryKey;autoIncrement"` + SoNumber string `gorm:"uniqueIndex;not null"` + CustomerId uint `gorm:"not null"` + SoDocs string `gorm:"type:varchar(20)"` + SoDate time.Time `gorm:"type:date;not null"` + SalesPersonId uint `gorm:"not null"` + Notes 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:"-"` + + Customer Customer `gorm:"foreignKey:CustomerId;references:Id"` + SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"` +} diff --git a/internal/entities/marketing_delivery_product.go b/internal/entities/marketing_delivery_product.go new file mode 100644 index 00000000..85b4591a --- /dev/null +++ b/internal/entities/marketing_delivery_product.go @@ -0,0 +1,24 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type MarketingDeliveryProduct struct { + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingProductId uint `gorm:"uniqueIndex;not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + TotalWeight float64 `gorm:"type:numeric(15,3);not null"` + AvgWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + DeliveryDate *time.Time `gorm:"type:timestamptz"` + VehicleNumber string `gorm:"type:varchar(50)"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"` +} diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go new file mode 100644 index 00000000..2e6aef58 --- /dev/null +++ b/internal/entities/marketing_product.go @@ -0,0 +1,24 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type MarketingProduct struct { + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + AvgWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` + ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` +} diff --git a/internal/entities/sales-orders.go b/internal/entities/sales-orders.go new file mode 100644 index 00000000..48615607 --- /dev/null +++ b/internal/entities/sales-orders.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type SalesOrders struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/marketing/module.go b/internal/modules/marketing/module.go new file mode 100644 index 00000000..9bf4f018 --- /dev/null +++ b/internal/modules/marketing/module.go @@ -0,0 +1,13 @@ +package marketing + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +type MarketingModule struct{} + +func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + RegisterRoutes(router, db, validate) +} diff --git a/internal/modules/marketing/route.go b/internal/modules/marketing/route.go new file mode 100644 index 00000000..1b72b8cb --- /dev/null +++ b/internal/modules/marketing/route.go @@ -0,0 +1,25 @@ +package marketing + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/modules" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders" + // MODULE IMPORTS +) + +func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + group := router.Group("/marketing") + + allModules := []modules.Module{ + salesOrderss.SalesOrdersModule{}, + // MODULE REGISTRY + } + + for _, m := range allModules { + m.RegisterRoutes(group, db, validate) + } +} diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go new file mode 100644 index 00000000..837a8ee0 --- /dev/null +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type SalesOrdersController struct { + SalesOrdersService service.SalesOrdersService +} + +func NewSalesOrdersController(salesOrdersService service.SalesOrdersService) *SalesOrdersController { + return &SalesOrdersController{ + SalesOrdersService: salesOrdersService, + } +} + +func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.SalesOrdersService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.SalesOrdersListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all salesOrderss successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToSalesOrdersListDTOs(result), + }) +} + +func (u *SalesOrdersController) 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.SalesOrdersService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get salesOrders successfully", + Data: dto.ToSalesOrdersListDTO(*result), + }) +} + +func (u *SalesOrdersController) 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.SalesOrdersService.CreateOne(c, req) + // if err != nil { + // return err + // } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create salesOrders successfully", + Data: req, + }) +} + +func (u *SalesOrdersController) 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.SalesOrdersService.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 salesOrders successfully", + Data: dto.ToSalesOrdersListDTO(*result), + }) +} + +func (u *SalesOrdersController) 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.SalesOrdersService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete salesOrders successfully", + }) +} diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go new file mode 100644 index 00000000..6dd7c8e3 --- /dev/null +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -0,0 +1,64 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type SalesOrdersBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type SalesOrdersListDTO struct { + SalesOrdersBaseDTO + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type SalesOrdersDetailDTO struct { + SalesOrdersListDTO +} + +// === Mapper Functions === + +func ToSalesOrdersBaseDTO(e entity.SalesOrders) SalesOrdersBaseDTO { + return SalesOrdersBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + return SalesOrdersListDTO{ + SalesOrdersBaseDTO: ToSalesOrdersBaseDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToSalesOrdersListDTOs(e []entity.SalesOrders) []SalesOrdersListDTO { + result := make([]SalesOrdersListDTO, len(e)) + for i, r := range e { + result[i] = ToSalesOrdersListDTO(r) + } + return result +} + +func ToSalesOrdersDetailDTO(e entity.SalesOrders) SalesOrdersDetailDTO { + return SalesOrdersDetailDTO{ + SalesOrdersListDTO: ToSalesOrdersListDTO(e), + } +} diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go new file mode 100644 index 00000000..12310354 --- /dev/null +++ b/internal/modules/marketing/sales-orders/module.go @@ -0,0 +1,26 @@ +package sales_orders + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type SalesOrdersModule struct{} + +func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db) + userRepo := rUser.NewUserRepository(db) + + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + SalesOrdersRoutes(router, userService, salesOrdersService) +} + diff --git a/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go b/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go new file mode 100644 index 00000000..5f8cfe79 --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type SalesOrdersRepository interface { + repository.BaseRepository[entity.SalesOrders] +} + +type SalesOrdersRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.SalesOrders] +} + +func NewSalesOrdersRepository(db *gorm.DB) SalesOrdersRepository { + return &SalesOrdersRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.SalesOrders](db), + } +} diff --git a/internal/modules/marketing/sales-orders/route.go b/internal/modules/marketing/sales-orders/route.go new file mode 100644 index 00000000..455977fb --- /dev/null +++ b/internal/modules/marketing/sales-orders/route.go @@ -0,0 +1,28 @@ +package sales_orders + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers" + salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) { + ctrl := controller.NewSalesOrdersController(s) + + route := v1.Group("/sales-orders") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go new file mode 100644 index 00000000..33c93f6d --- /dev/null +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -0,0 +1,129 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type SalesOrdersService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.SalesOrders, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type salesOrdersService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.SalesOrdersRepository +} + +func NewSalesOrdersService(repo repository.SalesOrdersRepository, validate *validator.Validate) SalesOrdersService { + return &salesOrdersService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + +func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + salesOrderss, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.Search != "" { + return db.Where("name LIKE ?", "%"+params.Search+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get salesOrderss: %+v", err) + return nil, 0, err + } + return salesOrderss, total, nil +} + +func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.SalesOrders, error) { + salesOrders, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") + } + if err != nil { + s.Log.Errorf("Failed get salesOrders by id: %+v", err) + return nil, err + } + return salesOrders, nil +} + +func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + createBody := &entity.SalesOrders{ + Name: req.Name, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create salesOrders: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.Name != nil { + updateBody["name"] = *req.Name + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") + } + s.Log.Errorf("Failed to update salesOrders: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") + } + s.Log.Errorf("Failed to delete salesOrders: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go new file mode 100644 index 00000000..7d16d3ee --- /dev/null +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + 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/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index d3b0061c..cf2dff05 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -71,6 +71,10 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { if period := c.QueryInt("period", 0); period > 0 { query.Period = period } + if category := c.Query("category", ""); category != "" { + query.Category = category + + } if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" { ids, err := parseUintList(kandangRaw) 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 index d914b128..a2b56dce 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -14,6 +14,7 @@ type ProjectFlockPopulationRepository interface { ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) + GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) // subset of base repository methods used by services CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error @@ -91,3 +92,17 @@ func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProd } return records, nil } + +func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { + var total float64 + err := r.DB().WithContext(ctx). + Table("project_flock_populations"). + Select("COALESCE(SUM(total_qty), 0) AS total_qty"). + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID). + Scan(&total).Error + if err != nil { + return 0, err + } + return total, nil +} diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index a8fab919..41cbde12 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -127,6 +127,9 @@ func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *vali return db } + if params.Category != "" { + db = db.Where("project_flocks.category = ?", params.Category) + } if params.AreaId > 0 { db = db.Where("project_flocks.area_id = ?", params.AreaId) } diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index 70a22bad..a8d4ca6d 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -28,5 +28,5 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary) - + } diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index 7932e07e..33f20725 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -27,6 +27,7 @@ type Query struct { 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"` + Category string `query:"category" validate:"omitempty"` KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` } diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index 62d64127..c69f4ff5 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -1,6 +1,7 @@ package controller import ( + "fmt" "math" "strconv" @@ -24,16 +25,8 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)), - TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)), - TransferDateFrom: c.Query("transfer_date_from", ""), - TransferDateTo: c.Query("transfer_date_to", ""), - ApprovalStatus: c.Query("approval_status", ""), - TransferNumber: c.Query("transfer_number", ""), - Sort: c.Query("sort", "created_at"), - Order: c.Query("order", "desc"), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), } if query.Page < 1 || query.Limit < 1 { @@ -45,8 +38,13 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { return err } + data := make([]dto.TransferLayingDetailDTO, len(result)) + for i, transfer := range result { + data[i] = dto.ToTransferLayingDetailDTOWithSingleApproval(transfer, transfer.LatestApproval) + } + return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{ + JSON(response.SuccessWithPaginate[dto.TransferLayingDetailDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get all transferLayings successfully", @@ -56,7 +54,7 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToTransferLayingListDTOs(result), + Data: data, }) } @@ -113,7 +111,7 @@ func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error { } if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %s", err.Error())) } result, err := u.TransferLayingService.UpdateOne(c, req, uint(id)) @@ -126,7 +124,7 @@ func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update transferLaying successfully", - Data: dto.ToTransferLayingListDTO(*result), + Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, result.LatestApproval), }) } @@ -180,3 +178,40 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error { Data: data, }) } + + +func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error { + projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") + } + + pf, kandangQtyMap, err := u.TransferLayingService.GetAvailableQtyPerKandang(c, uint(projectFlockID)) + if err != nil { + return err + } + + // Build kandang list response + kandangs := make([]dto.KandangAvailableQtyDTO, 0, len(kandangQtyMap)) + for kandangPFID, qty := range kandangQtyMap { + kandangs = append(kandangs, dto.KandangAvailableQtyDTO{ + ProjectFlockKandangId: kandangPFID, + AvailableQty: qty, + }) + } + + resp := dto.AvailableQtyForTransferDTO{ + ProjectFlockId: pf.Id, + ProjectFlockCode: pf.FlockName, + Category: pf.Category, + Kandangs: kandangs, + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get available quantity successfully", + Data: resp, + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 719e458a..087f146a 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -77,6 +77,20 @@ type TransferLayingDetailDTO struct { Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } +// === Available Quantity DTOs === + +type KandangAvailableQtyDTO struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + AvailableQty float64 `json:"available_qty"` +} + +type AvailableQtyForTransferDTO struct { + ProjectFlockId uint `json:"project_flock_id"` + ProjectFlockCode string `json:"project_flock_code"` + Category string `json:"category"` + Kandangs []KandangAvailableQtyDTO `json:"kandangs"` +} + // === Mapper Functions === func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { @@ -207,7 +221,6 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO { var latestApproval *approvalDTO.ApprovalBaseDTO - // Use LatestApproval from entity if available if e.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) latestApproval = &mapped diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go index 13a0bc8f..ad0cb9e1 100644 --- a/internal/modules/production/transfer_layings/route.go +++ b/internal/modules/production/transfer_layings/route.go @@ -27,4 +27,5 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying. route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) route.Post("/approvals", ctrl.Approval) + route.Get("/project-flocks/:project_flock_id/available-qty", ctrl.GetAvailableQtyPerKandang) } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 06c297bc..e7591aef 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -31,6 +31,7 @@ type TransferLayingService interface { UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) + GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) } type transferLayingService struct { @@ -92,32 +93,7 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - if params.SourceProjectFlockId != 0 { - db = db.Where("from_project_flock_id = ?", params.SourceProjectFlockId) - } - if params.TargetProjectFlockId != 0 { - db = db.Where("to_project_flock_id = ?", params.TargetProjectFlockId) - } - if params.TransferDateFrom != "" { - db = db.Where("transfer_date >= ?", params.TransferDateFrom) - } - if params.TransferDateTo != "" { - db = db.Where("transfer_date <= ?", params.TransferDateTo) - } - if params.TransferNumber != "" { - db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%") - } - - sortField := "created_at" - if params.Sort != "" { - sortField = params.Sort - } - sortOrder := "DESC" - if params.Order == "asc" { - sortOrder = "ASC" - } - db = db.Order(fmt.Sprintf("%s %s", sortField, sortOrder)) - + db = db.Order("created_at DESC") return db }) @@ -126,19 +102,14 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } - if params.ApprovalStatus != "" { - var filtered []entity.LayingTransfer - approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) - - for _, transfer := range transferLayings { - latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil) - if err == nil && latestApproval != nil && latestApproval.Action != nil { - if string(*latestApproval.Action) == params.ApprovalStatus { - filtered = append(filtered, transfer) - } - } + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + for i, transfer := range transferLayings { + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err == nil && latestApproval != nil { + transferLayings[i].LatestApproval = latestApproval } - transferLayings = filtered } return transferLayings, total, nil @@ -155,7 +126,9 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran } approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) - latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil) + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) if err == nil && latestApproval != nil { transferLaying.LatestApproval = latestApproval } @@ -547,7 +520,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action)) } } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) @@ -851,8 +823,6 @@ func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Contex return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration") } - // Restore in LIFO order (from newest to oldest) - // Add all quantity back to the last (newest) population if len(populations) > 0 { lastPop := populations[len(populations)-1] newQty := lastPop.TotalQty + quantityToRestore @@ -863,3 +833,37 @@ func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Contex return nil } + +func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) { + + pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { + return db + }) + if err != nil { + s.Log.Errorf("Failed to get project flock %d: %+v", projectFlockID, err) + return nil, nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + } + + kandangs, _, err := s.ProjectFlockKandangRepo.GetAll(ctx.Context(), 0, 1000, func(db *gorm.DB) *gorm.DB { + return db.Where("project_flock_id = ?", projectFlockID).Order("kandang_id ASC") + }) + if err != nil { + s.Log.Errorf("Failed to get kandangs for project flock %d: %+v", projectFlockID, err) + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs") + } + + kandangAvailableQty := make(map[uint]float64) + for _, kandang := range kandangs { + + totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id) + if err != nil { + s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err) + kandangAvailableQty[kandang.Id] = 0 + continue + } + + kandangAvailableQty[kandang.Id] = totalQty + } + + return pf, kandangAvailableQty, nil +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index bd117bd9..45a73e48 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -29,16 +29,8 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - SourceProjectFlockId uint `query:"source_project_flock_id" validate:"omitempty"` - TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"` - TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"` - TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"` - ApprovalStatus string `query:"approval_status" validate:"omitempty,oneof=PENDING APPROVED REJECTED"` // Filter by latest approval status - TransferNumber string `query:"transfer_number" validate:"omitempty"` // Search by transfer number - Sort string `query:"sort" validate:"omitempty,oneof=created_at transfer_date"` // Sort by field - Order string `query:"order" validate:"omitempty,oneof=asc desc"` // Sort order + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` } type Approve struct { diff --git a/internal/route/route.go b/internal/route/route.go index d6277549..d66828c6 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -15,6 +15,7 @@ import ( production "gitlab.com/mbugroup/lti-api.git/internal/modules/production" purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" + marketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing" // MODULE IMPORTS ) @@ -32,6 +33,7 @@ func Routes(app *fiber.App, db *gorm.DB) { production.ProductionModule{}, approvals.ApprovalModule{}, purchases.PurchaseModule{}, + marketing.MarketingModule{}, // MODULE REGISTRY } From 4c279baad785c8f9ae5257785edcfd7f43d9e573 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 13:32:03 +0700 Subject: [PATCH 26/59] FIX[BE} change json response on avaibility transfer laying --- .../production/transfer_layings/dto/transfer_laying.dto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 087f146a..98c1a2e7 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -86,7 +86,7 @@ type KandangAvailableQtyDTO struct { type AvailableQtyForTransferDTO struct { ProjectFlockId uint `json:"project_flock_id"` - ProjectFlockCode string `json:"project_flock_code"` + ProjectFlockCode string `json:"flock_name"` Category string `json:"category"` Kandangs []KandangAvailableQtyDTO `json:"kandangs"` } From 3785d529257c3d6bc9585e92b6ab855bab8b5ac6 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 15:26:32 +0700 Subject: [PATCH 27/59] FIX[BE]: fix get projectflock kandang periods not found --- internal/modules/production/project_flocks/route.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index a8d4ca6d..9ec64799 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -25,8 +25,8 @@ 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.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) - route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary) } From b2ed58c7342f16be2c1b54525b9fb7f91e8e1f30 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 16:41:52 +0700 Subject: [PATCH 28/59] Fix[BE]: availableqty not appeared --- .../project_flock_kandang.controller.go | 4 +-- .../dto/project_flock_kandang.dto.go | 14 ++++++++--- .../services/project_flock_kandang.service.go | 25 +++++++++++++++---- .../dto/projectflock_kandang.dto.go | 25 ++++++++----------- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 21701826..94316dce 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -73,7 +73,7 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, availableQtys, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) + result, availableQtys, productWarehouses, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) if err != nil { return err } @@ -83,6 +83,6 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get projectFlockKandang successfully", - Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys), + Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses), }) } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index e80e3220..35bf615d 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -97,7 +97,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO } } -func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64) ProjectFlockKandangDetailDTO { +func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse) ProjectFlockKandangDetailDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) @@ -116,7 +116,7 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang return ProjectFlockKandangDetailDTO{ ProjectFlockKandangListDTO: listDTO, Chickins: toChickinDTOs(e.Chickins), - AvailableQtys: toAvailableQtyDTOsFromMap(e.Chickins, availableQtyMap), + AvailableQtys: toAvailableQtyDTOsFromMap(e.Chickins, availableQtyMap, productWarehouses), } } @@ -190,11 +190,12 @@ func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO return result } -func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap map[uint]float64) []AvailableQtyDTO { +func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse) []AvailableQtyDTO { if len(availableQtyMap) == 0 { return nil } + // First, build map from chickins pwMap := make(map[uint]*entity.ProductWarehouse) for _, chickin := range chickins { if chickin.ProductWarehouse != nil && chickin.ProductWarehouse.Id != 0 { @@ -202,6 +203,13 @@ func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap } } + // Then, add productWarehouses that are not in chickins yet + for i := range productWarehouses { + if _, exists := pwMap[productWarehouses[i].Id]; !exists { + pwMap[productWarehouses[i].Id] = &productWarehouses[i] + } + } + result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) for pwId, availableQty := range availableQtyMap { pw, exists := pwMap[pwId] diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index fcdb31f4..11e8b0d5 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -19,7 +19,7 @@ import ( type ProjectFlockKandangService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) } // Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity @@ -83,13 +83,13 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer return projectFlockKandangs, total, nil } -func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, error) { +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } if err != nil { - return nil, nil, err + return nil, nil, nil, err } if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil { @@ -109,7 +109,22 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje availableQtyMap = nil } - return projectFlockKandang, availableQtyMap, nil + var productWarehouses []entity.ProductWarehouse + if len(availableQtyMap) > 0 { + pwIds := make([]uint, 0, len(availableQtyMap)) + for pwId := range availableQtyMap { + pwIds = append(pwIds, pwId) + } + productWarehouses, err = s.ProductWarehouseRepo.GetByIDs(c.Context(), pwIds, func(db *gorm.DB) *gorm.DB { + return db.Preload("Product").Preload("Warehouse") + }) + if err != nil { + s.Log.Warnf("Failed to fetch product warehouses: %+v", err) + productWarehouses = []entity.ProductWarehouse{} + } + } + + return projectFlockKandang, availableQtyMap, productWarehouses, nil } func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) (map[uint]float64, error) { diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 24e53d28..233f655b 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -28,13 +28,12 @@ type ProjectFlockWithPivotDTO struct { } type ProjectFlockKandangDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ProjectFlockId uint `json:"project_flock_id"` - KandangId uint `json:"kandang_id"` - Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` - ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` - AvailableQuantity float64 `json:"available_quantity"` + Id uint `json:"id"` + + KandangId uint `json:"kandang_id"` + Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` + ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` + AvailableQuantity float64 `json:"available_quantity"` } func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { @@ -89,12 +88,10 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD } return ProjectFlockKandangDTO{ - Id: e.Id, - ProjectFlockKandangId: e.Id, - ProjectFlockId: e.ProjectFlockId, - KandangId: e.KandangId, - Kandang: kandang, - ProjectFlock: pf, - AvailableQuantity: 0, + Id: e.Id, + KandangId: e.KandangId, + Kandang: kandang, + ProjectFlock: pf, + AvailableQuantity: 0, } } From fd0943dfafc85e388c029cb0b19d9233e9adab48 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 10 Nov 2025 14:49:46 +0700 Subject: [PATCH 29/59] feat[BE-222]: create migration create template for SO API and kandang id param on product warehouse --- .../product_warehouse.controller.go | 1 + .../inventory/product-warehouses/module.go | 5 +- .../product_warehouse.repository.go | 5 ++ .../services/product_warehouse.service.go | 32 +++++++--- .../product_warehouse.validation.go | 1 + .../controllers/sales-orders.controller.go | 12 ++-- .../modules/marketing/sales-orders/module.go | 10 ++- .../repositories/marketings.repository.go | 21 +++++++ .../services/sales-orders.service.go | 63 +++++++++++++++---- .../validations/sales-orders.validation.go | 18 +++++- .../repositories/customer.repository.go | 5 ++ .../repositories/kandang.repository.go | 5 ++ 12 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 internal/modules/marketing/sales-orders/repositories/marketings.repository.go diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go index 26f23278..671d964b 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -29,6 +29,7 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { ProductId: uint(c.QueryInt("product_id", 0)), WarehouseId: uint(c.QueryInt("warehouse_id", 0)), Flags: c.Query("flags", ""), + KandangId: uint(c.QueryInt("kandang_id", 0)), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/inventory/product-warehouses/module.go b/internal/modules/inventory/product-warehouses/module.go index dfb72e8f..0dd8047e 100644 --- a/internal/modules/inventory/product-warehouses/module.go +++ b/internal/modules/inventory/product-warehouses/module.go @@ -7,6 +7,7 @@ import ( rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" sProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services" + rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -17,10 +18,10 @@ type ProductWarehouseModule struct{} func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) userRepo := rUser.NewUserRepository(db) + kandangRepo := rKandang.NewKandangRepository(db) - productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate) + productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate, kandangRepo) userService := sUser.NewUserService(userRepo, validate) ProductWarehouseRoutes(router, userService, productWarehouseService) } - diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index 151a3697..80c551eb 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -24,6 +24,7 @@ type ProductWarehouseRepository interface { ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) + IdExists(ctx context.Context, id uint) (bool, error) } type ProductWarehouseRepositoryImpl struct { @@ -47,6 +48,10 @@ func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id) } +func (r *ProductWarehouseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id) +} + func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) { var count int64 query := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}). 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 cc925970..cc7d5b85 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -6,6 +6,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations" + kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" @@ -20,16 +21,18 @@ type ProductWarehouseService interface { } type productWarehouseService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.ProductWarehouseRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProductWarehouseRepository + KandangRepo kandangrepo.KandangRepository } -func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate) ProductWarehouseService { +func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate, kandangRepo kandangrepo.KandangRepository) ProductWarehouseService { return &productWarehouseService{ - Log: utils.Log, - Validate: validate, - Repository: repo, + Log: utils.Log, + Validate: validate, + Repository: repo, + KandangRepo: kandangRepo, } } @@ -69,6 +72,16 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) } } + if params.KandangId > 0 { + isKandangExist, err := s.KandangRepo.IdExists(c.Context(), params.KandangId) + if err != nil { + return nil, 0, err + } + if !isKandangExist { + return nil, 0, fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + } + offset := (params.Page - 1) * params.Limit cleanFlags := utils.ParseFlags(params.Flags) @@ -80,6 +93,11 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) db = db.Where("product_id = ?", params.ProductId) } + if params.KandangId != 0 { + db = db.Joins("JOIN warehouses ON product_warehouses.warehouse_id = warehouses.id"). + Where("warehouses.kandang_id = ?", params.KandangId) + } + if params.WarehouseId != 0 { db = db.Where("warehouse_id = ?", params.WarehouseId) } diff --git a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go index 3a3acb28..322d0a00 100644 --- a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -18,4 +18,5 @@ type Query struct { ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` Flags string `query:"flags" validate:"omitempty"` + KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` } diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index 837a8ee0..be578e15 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -29,7 +29,7 @@ func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { Search: c.Query("search", ""), } - if query.Page < 1 || query.Limit < 1 { + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -82,17 +82,17 @@ func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - // result, err := u.SalesOrdersService.CreateOne(c, req) - // if err != nil { - // return err - // } + result, err := u.SalesOrdersService.CreateOne(c, req) + if err != nil { + return err + } return c.Status(fiber.StatusCreated). JSON(response.Success{ Code: fiber.StatusCreated, Status: "success", Message: "Create salesOrders successfully", - Data: req, + Data: result, }) } diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 12310354..21926f93 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -5,8 +5,11 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" + rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" + rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -17,10 +20,13 @@ type SalesOrdersModule struct{} func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db) userRepo := rUser.NewUserRepository(db) + customerRepo := rCustomer.NewCustomerRepository(db) + kandangRepo := rKandang.NewKandangRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + marketingRepo := rSalesOrders.NewMarketingRepository(db) - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, validate) + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) } - diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go new file mode 100644 index 00000000..bb4896cb --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/marketings.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 MarketingRepository interface { + repository.BaseRepository[entity.Marketing] +} + +type MarketingRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.Marketing] +} + +func NewMarketingRepository(db *gorm.DB) MarketingRepository { + return &MarketingRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db), + } +} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 33c93f6d..847305c1 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -3,9 +3,13 @@ package service import ( "errors" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" + customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" + kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" @@ -23,16 +27,24 @@ type SalesOrdersService interface { } type salesOrdersService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.SalesOrdersRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.SalesOrdersRepository + CustomerRepo customerRepo.CustomerRepository + KandangRepo kandangRepo.KandangRepository + ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository + MarketingRepo repository.MarketingRepository } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ - Log: utils.Log, - Validate: validate, - Repository: repo, + Log: utils.Log, + Validate: validate, + Repository: repo, + CustomerRepo: customerRepo, + KandangRepo: kandangRepo, + ProductWarehouseRepo: productWarehouseRepo, + MarketingRepo: marketingRepo, } } @@ -79,13 +91,40 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, err } - createBody := &entity.SalesOrders{ - Name: req.Name, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists}, + ); err != nil { + return nil, err } - if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { - s.Log.Errorf("Failed to create salesOrders: %+v", err) - return nil, err + for _, item := range req.MarketingProducts { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Kandang", ID: &item.KandangId, Exists: s.KandangRepo.IdExists}, + commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, + ); err != nil { + return nil, err + } + } + + createBody := &entity.Marketing{ + CustomerId: req.CustomerId, + } + + err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + + if err := marketingRepoTx.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create marketing: %+v", err) + return err + } + + return nil + }) + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } return s.GetOne(c, createBody.Id) diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go index 7d16d3ee..b09129bc 100644 --- a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -1,11 +1,25 @@ package validation type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` + CustomerId uint `json:"customer_id" validate:"required,gt=0"` + Date string `json:"date" validate:"required,datetime=2006-01-02"` + Notes string `json:"notes" validate:"omitempty,max=500"` + MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"` +} + +type CreateMarketingProduct struct { + VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` + KandangId uint `json:"kandang_id" validate:"required,gt=0"` + ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` + UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` + TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"required,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` + TotalPrice float64 `json:"total_price" validate:"required,gt=0"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + Name *string `json:"name,omitempty" validate:"omitempty"` } type Query struct { diff --git a/internal/modules/master/customers/repositories/customer.repository.go b/internal/modules/master/customers/repositories/customer.repository.go index 13a08211..68877548 100644 --- a/internal/modules/master/customers/repositories/customer.repository.go +++ b/internal/modules/master/customers/repositories/customer.repository.go @@ -12,6 +12,7 @@ type CustomerRepository interface { repository.BaseRepository[entity.Customer] PicExists(ctx context.Context, areaId uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) + IdExists(ctx context.Context, id uint) (bool, error) } type CustomerRepositoryImpl struct { @@ -33,3 +34,7 @@ func (r *CustomerRepositoryImpl) PicExists(ctx context.Context, picId uint) (boo func (r *CustomerRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { return repository.ExistsByName[entity.Customer](ctx, r.db, name, excludeID) } + +func (r *CustomerRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Customer](ctx, r.db, id) +} diff --git a/internal/modules/master/kandangs/repositories/kandang.repository.go b/internal/modules/master/kandangs/repositories/kandang.repository.go index 8f32a7b2..04b6a914 100644 --- a/internal/modules/master/kandangs/repositories/kandang.repository.go +++ b/internal/modules/master/kandangs/repositories/kandang.repository.go @@ -21,6 +21,7 @@ type KandangRepository interface { UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error + IdExists(ctx context.Context, id uint) (bool, error) } type KandangRepositoryImpl struct { @@ -59,6 +60,10 @@ func (r *KandangRepositoryImpl) ProjectFlockExists(ctx context.Context, projectF return count > 0, nil } +func (r *KandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Kandang](ctx, r.db, id) +} + func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) { var count int64 q := r.db.WithContext(ctx). From 6b5d27ae8e992e7eae86ffd414d6330b117d67c6 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 11 Nov 2025 12:16:39 +0700 Subject: [PATCH 30/59] feat[BE]: add flock response to project flock and projectflockkandang getone and getall API --- .../project_flock_kandang.controller.go | 13 ++- .../dto/project_flock_kandang.dto.go | 24 ++++- .../project-flock-kandangs/module.go | 4 +- .../services/project_flock_kandang.service.go | 62 ++++++++++--- .../controllers/projectflock.controller.go | 24 +++-- .../project_flocks/dto/projectflock.dto.go | 64 +++++++++----- .../services/projectflock.service.go | 87 ++++++++++++++++--- 7 files changed, 220 insertions(+), 58 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 94316dce..d65cedfd 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -4,6 +4,7 @@ import ( "math" "strconv" + flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" @@ -40,14 +41,18 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } - results, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) + results, totalResults, flockMap, err := u.ProjectFlockKandangService.GetAll(c, query) if err != nil { return err } data := make([]dto.ProjectFlockKandangListDTO, 0) for _, result := range results { - data = append(data, dto.ToProjectFlockKandangListDTO(result)) + var flock *flockDTO.FlockBaseDTO + if flockMap != nil { + flock = flockMap[result.ProjectFlock.Id] + } + data = append(data, dto.ToProjectFlockKandangListDTOWithFlock(result, flock)) } return c.Status(fiber.StatusOK). @@ -73,7 +78,7 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, availableQtys, productWarehouses, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) + result, availableQtys, productWarehouses, flock, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) if err != nil { return err } @@ -83,6 +88,6 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get projectFlockKandang successfully", - Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses), + Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(*result, availableQtys, productWarehouses, flock), }) } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 35bf615d..5d8e6628 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -87,6 +87,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO Id: pf.Id, Name: pf.FlockName, Period: pf.Period, + Flock: pf.Flock, Area: pf.Area, Category: pf.Category, Fcr: pf.Fcr, @@ -98,9 +99,13 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO } func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse) ProjectFlockKandangDetailDTO { + return ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e, availableQtyMap, productWarehouses, nil) +} + +func ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse, flock *flockDTO.FlockBaseDTO) ProjectFlockKandangDetailDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { - mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock, flock) projectFlockSummary = &mapped } @@ -132,6 +137,17 @@ func toKandangDTO(kandang entity.Kandang) *KandangDTO { } } +func toFlockDTO(flock *entity.Flock) *flockDTO.FlockBaseDTO { + if flock == nil || flock.Id == 0 { + return nil + } + + return &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } +} + func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { if e.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) @@ -141,9 +157,13 @@ func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { } func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKandangListDTO { + return ToProjectFlockKandangListDTOWithFlock(e, nil) +} + +func ToProjectFlockKandangListDTOWithFlock(e entity.ProjectFlockKandang, flock *flockDTO.FlockBaseDTO) ProjectFlockKandangListDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { - mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + mapped := projectFlockDTO.ToProjectFlockListDTOWithFlock(e.ProjectFlock, flock) projectFlockSummary = &mapped } diff --git a/internal/modules/production/project-flock-kandangs/module.go b/internal/modules/production/project-flock-kandangs/module.go index 160cec5e..3e7e8fa0 100644 --- a/internal/modules/production/project-flock-kandangs/module.go +++ b/internal/modules/production/project-flock-kandangs/module.go @@ -13,6 +13,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -28,6 +29,7 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB userRepo := rUser.NewUserRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + flockRepo := rFlock.NewFlockRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) @@ -36,7 +38,7 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err)) } - projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate) + projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, flockRepo, validate) userService := sUser.NewUserService(userRepo, validate) ProjectFlockKandangRoutes(router, userService, projectFlockKandangService) diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 11e8b0d5..349eca4d 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -6,9 +6,12 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" + flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" @@ -18,8 +21,8 @@ import ( ) type ProjectFlockKandangService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockBaseDTO, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockBaseDTO, error) } // Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity @@ -32,9 +35,10 @@ type projectFlockKandangService struct { WarehouseRepo rWarehouse.WarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository PopulationRepo repository.ProjectFlockPopulationRepository + FlockRepo flockRepository.FlockRepository } -func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService { +func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, flockRepo flockRepository.FlockRepository, validate *validator.Validate) ProjectFlockKandangService { return &projectFlockKandangService{ Log: utils.Log, Validate: validate, @@ -43,12 +47,13 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, PopulationRepo: populationRepo, + FlockRepo: flockRepo, } } -func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) { +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockBaseDTO, error) { if err := s.Validate.Struct(params); err != nil { - return nil, 0, err + return nil, 0, nil, err } offset := (params.Page - 1) * params.Limit @@ -57,7 +62,7 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) - return nil, 0, err + return nil, 0, nil, err } if s.ApprovalSvc != nil { @@ -80,16 +85,36 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } } - return projectFlockKandangs, total, nil + // Fetch Flock master data for each ProjectFlockKandang + flockMap := make(map[uint]*flockDTO.FlockBaseDTO) + for i := range projectFlockKandangs { + if projectFlockKandangs[i].ProjectFlock.Id != 0 { + baseName := pfutils.DeriveBaseName(projectFlockKandangs[i].ProjectFlock.FlockName) + if baseName != "" { + flock, err := s.FlockRepo.GetByName(c.Context(), baseName) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) + } else if flock != nil { + // Convert to DTO and store in map with ProjectFlockId as key + flockMap[projectFlockKandangs[i].ProjectFlock.Id] = &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } + } + } + } + } + + return projectFlockKandangs, total, flockMap, nil } -func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockBaseDTO, error) { projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + return nil, nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil { @@ -123,8 +148,23 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje productWarehouses = []entity.ProductWarehouse{} } } + var flockResult *flockDTO.FlockBaseDTO + if projectFlockKandang.ProjectFlock.Id != 0 { + baseName := pfutils.DeriveBaseName(projectFlockKandang.ProjectFlock.FlockName) + if baseName != "" { + flock, err := s.FlockRepo.GetByName(c.Context(), baseName) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) + } else if flock != nil { + flockResult = &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } + } + } + } - return projectFlockKandang, availableQtyMap, productWarehouses, nil + return projectFlockKandang, availableQtyMap, productWarehouses, flockResult, nil } func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) (map[uint]float64, error) { diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index cf2dff05..67b04f99 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" @@ -84,11 +85,20 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { query.KandangIds = ids } - result, totalResults, err := u.ProjectflockService.GetAll(c, query) + result, totalResults, flockMap, err := u.ProjectflockService.GetAll(c, query) if err != nil { return err } + data := make([]dto.ProjectFlockListDTO, 0) + for _, projectFlock := range result { + var flock *flockDTO.FlockBaseDTO + if flockMap != nil { + flock = flockMap[projectFlock.Id] + } + data = append(data, dto.ToProjectFlockListDTO(projectFlock, flock)) + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.ProjectFlockListDTO]{ Code: fiber.StatusOK, @@ -100,7 +110,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToProjectFlockListDTOs(result), + Data: data, }) } @@ -112,7 +122,7 @@ func (u *ProjectflockController) GetOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, err := u.ProjectflockService.GetOne(c, uint(id)) + result, flock, err := u.ProjectflockService.GetOne(c, uint(id)) if err != nil { return err } @@ -122,7 +132,7 @@ func (u *ProjectflockController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result), + Data: dto.ToProjectFlockListDTO(*result, flock), }) } @@ -143,7 +153,7 @@ func (u *ProjectflockController) CreateOne(c *fiber.Ctx) error { Code: fiber.StatusCreated, Status: "success", Message: "Create projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result), + Data: dto.ToProjectFlockListDTO(*result, nil), }) } @@ -170,7 +180,7 @@ func (u *ProjectflockController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result), + Data: dto.ToProjectFlockListDTO(*result, nil), }) } @@ -210,7 +220,7 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error { message = "Submit projectflock approval successfully" ) if len(results) == 1 { - data = dto.ToProjectFlockListDTO(results[0]) + data = dto.ToProjectFlockListDTO(results[0], nil) } else { message = "Submit projectflock approvals successfully" data = dto.ToProjectFlockListDTOs(results) diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index ce9be3d3..2230d865 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -25,7 +25,7 @@ type ProjectFlockBaseDTO struct { type ProjectFlockListDTO struct { ProjectFlockBaseDTO - // Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` + Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` Category string `json:"category"` Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` @@ -51,7 +51,7 @@ type FlockPeriodDTO struct { NextPeriod int `json:"next_period"` } -func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { +func ToProjectFlockListDTO(e entity.ProjectFlock, flock *flockDTO.FlockBaseDTO) ProjectFlockListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(e.CreatedUser) @@ -62,7 +62,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { if len(e.Kandangs) > 0 { kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs)) for i, kandang := range e.Kandangs { - // Find project_flock_kandang_id dari KandangHistory + var pfkId uint for _, kh := range e.KandangHistory { if kh.KandangId == kandang.Id { @@ -77,12 +77,6 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { } } - // var flockSummary *flockDTO.FlockBaseDTO - // if baseName := pfutils.DeriveBaseName(e.FlockName); baseName != "" { - // summary := flockDTO.FlockBaseDTO{Id: 0, Name: baseName} - // flockSummary = &summary - // } - var areaSummary *areaDTO.AreaBaseDTO if e.Area.Id != 0 { mapped := areaDTO.ToAreaBaseDTO(e.Area) @@ -101,6 +95,11 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { locationSummary = &mapped } + var flockSummary *flockDTO.FlockBaseDTO + if flock != nil && flock.Id != 0 { + flockSummary = flock + } + latestApproval := defaultProjectFlockLatestApproval(e) if e.LatestApproval != nil { snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval) @@ -109,30 +108,51 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { return ProjectFlockListDTO{ ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), - // Flock: flockSummary, - Area: areaSummary, - Kandangs: kandangSummaries, - Category: e.Category, - Fcr: fcrSummary, - Location: locationSummary, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Approval: latestApproval, + Flock: flockSummary, + Area: areaSummary, + Kandangs: kandangSummaries, + Category: e.Category, + Fcr: fcrSummary, + Location: locationSummary, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Approval: latestApproval, } } +func ToProjectFlockListDTOWithFlock(e entity.ProjectFlock, flock *flockDTO.FlockBaseDTO) ProjectFlockListDTO { + return ToProjectFlockListDTO(e, flock) +} + func ToProjectFlockListDTOs(items []entity.ProjectFlock) []ProjectFlockListDTO { result := make([]ProjectFlockListDTO, len(items)) for i, item := range items { - result[i] = ToProjectFlockListDTO(item) + result[i] = ToProjectFlockListDTO(item, nil) } return result } -func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO { +func ToProjectFlockListDTOsWithFlocks(items []entity.ProjectFlock, flocks map[uint]*entity.Flock) []ProjectFlockListDTO { + result := make([]ProjectFlockListDTO, len(items)) + for i, item := range items { + var flock *flockDTO.FlockBaseDTO + if flocks != nil { + if f := flocks[item.Id]; f != nil { + flock = &flockDTO.FlockBaseDTO{ + Id: f.Id, + Name: f.Name, + } + } + } + result[i] = ToProjectFlockListDTOWithFlock(item, flock) + } + return result +} + +func ToProjectFlockDetailDTO(e entity.ProjectFlock, flock *flockDTO.FlockBaseDTO) ProjectFlockDetailDTO { return ProjectFlockDetailDTO{ - ProjectFlockListDTO: ToProjectFlockListDTO(e), + ProjectFlockListDTO: ToProjectFlockListDTO(e, flock), } } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 9b5b4750..715fb1fb 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -11,6 +11,7 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" @@ -27,8 +28,8 @@ import ( ) type ProjectflockService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockBaseDTO, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockBaseDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) @@ -92,9 +93,9 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { Preload("KandangHistory.Kandang") } -func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { +func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockBaseDTO, error) { if err := s.Validate.Struct(params); err != nil { - return nil, 0, err + return nil, 0, nil, err } if params.Page <= 0 { @@ -110,7 +111,7 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e if err != nil { s.Log.Errorf("Failed to get projectflocks: %+v", err) - return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks") + return nil, 0, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks") } if s.ApprovalSvc != nil && len(projectflocks) > 0 { @@ -133,10 +134,28 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } } - return projectflocks, total, nil + flockMap := make(map[uint]*flockDTO.FlockBaseDTO) + for i := range projectflocks { + if projectflocks[i].FlockName != "" { + baseName := pfutils.DeriveBaseName(projectflocks[i].FlockName) + if baseName != "" { + flock, err := s.FlockRepo.GetByName(c.Context(), baseName) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) + } else if flock != nil { + flockMap[projectflocks[i].Id] = &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } + } + } + } + } + + return projectflocks, total, flockMap, nil } -func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { +func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") @@ -165,6 +184,52 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock return projectflock, nil } +func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockBaseDTO, error) { + projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + } + if err != nil { + s.Log.Errorf("Failed get projectflock by id: %+v", err) + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + 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 + } + } + + // Fetch Flock master data for this ProjectFlock + var flockResult *flockDTO.FlockBaseDTO + if projectflock.FlockName != "" { + baseName := pfutils.DeriveBaseName(projectflock.FlockName) + if baseName != "" { + flock, err := s.FlockRepo.GetByName(c.Context(), baseName) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) + } else if flock != nil { + flockResult = &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } + } + } + } + + return projectflock, flockResult, nil +} + func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) { if err := s.Validate.Struct(req); err != nil { return nil, err @@ -275,7 +340,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project flock") } - return s.GetOne(c, createBody.Id) + return s.getOneEntityOnly(c, createBody.Id) } func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) { @@ -387,7 +452,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id hasChanges := hasBodyChanges || hasKandangChanges if !hasChanges { - return s.GetOne(c, id) + return s.getOneEntityOnly(c, id) } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { @@ -510,7 +575,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock") } - return s.GetOne(c, id) + return s.getOneEntityOnly(c, id) } func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) { @@ -600,7 +665,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([] updated := make([]entity.ProjectFlock, 0, len(approvableIDs)) for _, approvableID := range approvableIDs { - project, err := s.GetOne(c, approvableID) + project, err := s.getOneEntityOnly(c, approvableID) if err != nil { return nil, err } From 762dfa9fb9e7cdde0090b8a8e768aca8a014ef5f Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 11 Nov 2025 14:32:55 +0700 Subject: [PATCH 31/59] feat[BE-127] add source and target project flock to transfer laying API --- .../services/sales-orders.service.go | 72 +++++++++++++++++-- .../services/project_flock_kandang.service.go | 3 +- .../dto/transfer_laying.dto.go | 33 ++++++--- .../services/transfer_laying.service.go | 4 ++ 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 847305c1..0a1e5898 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -2,6 +2,7 @@ package service import ( "errors" + "time" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -106,18 +107,68 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } } - createBody := &entity.Marketing{ - CustomerId: req.CustomerId, + // parse date + soDate, err := utils.ParseDateString(req.Date) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + // generate SO number + soNumber := "SO-" + time.Now().Format("20060102150405") - if err := marketingRepoTx.CreateOne(c.Context(), createBody, nil); err != nil { + var marketing *entity.Marketing + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + // create marketing directly (tanpa create SalesOrders) + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + marketing = &entity.Marketing{ + CustomerId: req.CustomerId, + SoNumber: soNumber, + SoDate: soDate, + SalesPersonId: 1, + Notes: req.Notes, + CreatedBy: 1, + } + if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { s.Log.Errorf("Failed to create marketing: %+v", err) return err } + // Create MarketingProducts and MarketingDeliveryProducts + if len(req.MarketingProducts) > 0 { + for _, product := range req.MarketingProducts { + marketingProduct := &entity.MarketingProduct{ + MarketingId: marketing.Id, + ProductWarehouseId: product.ProductWarehouseId, + Qty: product.Qty, + UnitPrice: product.UnitPrice, + AvgWeight: product.AvgWeight, + TotalWeight: product.TotalWeight, + TotalPrice: product.TotalPrice, + } + if err := dbTransaction.Create(marketingProduct).Error; err != nil { + s.Log.Errorf("Failed to create marketing product: %+v", err) + return err + } + + // create delivery product with zeroed numeric fields and nil delivery date + marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ + MarketingProductId: marketingProduct.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: product.VehicleNumber, + } + if err := dbTransaction.Create(marketingDeliveryProduct).Error; err != nil { + s.Log.Errorf("Failed to create marketing delivery product: %+v", err) + return err + } + } + } + return nil }) if err != nil { @@ -127,7 +178,16 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } - return s.GetOne(c, createBody.Id) + createdMarketing, err := s.MarketingRepo.GetByID(c.Context(), marketing.Id, nil) + if err != nil { + s.Log.Errorf("Failed to fetch created marketing: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created marketing") + } + + return &entity.SalesOrders{ + Id: createdMarketing.Id, + Name: createdMarketing.SoNumber, + }, nil } func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) { diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 349eca4d..ec61a7e4 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -85,7 +85,6 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } } - // Fetch Flock master data for each ProjectFlockKandang flockMap := make(map[uint]*flockDTO.FlockBaseDTO) for i := range projectFlockKandangs { if projectFlockKandangs[i].ProjectFlock.Id != 0 { @@ -95,7 +94,7 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) } else if flock != nil { - // Convert to DTO and store in map with ProjectFlockId as key + flockMap[projectFlockKandangs[i].ProjectFlock.Id] = &flockDTO.FlockBaseDTO{ Id: flock.Id, Name: flock.Name, diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 98c1a2e7..ef537e19 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -18,9 +18,9 @@ type TransferLayingBaseDTO struct { } type ProjectFlockSummaryDTO struct { - Id uint `json:"id"` - Period int `json:"period"` - Category string `json:"category"` + Id uint `json:"id"` + FlockName string `json:"flock_name"` + Category string `json:"category"` } type ProductSummaryDTO struct { @@ -40,8 +40,13 @@ type ProductWarehouseSummaryDTO struct { } type ProjectFlockKandangSummaryDTO struct { - Id uint `json:"id"` - KandangId uint `json:"kandang_id"` + Id uint `json:"id"` + Kandang *KandangSummaryDTO `json:"kandang,omitempty"` +} + +type KandangSummaryDTO struct { + Id uint `json:"id"` + Name string `json:"name"` } type LayingTransferSourceDTO struct { @@ -99,9 +104,9 @@ func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { } return &ProjectFlockSummaryDTO{ - Id: pf.Id, - Period: pf.Period, - Category: pf.Category, + Id: pf.Id, + FlockName: pf.FlockName, + Category: pf.Category, } } @@ -110,9 +115,17 @@ func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFl return nil } + var kandang *KandangSummaryDTO + if pfk.Kandang.Id != 0 { + kandang = &KandangSummaryDTO{ + Id: pfk.Kandang.Id, + Name: pfk.Kandang.Name, + } + } + return &ProjectFlockKandangSummaryDTO{ - Id: pfk.Id, - KandangId: pfk.KandangId, + Id: pfk.Id, + Kandang: kandang, } } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index e7591aef..2aa7129c 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -72,13 +72,17 @@ func NewTransferLayingService( func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). + Preload("FromProjectFlock"). + Preload("ToProjectFlock"). Preload("Sources"). Preload("Sources.SourceProjectFlockKandang"). + Preload("Sources.SourceProjectFlockKandang.Kandang"). Preload("Sources.ProductWarehouse"). Preload("Sources.ProductWarehouse.Product"). Preload("Sources.ProductWarehouse.Warehouse"). Preload("Targets"). Preload("Targets.TargetProjectFlockKandang"). + Preload("Targets.TargetProjectFlockKandang.Kandang"). Preload("Targets.ProductWarehouse"). Preload("Targets.ProductWarehouse.Product"). Preload("Targets.ProductWarehouse.Warehouse") From 0a0c3f869bcf394e0b6385e8ec62575140c95a34 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 12 Nov 2025 11:28:18 +0700 Subject: [PATCH 32/59] Feat[BE-222,223,224]: creating So create delete patch update get getall approval API --- internal/entities/delivery-orders.go | 23 + internal/entities/marketing.go | 9 +- internal/entities/marketing_product.go | 5 +- internal/entities/sales-orders.go | 3 +- .../marketing-delivery-products.repository.go | 21 + .../controllers/delivery-orders.controller.go | 165 +++++++ .../dto/delivery-orders.dto.go | 164 +++++++ .../marketing/delivery-orderss/module.go | 32 ++ .../delivery-orders.repository.go | 21 + .../marketing/delivery-orderss/route.go | 29 ++ .../services/delivery-orders.service.go | 412 ++++++++++++++++ .../validations/delivery-orders.validation.go | 37 ++ internal/modules/marketing/route.go | 2 + .../controllers/sales-orders.controller.go | 52 +- .../sales-orders/dto/sales-orders.dto.go | 223 ++++++++- .../modules/marketing/sales-orders/module.go | 5 +- .../marketing-delivery-products.repository.go | 21 + .../marketing-products.repository.go | 35 ++ .../repositories/marketings.repository.go | 7 + .../modules/marketing/sales-orders/route.go | 1 + .../services/sales-orders.service.go | 464 +++++++++++++++--- .../validations/sales-orders.validation.go | 13 +- .../users/repositories/user.repository.go | 9 + internal/utils/constant.go | 17 + 24 files changed, 1688 insertions(+), 82 deletions(-) create mode 100644 internal/entities/delivery-orders.go create mode 100644 internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go create mode 100644 internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go create mode 100644 internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go create mode 100644 internal/modules/marketing/delivery-orderss/module.go create mode 100644 internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go create mode 100644 internal/modules/marketing/delivery-orderss/route.go create mode 100644 internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go create mode 100644 internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go create mode 100644 internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go create mode 100644 internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go diff --git a/internal/entities/delivery-orders.go b/internal/entities/delivery-orders.go new file mode 100644 index 00000000..291ba20c --- /dev/null +++ b/internal/entities/delivery-orders.go @@ -0,0 +1,23 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type DeliveryOrders struct { + Id uint `gorm:"primaryKey" json:"id"` + DeliveryNumber string `gorm:"type:varchar(255);not null;uniqueIndex" json:"delivery_number"` + DeliveryDate time.Time `gorm:"not null" json:"delivery_date"` + MarketingId uint `gorm:"not null" json:"marketing_id"` + Notes string `gorm:"type:text" json:"notes"` + CreatedBy uint `gorm:"not null" json:"created_by"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Marketing *Marketing `gorm:"foreignKey:MarketingId;references:Id" json:"marketing,omitempty"` + DeliveryProducts []MarketingDeliveryProduct `gorm:"-" json:"delivery_products,omitempty"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id" json:"created_user,omitempty"` +} diff --git a/internal/entities/marketing.go b/internal/entities/marketing.go index 1ae4d8c3..c9ff7624 100644 --- a/internal/entities/marketing.go +++ b/internal/entities/marketing.go @@ -19,8 +19,9 @@ type Marketing struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Customer Customer `gorm:"foreignKey:CustomerId;references:Id"` - SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"` + Customer Customer `gorm:"foreignKey:CustomerId;references:Id"` + SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"` + LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` } diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go index 2e6aef58..f0fe7f38 100644 --- a/internal/entities/marketing_product.go +++ b/internal/entities/marketing_product.go @@ -19,6 +19,7 @@ type MarketingProduct struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` - ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` + ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + DeliveryProducts []MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"` } diff --git a/internal/entities/sales-orders.go b/internal/entities/sales-orders.go index 48615607..faa6d901 100644 --- a/internal/entities/sales-orders.go +++ b/internal/entities/sales-orders.go @@ -14,5 +14,6 @@ type SalesOrders struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` } diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go new file mode 100644 index 00000000..95e9b3bb --- /dev/null +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.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 MarketingDeliveryProductRepository interface { + repository.BaseRepository[entity.MarketingDeliveryProduct] +} + +type MarketingDeliveryProductRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct] +} + +func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository { + return &MarketingDeliveryProductRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db), + } +} diff --git a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go new file mode 100644 index 00000000..a5f2839a --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go @@ -0,0 +1,165 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type DeliveryOrdersController struct { + DeliveryOrdersService service.DeliveryOrdersService +} + +func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersService) *DeliveryOrdersController { + return &DeliveryOrdersController{ + DeliveryOrdersService: deliveryOrdersService, + } +} + +func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + MarketingId: uint(c.QueryInt("marketing_id", 0)), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.DeliveryOrdersService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.DeliveryOrdersListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all deliveryOrderss successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: result, + }) +} + +func (u *DeliveryOrdersController) 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.DeliveryOrdersService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get deliveryOrders successfully", + Data: *result, + }) +} + +func (u *DeliveryOrdersController) 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.DeliveryOrdersService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create delivery products successfully", + Data: result, + }) +} + +func (u *DeliveryOrdersController) 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.DeliveryOrdersService.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 deliveryOrders successfully", + Data: dto.ToDeliveryOrdersListDTO(*result), + }) +} + +func (u *DeliveryOrdersController) 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.DeliveryOrdersService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete deliveryOrders successfully", + }) +} + +func (u *DeliveryOrdersController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + results, err := u.DeliveryOrdersService.Approval(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Submit delivery order approval successfully", + Data: results, + }) +} diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go new file mode 100644 index 00000000..2b2ea51e --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -0,0 +1,164 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type MarketingDeliveryProductDTO struct { + Id uint `json:"id"` + MarketingProductId uint `json:"marketing_product_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` +} + +type DeliveryOrdersBaseDTO struct { + Id uint `json:"id,omitempty"` + DeliveryNumber *string `json:"delivery_number,omitempty"` + DeliveryDate *time.Time `json:"delivery_date,omitempty"` + MarketingId uint `json:"marketing_id"` + Notes string `json:"notes,omitempty"` +} + +type MarketingBaseDTO struct { + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` +} + +type DeliveryOrdersListDTO struct { + DeliveryOrdersBaseDTO + Marketing *MarketingBaseDTO `json:"marketing,omitempty"` + DeliveryProducts []MarketingDeliveryProductDTO `json:"delivery_products,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` +} + +type DeliveryOrdersDetailDTO struct { + DeliveryOrdersListDTO +} + +// === Mapper Functions === + +func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { + return MarketingDeliveryProductDTO{ + Id: e.Id, + MarketingProductId: e.MarketingProductId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + TotalWeight: e.TotalWeight, + AvgWeight: e.AvgWeight, + TotalPrice: e.TotalPrice, + DeliveryDate: e.DeliveryDate, + VehicleNumber: e.VehicleNumber, + } +} + +func ToDeliveryOrdersBaseDTO(e entity.DeliveryOrders) DeliveryOrdersBaseDTO { + var deliveryNumber *string + if e.DeliveryNumber != "" { + deliveryNumber = &e.DeliveryNumber + } + + return DeliveryOrdersBaseDTO{ + Id: e.Id, + DeliveryNumber: deliveryNumber, + DeliveryDate: &e.DeliveryDate, + MarketingId: e.MarketingId, + Notes: e.Notes, + } +} + +func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + + var marketing *MarketingBaseDTO + if e.Marketing != nil && e.Marketing.Id != 0 { + marketing = &MarketingBaseDTO{ + Id: e.Marketing.Id, + SoNumber: e.Marketing.SoNumber, + SoDate: e.Marketing.SoDate, + } + } + + var deliveryProductsDTOs []MarketingDeliveryProductDTO + if len(e.DeliveryProducts) > 0 { + deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(e.DeliveryProducts)) + for i, dp := range e.DeliveryProducts { + deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) + } + } + + return DeliveryOrdersListDTO{ + DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), + Marketing: marketing, + DeliveryProducts: deliveryProductsDTOs, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProducts []entity.MarketingDeliveryProduct) DeliveryOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + + var marketing *MarketingBaseDTO + if e.Marketing != nil && e.Marketing.Id != 0 { + marketing = &MarketingBaseDTO{ + Id: e.Marketing.Id, + SoNumber: e.Marketing.SoNumber, + SoDate: e.Marketing.SoDate, + } + } + + var deliveryProductsDTOs []MarketingDeliveryProductDTO + if len(deliveryProducts) > 0 { + deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(deliveryProducts)) + for i, dp := range deliveryProducts { + deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) + } + } + + return DeliveryOrdersListDTO{ + DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), + Marketing: marketing, + DeliveryProducts: deliveryProductsDTOs, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToDeliveryOrdersListDTOs(e []entity.DeliveryOrders) []DeliveryOrdersListDTO { + result := make([]DeliveryOrdersListDTO, len(e)) + for i, r := range e { + result[i] = ToDeliveryOrdersListDTO(r) + } + return result +} + +func ToDeliveryOrdersDetailDTO(e entity.DeliveryOrders) DeliveryOrdersDetailDTO { + return DeliveryOrdersDetailDTO{ + DeliveryOrdersListDTO: ToDeliveryOrdersListDTO(e), + } +} diff --git a/internal/modules/marketing/delivery-orderss/module.go b/internal/modules/marketing/delivery-orderss/module.go new file mode 100644 index 00000000..c6932c51 --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/module.go @@ -0,0 +1,32 @@ +package delivery_orderss + +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" + rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" + rDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" + sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" + rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type DeliveryOrdersModule struct{} + +func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + deliveryOrdersRepo := rDeliveryOrders.NewDeliveryOrdersRepository(db) + marketingRepo := rMarketing.NewMarketingRepository(db) + marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db) + userRepo := rUser.NewUserRepository(db) + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalSvc := commonSvc.NewApprovalService(approvalRepo) + + deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingDeliveryProductRepo, approvalSvc, validate) + userService := sUser.NewUserService(userRepo, validate) + + DeliveryOrdersRoutes(router, userService, deliveryOrdersService) +} diff --git a/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go b/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go new file mode 100644 index 00000000..0a7d7f3d --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type DeliveryOrdersRepository interface { + repository.BaseRepository[entity.DeliveryOrders] +} + +type DeliveryOrdersRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.DeliveryOrders] +} + +func NewDeliveryOrdersRepository(db *gorm.DB) DeliveryOrdersRepository { + return &DeliveryOrdersRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.DeliveryOrders](db), + } +} diff --git a/internal/modules/marketing/delivery-orderss/route.go b/internal/modules/marketing/delivery-orderss/route.go new file mode 100644 index 00000000..8d58b70e --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/route.go @@ -0,0 +1,29 @@ +package delivery_orderss + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers" + deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) { + ctrl := controller.NewDeliveryOrdersController(s) + + route := v1.Group("/delivery-orders") + + // 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("/approvals", ctrl.Approval) +} diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go new file mode 100644 index 00000000..db238d0f --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -0,0 +1,412 @@ +package service + +import ( + "errors" + "fmt" + "time" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" + "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations" + marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/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 DeliveryOrdersService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) + DeleteOne(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) +} + +type deliveryOrdersService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.DeliveryOrdersRepository + MarketingRepo marketingRepo.MarketingRepository + MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository + ApprovalSvc commonSvc.ApprovalService +} + +func NewDeliveryOrdersService( + repo repository.DeliveryOrdersRepository, + marketingRepo marketingRepo.MarketingRepository, + marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository, + approvalSvc commonSvc.ApprovalService, + validate *validator.Validate, +) DeliveryOrdersService { + return &deliveryOrdersService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + MarketingRepo: marketingRepo, + MarketingDeliveryProductRepo: marketingDeliveryProductRepo, + ApprovalSvc: approvalSvc, + } +} + +func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser"). + Preload("Marketing") +} + +func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + // Fetch dari Marketing, bukan DeliveryOrders + marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = db.Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse") + if params.MarketingId != 0 { + return db.Where("id = ?", params.MarketingId) + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get marketings: %+v", err) + return nil, 0, err + } + + // Load delivery products untuk setiap marketing + result := make([]dto.DeliveryOrdersListDTO, len(marketings)) + for i, marketing := range marketings { + // Get marketing delivery products + var deliveryProducts []entity.MarketingDeliveryProduct + if err := s.Repository.DB().WithContext(c.Context()). + Preload("MarketingProduct"). + Where("marketing_product_id IN (?)", + s.Repository.DB().WithContext(c.Context()). + Model(&entity.MarketingProduct{}). + Select("id"). + Where("marketing_id = ?", marketing.Id)). + Find(&deliveryProducts).Error; err != nil { + s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) + // Continue without products + } + + // Create dummy DeliveryOrders untuk dto mapping + dummyDO := &entity.DeliveryOrders{ + MarketingId: marketing.Id, + CreatedUser: &marketing.CreatedUser, + Marketing: &marketing, + DeliveryProducts: deliveryProducts, + } + + result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + } + + return result, total, nil +} + +func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) { + // Fetch Marketing by ID, bukan DeliveryOrders + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse") + }) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + if err != nil { + s.Log.Errorf("Failed get marketing by id: %+v", err) + return nil, err + } + + // Get marketing delivery products + var deliveryProducts []entity.MarketingDeliveryProduct + if err := s.Repository.DB().WithContext(c.Context()). + Preload("MarketingProduct"). + Where("marketing_product_id IN (?)", + s.Repository.DB().WithContext(c.Context()). + Model(&entity.MarketingProduct{}). + Select("id"). + Where("marketing_id = ?", marketing.Id)). + Find(&deliveryProducts).Error; err != nil { + s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) + // Continue without products + } + + // Create dummy DeliveryOrders untuk dto mapping + dummyDO := &entity.DeliveryOrders{ + MarketingId: marketing.Id, + CreatedUser: &marketing.CreatedUser, + Marketing: marketing, + DeliveryProducts: deliveryProducts, + } + + result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + return &result, nil +} + +func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + // Validate marketing exists + _, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + if err != nil { + s.Log.Errorf("Failed to fetch marketing %d: %+v", req.MarketingId, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } + + // Validate marketing approval status - harus sudah di approve ke step Sales Order + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + if latestApproval == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval") + } + + // Cek apakah status approval sudah Sales Order (step 2) atau lebih + if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") + } + + if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { + return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing is not approved for delivery") + } + + // Validate semua delivery products ada dan update mereka + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { + for _, product := range req.DeliveryProducts { + // Fetch marketing_product terlebih dahulu untuk pastikan punya marketing_id yang sama + var marketingProduct entity.MarketingProduct + if err := dbTx.Where("id = ? AND marketing_id = ?", product.MarketingProductId, req.MarketingId). + First(&marketingProduct).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", product.MarketingProductId)) + } + s.Log.Errorf("Failed to fetch marketing product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing product") + } + + // Fetch marketing_delivery_product by marketing_product_id + var mdp entity.MarketingDeliveryProduct + if err := dbTx.Where("marketing_product_id = ?", marketingProduct.Id). + First(&mdp).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", product.MarketingProductId)) + } + s.Log.Errorf("Failed to fetch marketing delivery product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") + } + + // Parse delivery date per item (jika ada), atau gunakan default + itemDeliveryDate := time.Now() + if product.DeliveryDate != "" { + parsedItemDate, err := time.Parse("2006-01-02", product.DeliveryDate) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d", product.MarketingProductId)) + } + itemDeliveryDate = parsedItemDate + } + + // Update dengan data dari request - PASTIKAN UPDATE LANGSUNG KE FIELD + updates := map[string]interface{}{ + "qty": product.Qty, + "unit_price": product.UnitPrice, + "avg_weight": product.AvgWeight, + "total_weight": product.TotalWeight, + "total_price": product.TotalPrice, + "delivery_date": &itemDeliveryDate, + "vehicle_number": product.VehicleNumber, + } + + if err := dbTx.Model(&mdp).Updates(updates).Error; err != nil { + s.Log.Errorf("Failed to update marketing delivery product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + } + + s.Log.Infof("Updated MDP %d: qty=%v, unitPrice=%v, totalPrice=%v", mdp.Id, product.Qty, product.UnitPrice, product.TotalPrice) + } + + return nil + }) + + if err != nil { + return nil, err + } + + // Fetch marketing dengan delivery products yang sudah di-update + marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser").Preload("Products") + }) + if err != nil { + s.Log.Errorf("Failed to fetch marketing after update: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") + } + + // Get marketing delivery products + var deliveryProducts []entity.MarketingDeliveryProduct + if err := s.Repository.DB().WithContext(c.Context()). + Preload("MarketingProduct"). + Where("marketing_product_id IN (?)", + s.Repository.DB().WithContext(c.Context()). + Model(&entity.MarketingProduct{}). + Select("id"). + Where("marketing_id = ?", req.MarketingId)). + Find(&deliveryProducts).Error; err != nil { + s.Log.Errorf("Failed to load delivery products: %+v", err) + // Continue tanpa delivery products + } + + // Create dummy DeliveryOrders untuk dipakai dto mapping + dummyDO := &entity.DeliveryOrders{ + MarketingId: req.MarketingId, + Notes: req.Notes, + CreatedUser: &marketing.CreatedUser, + Marketing: marketing, + DeliveryProducts: deliveryProducts, + } + + result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + return &result, nil +} + +func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.DeliveryDate != "" { + deliveryDate, err := time.Parse("2006-01-02", req.DeliveryDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid delivery date format") + } + updateBody["delivery_date"] = deliveryDate + } + + if req.Notes != "" { + updateBody["notes"] = req.Notes + } + + if len(updateBody) == 0 { + return s.Repository.GetByID(c.Context(), id, s.withRelations) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") + } + s.Log.Errorf("Failed to update deliveryOrders: %+v", err) + return nil, err + } + + return s.Repository.GetByID(c.Context(), id, s.withRelations) +} + +func (s deliveryOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + var action entity.ApprovalAction + switch req.Action { + case "APPROVED": + action = entity.ApprovalActionApproved + case "REJECTED": + action = entity.ApprovalActionRejected + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") + } + + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + // Validate semua delivery order ada + for _, id := range approvableIDs { + _, err := s.Repository.GetByID(c.Context(), id, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) + } + if err != nil { + s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) + } + } + + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTx)) + + for _, approvableID := range approvableIDs { + actorID := uint(1) // TODO: get from auth context + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + approvableID, + utils.MarketingDeliveryOrder, + &action, + actorID, + req.Notes, + ); err != nil { + s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") + } + } + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + updated := make([]entity.DeliveryOrders, 0, len(approvableIDs)) + for _, id := range approvableIDs { + deliveryOrder, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) + } + s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) + } + updated = append(updated, *deliveryOrder) + } + + return updated, nil +} + +func (s deliveryOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") + } + s.Log.Errorf("Failed to delete deliveryOrders: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go new file mode 100644 index 00000000..a80ad8d5 --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go @@ -0,0 +1,37 @@ +package validation + +type DeliveryProduct struct { + MarketingProductId uint `json:"marketing_product_id" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"required,gt=0"` + UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` + TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` + TotalPrice float64 `json:"total_price" validate:"required,gt=0"` + DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` + VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"` +} + +type Create struct { + MarketingId uint `json:"marketing_id" validate:"required,gt=0"` + DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` + DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"` + Notes string `json:"notes" validate:"omitempty,max=500"` +} + +type Update struct { + DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` + DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"` + Notes string `json:"notes" validate:"omitempty,max=500"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"` +} + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/modules/marketing/route.go b/internal/modules/marketing/route.go index 1b72b8cb..1ab03896 100644 --- a/internal/modules/marketing/route.go +++ b/internal/modules/marketing/route.go @@ -8,6 +8,7 @@ import ( "gorm.io/gorm" salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders" + deliveryOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss" // MODULE IMPORTS ) @@ -16,6 +17,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida allModules := []modules.Module{ salesOrderss.SalesOrdersModule{}, + deliveryOrderss.DeliveryOrdersModule{}, // MODULE REGISTRY } diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index be578e15..4c49baeb 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -38,6 +38,12 @@ func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { return err } + // Convert marketing data to sales orders DTOs with products + salesOrdersDTOs := make([]dto.SalesOrdersListDTO, len(result)) + for i, marketing := range result { + salesOrdersDTOs[i] = dto.ToSalesOrdersListDTOFromMarketing(marketing) + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.SalesOrdersListDTO]{ Code: fiber.StatusOK, @@ -49,7 +55,7 @@ func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToSalesOrdersListDTOs(result), + Data: salesOrdersDTOs, }) } @@ -71,7 +77,7 @@ func (u *SalesOrdersController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get salesOrders successfully", - Data: dto.ToSalesOrdersListDTO(*result), + Data: dto.ToSalesOrdersListDTOFromMarketing(*result), }) } @@ -109,7 +115,13 @@ func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - result, err := u.SalesOrdersService.UpdateOne(c, req, uint(id)) + _, err = u.SalesOrdersService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + // Fetch full updated data for response + result, err := u.SalesOrdersService.GetOne(c, uint(id)) if err != nil { return err } @@ -119,7 +131,7 @@ func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update salesOrders successfully", - Data: dto.ToSalesOrdersListDTO(*result), + Data: dto.ToSalesOrdersListDTOFromMarketing(*result), }) } @@ -142,3 +154,35 @@ func (u *SalesOrdersController) DeleteOne(c *fiber.Ctx) error { Message: "Delete salesOrders successfully", }) } + +func (u *SalesOrdersController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + results, err := u.SalesOrdersService.Approval(c, req) + if err != nil { + return err + } + + var ( + data interface{} + message = "Submit sales order approval successfully" + ) + if len(results) == 1 { + data = dto.ToSalesOrdersListDTOFromMarketing(results[0]) + } else { + message = "Submit sales order approvals successfully" + data = dto.ToSalesOrdersListDTOsFromMarketing(results) + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: message, + Data: data, + }) +} diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 6dd7c8e3..920b8d26 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -4,21 +4,60 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/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" ) // === DTO Structs === type SalesOrdersBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint `json:"id"` + Name string `json:"name"` +} + +type MarketingProductDTO struct { + Id uint `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ProductWarehouse *struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + } `json:"product_warehouse,omitempty"` +} + +type MarketingDeliveryProductDTO struct { + Id uint `json:"id"` + MarketingProductId uint `json:"marketing_product_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` } type SalesOrdersListDTO struct { SalesOrdersBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CustomerId uint `json:"customer_id,omitempty"` + Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` + SoDate *time.Time `json:"so_date,omitempty"` + SalesPersonId uint `json:"sales_person_id,omitempty"` + Notes string `json:"notes,omitempty"` + MarketingProducts []MarketingProductDTO `json:"marketing_products,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 SalesOrdersDetailDTO struct { @@ -29,11 +68,92 @@ type SalesOrdersDetailDTO struct { func ToSalesOrdersBaseDTO(e entity.SalesOrders) SalesOrdersBaseDTO { return SalesOrdersBaseDTO{ - Id: e.Id, - Name: e.Name, + Id: e.Id, + Name: e.Name, } } +func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { + var productWarehouse *struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + } + + if e.ProductWarehouse.Id != 0 { + product := (*productDTO.ProductBaseDTO)(nil) + if e.ProductWarehouse.Product.Id != 0 { + mapped := productDTO.ToProductBaseDTO(e.ProductWarehouse.Product) + product = &mapped + } + + warehouse := (*warehouseDTO.WarehouseBaseDTO)(nil) + if e.ProductWarehouse.Warehouse.Id != 0 { + mapped := warehouseDTO.ToWarehouseBaseDTO(e.ProductWarehouse.Warehouse) + warehouse = &mapped + } + + productWarehouse = &struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + }{ + Id: e.ProductWarehouse.Id, + Product: product, + Warehouse: warehouse, + } + } + + return MarketingProductDTO{ + Id: e.Id, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + AvgWeight: e.AvgWeight, + TotalWeight: e.TotalWeight, + TotalPrice: e.TotalPrice, + ProductWarehouse: productWarehouse, + } +} + +func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { + return MarketingDeliveryProductDTO{ + Id: e.Id, + MarketingProductId: e.MarketingProductId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + TotalWeight: e.TotalWeight, + AvgWeight: e.AvgWeight, + TotalPrice: e.TotalPrice, + DeliveryDate: e.DeliveryDate, + VehicleNumber: e.VehicleNumber, + } +} + +func defaultSalesOrdersLatestApproval(e entity.SalesOrders) approvalDTO.ApprovalBaseDTO { + result := approvalDTO.ApprovalBaseDTO{} + + step := utils.MarketingStepPengajuan + if step > 0 { + result.StepNumber = uint16(step) + if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { + result.StepName = label + } else if label, ok := utils.MarketingApprovalSteps[step]; ok { + result.StepName = label + } + } + + 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 ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { @@ -41,14 +161,97 @@ func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { createdUser = &mapped } + approval := defaultSalesOrdersLatestApproval(e) + if e.LatestApproval != nil { + approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) + } + return SalesOrdersListDTO{ SalesOrdersBaseDTO: ToSalesOrdersBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Approval: approval, } } +func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + var marketingProducts []MarketingProductDTO + if len(e.Products) > 0 { + marketingProducts = make([]MarketingProductDTO, len(e.Products)) + for i, product := range e.Products { + marketingProducts[i] = ToMarketingProductDTO(product) + } + } + + var customerSummary *customerDTO.CustomerBaseDTO + if e.Customer.Id != 0 { + mapped := customerDTO.ToCustomerBaseDTO(e.Customer) + customerSummary = &mapped + } + + approval := defaultSalesOrdersLatestApprovalFromMarketing(e) + if e.LatestApproval != nil { + approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) + } + + return SalesOrdersListDTO{ + SalesOrdersBaseDTO: SalesOrdersBaseDTO{ + Id: e.Id, + Name: e.SoNumber, + }, + CustomerId: e.Customer.Id, + Customer: customerSummary, + SoDate: &e.SoDate, + SalesPersonId: e.SalesPersonId, + Notes: e.Notes, + MarketingProducts: marketingProducts, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Approval: approval, + } +} + +func defaultSalesOrdersLatestApprovalFromMarketing(e entity.Marketing) approvalDTO.ApprovalBaseDTO { + result := approvalDTO.ApprovalBaseDTO{} + + step := utils.MarketingStepPengajuan + if step > 0 { + result.StepNumber = uint16(step) + if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { + result.StepName = label + } else if label, ok := utils.MarketingApprovalSteps[step]; ok { + result.StepName = label + } + } + + 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 ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListDTO { + result := make([]SalesOrdersListDTO, len(e)) + for i, r := range e { + result[i] = ToSalesOrdersListDTOFromMarketing(r) + } + return result +} + func ToSalesOrdersListDTOs(e []entity.SalesOrders) []SalesOrdersListDTO { result := make([]SalesOrdersListDTO, len(e)) for i, r := range e { diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 21926f93..35d002fc 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -5,6 +5,8 @@ import ( "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" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" @@ -25,7 +27,8 @@ func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valida productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) marketingRepo := rSalesOrders.NewMarketingRepository(db) - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, validate) + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) diff --git a/internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go b/internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go new file mode 100644 index 00000000..95e9b3bb --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.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 MarketingDeliveryProductRepository interface { + repository.BaseRepository[entity.MarketingDeliveryProduct] +} + +type MarketingDeliveryProductRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct] +} + +func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository { + return &MarketingDeliveryProductRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db), + } +} diff --git a/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go b/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go new file mode 100644 index 00000000..d3a6798f --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/marketing-products.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 MarketingProductRepository interface { + repository.BaseRepository[entity.MarketingProduct] + GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) +} + +type MarketingProductRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.MarketingProduct] +} + +func NewMarketingProductRepository(db *gorm.DB) MarketingProductRepository { + return &MarketingProductRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingProduct](db), + } +} + +func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) { + var products []entity.MarketingProduct + if err := r.DB().WithContext(ctx).Where("marketing_id = ?", marketingID).Find(&products).Error; err != nil { + return nil, err + } + if len(products) == 0 { + return products, gorm.ErrRecordNotFound + } + return products, nil +} diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go index bb4896cb..f06bf401 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go @@ -1,6 +1,8 @@ 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" @@ -8,6 +10,7 @@ import ( type MarketingRepository interface { repository.BaseRepository[entity.Marketing] + IdExists(ctx context.Context, id uint) (bool, error) } type MarketingRepositoryImpl struct { @@ -19,3 +22,7 @@ func NewMarketingRepository(db *gorm.DB) MarketingRepository { BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db), } } + +func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Marketing](ctx, r.DB(), id) +} diff --git a/internal/modules/marketing/sales-orders/route.go b/internal/modules/marketing/sales-orders/route.go index 455977fb..c48ae2a7 100644 --- a/internal/modules/marketing/sales-orders/route.go +++ b/internal/modules/marketing/sales-orders/route.go @@ -25,4 +25,5 @@ func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesO route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 0a1e5898..67163564 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -2,8 +2,11 @@ package service import ( "errors" + "fmt" + "strings" "time" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" @@ -11,7 +14,9 @@ import ( validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" + userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "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" @@ -20,11 +25,12 @@ import ( ) type SalesOrdersService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.SalesOrders, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.Marketing, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) DeleteOne(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) } type salesOrdersService struct { @@ -35,9 +41,11 @@ type salesOrdersService struct { KandangRepo kandangRepo.KandangRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository MarketingRepo repository.MarketingRepository + UserRepo userRepo.UserRepository + ApprovalSvc commonSvc.ApprovalService } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, Validate: validate, @@ -46,48 +54,94 @@ func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo c KandangRepo: kandangRepo, ProductWarehouseRepo: productWarehouseRepo, MarketingRepo: marketingRepo, + UserRepo: userRepo, + ApprovalSvc: approvalSvc, } } func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") + return db.Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse"). + Preload("Products.DeliveryProducts") } -func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) { +func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit - salesOrderss, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.Search != "" { - return db.Where("name LIKE ?", "%"+params.Search+"%") + return db.Where("so_number LIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { - s.Log.Errorf("Failed to get salesOrderss: %+v", err) - return nil, 0, err + s.Log.Errorf("Failed to get marketings: %+v", err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales orders") } - return salesOrderss, total, nil + + if s.ApprovalSvc != nil && len(marketings) > 0 { + ids := make([]uint, len(marketings)) + for i, item := range marketings { + ids[i] = item.Id + } + + latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, ids, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load latest approvals for marketings: %+v", err) + } else if len(latestMap) > 0 { + for i := range marketings { + if approval, ok := latestMap[marketings[i].Id]; ok { + marketings[i].LatestApproval = approval + } + } + } + } + + return marketings, total, nil } -func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.SalesOrders, error) { - salesOrders, err := s.Repository.GetByID(c.Context(), id, s.withRelations) +func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { + + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } if err != nil { - s.Log.Errorf("Failed get salesOrders by id: %+v", err) - return nil, err + s.Log.Errorf("Failed get marketing by id: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } - return salesOrders, nil + + if s.ApprovalSvc != nil { + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load approvals for marketing %d: %+v", id, err) + } else if len(approvals) > 0 { + if marketing.LatestApproval == nil { + latest := approvals[len(approvals)-1] + marketing.LatestApproval = &latest + } + } else { + marketing.LatestApproval = nil + } + } + + return marketing, nil } -func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) { +func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -107,25 +161,26 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } } - // parse date soDate, err := utils.ParseDateString(req.Date) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - // generate SO number soNumber := "SO-" + time.Now().Format("20060102150405") var marketing *entity.Marketing - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - // create marketing directly (tanpa create SalesOrders) + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + marketing = &entity.Marketing{ CustomerId: req.CustomerId, SoNumber: soNumber, SoDate: soDate, - SalesPersonId: 1, + SalesPersonId: req.SalesPersonId, Notes: req.Notes, CreatedBy: 1, } @@ -134,7 +189,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return err } - // Create MarketingProducts and MarketingDeliveryProducts if len(req.MarketingProducts) > 0 { for _, product := range req.MarketingProducts { marketingProduct := &entity.MarketingProduct{ @@ -146,12 +200,11 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e TotalWeight: product.TotalWeight, TotalPrice: product.TotalPrice, } - if err := dbTransaction.Create(marketingProduct).Error; err != nil { + if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { s.Log.Errorf("Failed to create marketing product: %+v", err) return err } - // create delivery product with zeroed numeric fields and nil delivery date marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ MarketingProductId: marketingProduct.Id, Qty: 0, @@ -162,13 +215,37 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e DeliveryDate: nil, VehicleNumber: product.VehicleNumber, } - if err := dbTransaction.Create(marketingDeliveryProduct).Error; err != nil { + if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { s.Log.Errorf("Failed to create marketing delivery product: %+v", err) return err } } } + actorID := uint(1) // TODO: ambil dari auth context + if err := approvalSvcTx.RegisterWorkflowSteps( + utils.ApprovalWorkflowMarketing, + utils.MarketingApprovalSteps, + ); err != nil { + s.Log.Errorf("Failed to register workflow steps: %+v", err) + return err + } + + approvalAction := entity.ApprovalActionCreated + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + marketing.Id, + utils.MarketingStepPengajuan, + &approvalAction, + actorID, + nil); err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + s.Log.Errorf("Failed to create approval: %+v", err) + return err + } + } + return nil }) if err != nil { @@ -178,51 +255,320 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } - createdMarketing, err := s.MarketingRepo.GetByID(c.Context(), marketing.Id, nil) - if err != nil { - s.Log.Errorf("Failed to fetch created marketing: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created marketing") - } - - return &entity.SalesOrders{ - Id: createdMarketing.Id, - Name: createdMarketing.SoNumber, - }, nil + return s.GetOne(c, marketing.Id) } -func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) { +func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - updateBody := make(map[string]any) - - if req.Name != nil { - updateBody["name"] = *req.Name - } - - if len(updateBody) == 0 { - return s.GetOne(c, id) - } - - if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") - } - s.Log.Errorf("Failed to update salesOrders: %+v", err) + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, + commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists}, + commonSvc.RelationCheck{Name: "SalesPerson", ID: &req.SalesPersonId, Exists: s.UserRepo.IdExists}, + ); err != nil { return nil, err } + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) + if err != nil { + s.Log.Errorf("Failed to check approval status: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + if latestApproval != nil && latestApproval.StepNumber >= 3 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Cannot update sales order after delivery order approval") + } + + if len(req.MarketingProducts) > 0 { + for _, item := range req.MarketingProducts { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, + ); err != nil { + return nil, err + } + } + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + updateBody := make(map[string]any) + if req.CustomerId != 0 { + updateBody["customer_id"] = req.CustomerId + } + if req.SalesPersonId != 0 { + updateBody["sales_person_id"] = req.SalesPersonId + } + if req.Date != "" { + soDate, err := utils.ParseDateString(req.Date) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid date format") + } + updateBody["so_date"] = soDate + } + if req.Notes != "" { + updateBody["notes"] = req.Notes + } + + if len(updateBody) > 0 { + if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") + } + } + + if len(req.MarketingProducts) > 0 { + + oldProducts, err := marketingProductRepoTx.GetByMarketingID(c.Context(), id) + if err != nil && err != gorm.ErrRecordNotFound { + s.Log.Errorf("Failed to fetch old marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + } + + for _, oldProduct := range oldProducts { + if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_product_id = ?", oldProduct.Id) + }); err != nil && err != gorm.ErrRecordNotFound { + s.Log.Errorf("Failed to delete delivery products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + } + } + + if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_id = ?", id) + }); err != nil && err != gorm.ErrRecordNotFound { + s.Log.Errorf("Failed to delete marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + } + + for _, product := range req.MarketingProducts { + marketingProduct := &entity.MarketingProduct{ + MarketingId: id, + ProductWarehouseId: product.ProductWarehouseId, + Qty: product.Qty, + UnitPrice: product.UnitPrice, + AvgWeight: product.AvgWeight, + TotalWeight: product.TotalWeight, + TotalPrice: product.TotalPrice, + } + if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { + + return err + } + + marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ + MarketingProductId: marketingProduct.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: product.VehicleNumber, + } + if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { + return err + } + } + } + + if latestApproval != nil && latestApproval.StepNumber == 2 { + actorID := uint(1) // todo: ambil dari auth context + resetNote := "" + action := entity.ApprovalActionApproved + _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + id, + utils.MarketingStepPengajuan, + &action, + actorID, + &resetNote) + if err != nil { + s.Log.Errorf("Failed to create reset approval: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status") + } + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") + } + return s.GetOne(c, id) } func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") - } - s.Log.Errorf("Failed to delete salesOrders: %+v", err) - return err + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } + if err != nil { + s.Log.Errorf("Failed to fetch marketing %d before delete: %+v", id, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + + if len(marketing.Products) > 0 { + for _, product := range marketing.Products { + if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_product_id = ?", product.Id) + }); err != nil && err != gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") + } + } + } + + if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_id = ?", id) + }); err != nil && err != gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") + } + + if err := marketingRepoTx.DeleteOne(c.Context(), id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order") + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return fiberErr + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order") + } + return nil } + +func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) + + 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") + } + + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + for _, id := range approvableIDs { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, + ); err != nil { + return nil, err + } + + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + if latestApproval == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d - sales orders must be created first", id)) + } + + if action == entity.ApprovalActionApproved { + switch latestApproval.StepNumber { + case uint16(utils.MarketingStepPengajuan): + case uint16(utils.MarketingStepSalesOrder): + default: + return nil, fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber)) + } + } + } + + err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + for _, approvableID := range approvableIDs { + + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nil) + if err != nil { + s.Log.Errorf("Failed to get latest approval for %d: %+v", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check current approval step") + } + + if latestApproval == nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", approvableID)) + } + + var nextStep approvalutils.ApprovalStep + currentStep := latestApproval.StepNumber + + if action == entity.ApprovalActionApproved { + + if currentStep == uint16(utils.MarketingStepPengajuan) { + nextStep = utils.MarketingStepSalesOrder + } else if currentStep == uint16(utils.MarketingStepSalesOrder) { + nextStep = utils.MarketingDeliveryOrder + } else { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d already completed all approval steps", approvableID)) + } + } else { + + nextStep = approvalutils.ApprovalStep(currentStep) + } + + actorID := uint(1) // todo ambil dari auth context + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + approvableID, + nextStep, + &action, + actorID, + req.Notes, + ); err != nil { + s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") + } + } + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + updated := make([]entity.Marketing, 0, len(approvableIDs)) + for _, id := range approvableIDs { + marketing, err := s.GetOne(c, id) + if err != nil { + return nil, err + } + updated = append(updated, *marketing) + } + + return updated, nil +} diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go index b09129bc..01b3af9d 100644 --- a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -2,6 +2,7 @@ package validation type Create struct { CustomerId uint `json:"customer_id" validate:"required,gt=0"` + SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"` Date string `json:"date" validate:"required,datetime=2006-01-02"` Notes string `json:"notes" validate:"omitempty,max=500"` MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"` @@ -19,7 +20,11 @@ type CreateMarketingProduct struct { } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + CustomerId uint `json:"customer_id" validate:"omitempty,gt=0"` + SalesPersonId uint `json:"sales_person_id" validate:"omitempty,gt=0"` + Date string `json:"date" validate:"omitempty,datetime=2006-01-02"` + Notes string `json:"notes" validate:"omitempty,max=500"` + MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"` } type Query struct { @@ -27,3 +32,9 @@ type Query struct { Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Search string `query:"search" validate:"omitempty,max=50"` } + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/modules/users/repositories/user.repository.go b/internal/modules/users/repositories/user.repository.go index 8472db13..28c06a74 100644 --- a/internal/modules/users/repositories/user.repository.go +++ b/internal/modules/users/repositories/user.repository.go @@ -1,6 +1,8 @@ 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" @@ -8,14 +10,21 @@ import ( type UserRepository interface { repository.BaseRepository[entity.User] + IdExists(ctx context.Context, id uint) (bool, error) } type UserRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.User] + db *gorm.DB } func NewUserRepository(db *gorm.DB) UserRepository { return &UserRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.User](db), + db: db, } } + +func (r *UserRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.User](ctx, r.db, id) +} diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 099b6510..fc01a231 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -224,6 +224,23 @@ var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{ PurchaseStepStaffPurchase: "Staff Purchase", } +// ------------------------------------------------------------------- +// Marketings Approval +// ------------------------------------------------------------------- + +const ( + ApprovalWorkflowMarketing approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("MARKETINGS") + MarketingStepPengajuan approvalutils.ApprovalStep = 1 + MarketingStepSalesOrder approvalutils.ApprovalStep = 2 + MarketingDeliveryOrder approvalutils.ApprovalStep = 3 +) + +var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{ + MarketingStepPengajuan: "Pengajuan", + MarketingStepSalesOrder: "Sales Order", + MarketingDeliveryOrder: "Delivery Order", +} + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- From 74ec25db5b38714a6adf6fb7507992ecb30f67d3 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 13 Nov 2025 09:50:34 +0700 Subject: [PATCH 33/59] Feat[BE-222.223.224]: creating create one delivery order and getone delivery order[Unfinished] --- internal/entities/marketing_product.go | 6 +- .../marketing-delivery-products.repository.go | 11 ++ .../marketing/delivery-orderss/module.go | 4 +- .../services/delivery-orders.service.go | 152 +++++++++--------- .../controllers/sales-orders.controller.go | 2 +- .../sales-orders/dto/sales-orders.dto.go | 8 + .../marketing-products.repository.go | 5 + .../services/sales-orders.service.go | 10 +- 8 files changed, 119 insertions(+), 79 deletions(-) diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go index f0fe7f38..66524bc6 100644 --- a/internal/entities/marketing_product.go +++ b/internal/entities/marketing_product.go @@ -19,7 +19,7 @@ type MarketingProduct struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` - ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` - DeliveryProducts []MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"` + Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` + ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + DeliveryProduct *MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"` } diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go index 95e9b3bb..ce94a1eb 100644 --- a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go @@ -1,6 +1,8 @@ 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" @@ -8,6 +10,7 @@ import ( type MarketingDeliveryProductRepository interface { repository.BaseRepository[entity.MarketingDeliveryProduct] + GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) } type MarketingDeliveryProductRepositoryImpl struct { @@ -19,3 +22,11 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db), } } + +func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) { + var deliveryProduct entity.MarketingDeliveryProduct + if err := r.DB().WithContext(ctx).Where("marketing_product_id = ?", marketingProductID).First(&deliveryProduct).Error; err != nil { + return nil, err + } + return &deliveryProduct, nil +} diff --git a/internal/modules/marketing/delivery-orderss/module.go b/internal/modules/marketing/delivery-orderss/module.go index c6932c51..7fcc8ccc 100644 --- a/internal/modules/marketing/delivery-orderss/module.go +++ b/internal/modules/marketing/delivery-orderss/module.go @@ -11,6 +11,7 @@ import ( rDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + rMarketingProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) @@ -20,12 +21,13 @@ type DeliveryOrdersModule struct{} func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { deliveryOrdersRepo := rDeliveryOrders.NewDeliveryOrdersRepository(db) marketingRepo := rMarketing.NewMarketingRepository(db) + marketingProductRepo := rMarketingProduct.NewMarketingProductRepository(db) marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db) userRepo := rUser.NewUserRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalSvc := commonSvc.NewApprovalService(approvalRepo) - deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingDeliveryProductRepo, approvalSvc, validate) + deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) DeliveryOrdersRoutes(router, userService, deliveryOrdersService) diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index db238d0f..7a2efa3f 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -35,6 +35,7 @@ type deliveryOrdersService struct { Validate *validator.Validate Repository repository.DeliveryOrdersRepository MarketingRepo marketingRepo.MarketingRepository + MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository ApprovalSvc commonSvc.ApprovalService } @@ -42,6 +43,7 @@ type deliveryOrdersService struct { func NewDeliveryOrdersService( repo repository.DeliveryOrdersRepository, marketingRepo marketingRepo.MarketingRepository, + marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate, @@ -51,6 +53,7 @@ func NewDeliveryOrdersService( Validate: validate, Repository: repo, MarketingRepo: marketingRepo, + MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ApprovalSvc: approvalSvc, } @@ -89,7 +92,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ result := make([]dto.DeliveryOrdersListDTO, len(marketings)) for i, marketing := range marketings { // Get marketing delivery products - var deliveryProducts []entity.MarketingDeliveryProduct + var allDeliveryProducts []entity.MarketingDeliveryProduct if err := s.Repository.DB().WithContext(c.Context()). Preload("MarketingProduct"). Where("marketing_product_id IN (?)", @@ -97,20 +100,20 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ Model(&entity.MarketingProduct{}). Select("id"). Where("marketing_id = ?", marketing.Id)). - Find(&deliveryProducts).Error; err != nil { + Find(&allDeliveryProducts).Error; err != nil { s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) // Continue without products } - // Create dummy DeliveryOrders untuk dto mapping - dummyDO := &entity.DeliveryOrders{ + // Build response DTO + deliveryOrderResponse := &entity.DeliveryOrders{ MarketingId: marketing.Id, CreatedUser: &marketing.CreatedUser, Marketing: &marketing, - DeliveryProducts: deliveryProducts, + DeliveryProducts: allDeliveryProducts, } - result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) } return result, total, nil @@ -133,7 +136,7 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrder } // Get marketing delivery products - var deliveryProducts []entity.MarketingDeliveryProduct + var allDeliveryProducts []entity.MarketingDeliveryProduct if err := s.Repository.DB().WithContext(c.Context()). Preload("MarketingProduct"). Where("marketing_product_id IN (?)", @@ -141,21 +144,21 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrder Model(&entity.MarketingProduct{}). Select("id"). Where("marketing_id = ?", marketing.Id)). - Find(&deliveryProducts).Error; err != nil { + Find(&allDeliveryProducts).Error; err != nil { s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) // Continue without products } - // Create dummy DeliveryOrders untuk dto mapping - dummyDO := &entity.DeliveryOrders{ + // Build response DTO + deliveryOrderResponse := &entity.DeliveryOrders{ MarketingId: marketing.Id, CreatedUser: &marketing.CreatedUser, Marketing: marketing, - DeliveryProducts: deliveryProducts, + DeliveryProducts: allDeliveryProducts, } - result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) - return &result, nil + responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + return &responseDTO, nil } func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) { @@ -163,88 +166,93 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - // Validate marketing exists - _, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") - } - if err != nil { - s.Log.Errorf("Failed to fetch marketing %d: %+v", req.MarketingId, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists}, + ); err != nil { + return nil, err + } + + var relationChecks []commonSvc.RelationCheck + for _, requestedProduct := range req.DeliveryProducts { + relationChecks = append(relationChecks, commonSvc.RelationCheck{ + Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists, + }) } - // Validate marketing approval status - harus sudah di approve ke step Sales Order approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } - if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval") } - - // Cek apakah status approval sudah Sales Order (step 2) atau lebih if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") } - if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing is not approved for delivery") } - // Validate semua delivery products ada dan update mereka - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { - for _, product := range req.DeliveryProducts { - // Fetch marketing_product terlebih dahulu untuk pastikan punya marketing_id yang sama - var marketingProduct entity.MarketingProduct - if err := dbTx.Where("id = ? AND marketing_id = ?", product.MarketingProductId, req.MarketingId). - First(&marketingProduct).Error; err != nil { + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) + + for _, requestedProduct := range req.DeliveryProducts { + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) + if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", product.MarketingProductId)) + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) } - s.Log.Errorf("Failed to fetch marketing product: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing product") + s.Log.Errorf("Failed to fetch marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") } - // Fetch marketing_delivery_product by marketing_product_id - var mdp entity.MarketingDeliveryProduct - if err := dbTx.Where("marketing_product_id = ?", marketingProduct.Id). - First(&mdp).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", product.MarketingProductId)) + var foundMarketingProduct *entity.MarketingProduct + for i := range allMarketingProducts { + if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { + foundMarketingProduct = &allMarketingProducts[i] + break + } + } + if foundMarketingProduct == nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) + } + + deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId)) } - s.Log.Errorf("Failed to fetch marketing delivery product: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } - // Parse delivery date per item (jika ada), atau gunakan default - itemDeliveryDate := time.Now() - if product.DeliveryDate != "" { - parsedItemDate, err := time.Parse("2006-01-02", product.DeliveryDate) + var itemDeliveryDate time.Time + if requestedProduct.DeliveryDate != "" { + parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d", product.MarketingProductId)) + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } - itemDeliveryDate = parsedItemDate + itemDeliveryDate = parsedDate + } else { + itemDeliveryDate = time.Now() } - // Update dengan data dari request - PASTIKAN UPDATE LANGSUNG KE FIELD - updates := map[string]interface{}{ - "qty": product.Qty, - "unit_price": product.UnitPrice, - "avg_weight": product.AvgWeight, - "total_weight": product.TotalWeight, - "total_price": product.TotalPrice, - "delivery_date": &itemDeliveryDate, - "vehicle_number": product.VehicleNumber, - } + deliveryProduct.Qty = requestedProduct.Qty + deliveryProduct.UnitPrice = requestedProduct.UnitPrice + deliveryProduct.AvgWeight = requestedProduct.AvgWeight + deliveryProduct.TotalWeight = requestedProduct.TotalWeight + deliveryProduct.TotalPrice = requestedProduct.TotalPrice + deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber - if err := dbTx.Model(&mdp).Updates(updates).Error; err != nil { + if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { s.Log.Errorf("Failed to update marketing delivery product: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } - s.Log.Infof("Updated MDP %d: qty=%v, unitPrice=%v, totalPrice=%v", mdp.Id, product.Qty, product.UnitPrice, product.TotalPrice) + s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) } return nil @@ -254,9 +262,9 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - // Fetch marketing dengan delivery products yang sudah di-update + // Fetch marketing dengan delivery products yang sudah di-updated marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser").Preload("Products") + return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") }) if err != nil { s.Log.Errorf("Failed to fetch marketing after update: %+v", err) @@ -264,30 +272,30 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) } // Get marketing delivery products - var deliveryProducts []entity.MarketingDeliveryProduct - if err := s.Repository.DB().WithContext(c.Context()). + var allDeliveryProducts []entity.MarketingDeliveryProduct + if err := s.MarketingDeliveryProductRepo.DB().WithContext(c.Context()). Preload("MarketingProduct"). Where("marketing_product_id IN (?)", - s.Repository.DB().WithContext(c.Context()). + s.MarketingProductRepo.DB().WithContext(c.Context()). Model(&entity.MarketingProduct{}). Select("id"). Where("marketing_id = ?", req.MarketingId)). - Find(&deliveryProducts).Error; err != nil { + Find(&allDeliveryProducts).Error; err != nil { s.Log.Errorf("Failed to load delivery products: %+v", err) // Continue tanpa delivery products } - // Create dummy DeliveryOrders untuk dipakai dto mapping - dummyDO := &entity.DeliveryOrders{ + // Build response DTO + deliveryOrderResponse := &entity.DeliveryOrders{ MarketingId: req.MarketingId, Notes: req.Notes, CreatedUser: &marketing.CreatedUser, Marketing: marketing, - DeliveryProducts: deliveryProducts, + DeliveryProducts: allDeliveryProducts, } - result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) - return &result, nil + responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + return &responseDTO, nil } func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) { diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index 4c49baeb..a5365787 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -98,7 +98,7 @@ func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error { Code: fiber.StatusCreated, Status: "success", Message: "Create salesOrders successfully", - Data: result, + Data: dto.ToSalesOrdersListDTOFromMarketing(*result), }) } diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 920b8d26..79bc6453 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -32,6 +32,7 @@ type MarketingProductDTO struct { Product *productDTO.ProductBaseDTO `json:"product,omitempty"` Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` } `json:"product_warehouse,omitempty"` + DeliveryProduct *MarketingDeliveryProductDTO `json:"delivery_product,omitempty"` } type MarketingDeliveryProductDTO struct { @@ -104,6 +105,12 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { } } + var deliveryProduct *MarketingDeliveryProductDTO + if e.DeliveryProduct != nil && e.DeliveryProduct.Id != 0 { + mapped := ToMarketingDeliveryProductDTO(*e.DeliveryProduct) + deliveryProduct = &mapped + } + return MarketingProductDTO{ Id: e.Id, Qty: e.Qty, @@ -112,6 +119,7 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { TotalWeight: e.TotalWeight, TotalPrice: e.TotalPrice, ProductWarehouse: productWarehouse, + DeliveryProduct: deliveryProduct, } } diff --git a/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go b/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go index d3a6798f..4d5eb43f 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go @@ -11,6 +11,7 @@ import ( type MarketingProductRepository interface { repository.BaseRepository[entity.MarketingProduct] GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) + IdExists(ctx context.Context, id uint) (bool, error) } type MarketingProductRepositoryImpl struct { @@ -33,3 +34,7 @@ func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, m } return products, nil } + +func (r *MarketingProductRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.MarketingProduct](ctx, r.DB(), id) +} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 67163564..1fb36733 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -65,7 +65,7 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Warehouse"). - Preload("Products.DeliveryProducts") + Preload("Products.DeliveryProduct") } func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { @@ -255,7 +255,13 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } - return s.GetOne(c, marketing.Id) + marketing, err = s.MarketingRepo.GetByID(c.Context(), marketing.Id, s.withRelations) + if err != nil { + s.Log.Errorf("Failed to fetch created marketing: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created sales order") + } + + return marketing, nil } func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) { From 2f5fab9f8081740e0bbe01161b03def69bd8771a Mon Sep 17 00:00:00 2001 From: Asep Teguh Hidayat <114033670+Aguh18@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:28:50 +0700 Subject: [PATCH 34/59] Feat[BE-222]: creating update DO(Unfinished) --- ...create_marketing_product_deliveries.up.sql | 2 +- .../marketing-delivery-products.repository.go | 17 ++ .../controllers/delivery-orders.controller.go | 2 +- .../dto/delivery-orders.dto.go | 170 +++++++++++- .../services/delivery-orders.service.go | 253 +++++++++++++----- .../services/sales-orders.service.go | 3 +- 6 files changed, 367 insertions(+), 80 deletions(-) diff --git a/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql index 09625c16..45ca0907 100644 --- a/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql +++ b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql @@ -6,7 +6,7 @@ CREATE TABLE marketing_delivery_products ( total_weight NUMERIC(15, 3) NOT NULL, avg_weight NUMERIC(15, 3) NOT NULL, total_price NUMERIC(15, 3) NOT NULL, - delivery_date TIMESTAMPTZ, + delivery_date DATE, vehicle_number VARCHAR(50), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go index ce94a1eb..96590990 100644 --- a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go @@ -11,6 +11,7 @@ import ( type MarketingDeliveryProductRepository interface { repository.BaseRepository[entity.MarketingDeliveryProduct] GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) + GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) } type MarketingDeliveryProductRepositoryImpl struct { @@ -30,3 +31,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx con } return &deliveryProduct, nil } + +func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) { + var deliveryProducts []entity.MarketingDeliveryProduct + + // Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct + if err := r.DB().WithContext(ctx). + Preload("MarketingProduct"). + Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id"). + Where("mp.marketing_id = ?", marketingId). + Order("marketing_delivery_products.id ASC"). + Find(&deliveryProducts).Error; err != nil { + return nil, err + } + + return deliveryProducts, nil +} diff --git a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go index a5f2839a..8ca51dc5 100644 --- a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go +++ b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go @@ -119,7 +119,7 @@ func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update deliveryOrders successfully", - Data: dto.ToDeliveryOrdersListDTO(*result), + Data: result, }) } diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go index 2b2ea51e..6008269d 100644 --- a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -1,6 +1,8 @@ package dto import ( + "fmt" + "sort" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -22,6 +24,22 @@ type MarketingDeliveryProductDTO struct { VehicleNumber string `json:"vehicle_number"` } +// DTO untuk grouping delivery products berdasarkan warehouse dan tanggal +type DeliveryGroupDTO struct { + WarehouseId uint `json:"warehouse_id"` + WarehouseName string `json:"warehouse_name"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` + TotalQty float64 `json:"total_qty"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` +} + +// DTO untuk Delivery Order (DO) - berisi data delivery yang sudah digroup +type DeliveryOrderDTO struct { + DeliveryGroups []DeliveryGroupDTO `json:"delivery_groups"` +} + type DeliveryOrdersBaseDTO struct { Id uint `json:"id,omitempty"` DeliveryNumber *string `json:"delivery_number,omitempty"` @@ -30,20 +48,33 @@ type DeliveryOrdersBaseDTO struct { Notes string `json:"notes,omitempty"` } +type MarketingProductDTO struct { + Id uint `json:"id"` + MarketingId uint `json:"marketing_id"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + // Add product relation if needed +} + type MarketingBaseDTO struct { - Id uint `json:"id"` - SoNumber string `json:"so_number"` - SoDate time.Time `json:"so_date"` + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` + Products []MarketingProductDTO `json:"products,omitempty"` } type DeliveryOrdersListDTO struct { DeliveryOrdersBaseDTO - Marketing *MarketingBaseDTO `json:"marketing,omitempty"` - DeliveryProducts []MarketingDeliveryProductDTO `json:"delivery_products,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` + SalesOrder *MarketingBaseDTO `json:"sales_order,omitempty"` // SO - Sales Order data + DeliveryOrder *DeliveryOrderDTO `json:"delivery_order,omitempty"` // DO - Delivery Order data (grouped) + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } type DeliveryOrdersDetailDTO struct { @@ -52,6 +83,19 @@ type DeliveryOrdersDetailDTO struct { // === Mapper Functions === +func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { + return MarketingProductDTO{ + Id: e.Id, + MarketingId: e.MarketingId, + ProductWarehouseId: e.ProductWarehouseId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + AvgWeight: e.AvgWeight, + TotalWeight: e.TotalWeight, + TotalPrice: e.TotalPrice, + } +} + func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { return MarketingDeliveryProductDTO{ Id: e.Id, @@ -90,10 +134,19 @@ func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { var marketing *MarketingBaseDTO if e.Marketing != nil && e.Marketing.Id != 0 { + var marketingProducts []MarketingProductDTO + if len(e.Marketing.Products) > 0 { + marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) + for i, product := range e.Marketing.Products { + marketingProducts[i] = ToMarketingProductDTO(product) + } + } + marketing = &MarketingBaseDTO{ Id: e.Marketing.Id, SoNumber: e.Marketing.SoNumber, SoDate: e.Marketing.SoDate, + Products: marketingProducts, } } @@ -105,10 +158,16 @@ func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { } } + // Group delivery products by warehouse and delivery date + deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + + // Create delivery order DTO with summary + deliveryOrder := createDeliveryOrderDTO(deliveryGroups) + return DeliveryOrdersListDTO{ DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - Marketing: marketing, - DeliveryProducts: deliveryProductsDTOs, + SalesOrder: marketing, + DeliveryOrder: deliveryOrder, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, @@ -124,10 +183,19 @@ func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProduc var marketing *MarketingBaseDTO if e.Marketing != nil && e.Marketing.Id != 0 { + var marketingProducts []MarketingProductDTO + if len(e.Marketing.Products) > 0 { + marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) + for i, product := range e.Marketing.Products { + marketingProducts[i] = ToMarketingProductDTO(product) + } + } + marketing = &MarketingBaseDTO{ Id: e.Marketing.Id, SoNumber: e.Marketing.SoNumber, SoDate: e.Marketing.SoDate, + Products: marketingProducts, } } @@ -139,10 +207,16 @@ func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProduc } } + // Group delivery products by warehouse and delivery date + deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + + // Create delivery order DTO with summary + deliveryOrder := createDeliveryOrderDTO(deliveryGroups) + return DeliveryOrdersListDTO{ DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - Marketing: marketing, - DeliveryProducts: deliveryProductsDTOs, + SalesOrder: marketing, + DeliveryOrder: deliveryOrder, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, @@ -162,3 +236,73 @@ func ToDeliveryOrdersDetailDTO(e entity.DeliveryOrders) DeliveryOrdersDetailDTO DeliveryOrdersListDTO: ToDeliveryOrdersListDTO(e), } } + +// groupDeliveryProducts groups delivery products by warehouse and delivery date +func groupDeliveryProducts(products []MarketingDeliveryProductDTO) []DeliveryGroupDTO { + // Create a map to group products + groupMap := make(map[string]*DeliveryGroupDTO) + + for _, product := range products { + // Create unique key for grouping (warehouse_id + delivery_date) + // Since we're working with DTO, we need to handle the warehouse id differently + warehouseId := uint(0) + warehouseName := "" + + // Extract warehouse info from product (assuming it might be available in future) + // For now, we'll use a simple grouping by delivery date and vehicle number + var key string + if product.DeliveryDate != nil { + key = fmt.Sprintf("%s_%s", product.DeliveryDate.Format("2006-01-02"), product.VehicleNumber) + } else { + key = fmt.Sprintf("no_date_%s", product.VehicleNumber) + } + + // Get or create group + group, exists := groupMap[key] + if !exists { + group = &DeliveryGroupDTO{ + WarehouseId: warehouseId, + WarehouseName: warehouseName, + DeliveryDate: product.DeliveryDate, + VehicleNumber: product.VehicleNumber, + TotalQty: 0, + TotalWeight: 0, + TotalPrice: 0, + } + + groupMap[key] = group + } + + // Update totals + group.TotalQty += product.Qty + group.TotalWeight += product.TotalWeight + group.TotalPrice += product.TotalPrice + } + + // Convert map to slice + var groups []DeliveryGroupDTO + for _, group := range groupMap { + groups = append(groups, *group) + } + + // Sort groups by delivery date + sort.Slice(groups, func(i, j int) bool { + if groups[i].DeliveryDate == nil || groups[j].DeliveryDate == nil { + return false + } + return groups[i].DeliveryDate.Before(*groups[j].DeliveryDate) + }) + + return groups +} + +// createDeliveryOrderDTO creates delivery order DTO +func createDeliveryOrderDTO(deliveryGroups []DeliveryGroupDTO) *DeliveryOrderDTO { + if len(deliveryGroups) == 0 { + return nil + } + + return &DeliveryOrderDTO{ + DeliveryGroups: deliveryGroups, + } +} diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index 7a2efa3f..f090ac01 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -25,7 +25,7 @@ type DeliveryOrdersService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) } @@ -91,18 +91,11 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ // Load delivery products untuk setiap marketing result := make([]dto.DeliveryOrdersListDTO, len(marketings)) for i, marketing := range marketings { - // Get marketing delivery products - var allDeliveryProducts []entity.MarketingDeliveryProduct - if err := s.Repository.DB().WithContext(c.Context()). - Preload("MarketingProduct"). - Where("marketing_product_id IN (?)", - s.Repository.DB().WithContext(c.Context()). - Model(&entity.MarketingProduct{}). - Select("id"). - Where("marketing_id = ?", marketing.Id)). - Find(&allDeliveryProducts).Error; err != nil { + // Get marketing delivery products menggunakan repository method + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketing.Id) + if err != nil { s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) - // Continue without products + allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal } // Build response DTO @@ -125,7 +118,8 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrder return db.Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). - Preload("Products.ProductWarehouse") + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse") }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") @@ -135,26 +129,52 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrder return nil, err } - // Get marketing delivery products - var allDeliveryProducts []entity.MarketingDeliveryProduct - if err := s.Repository.DB().WithContext(c.Context()). - Preload("MarketingProduct"). - Where("marketing_product_id IN (?)", - s.Repository.DB().WithContext(c.Context()). - Model(&entity.MarketingProduct{}). - Select("id"). - Where("marketing_id = ?", marketing.Id)). - Find(&allDeliveryProducts).Error; err != nil { - s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) - // Continue without products + // Get marketing delivery products menggunakan repository method + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) + if err != nil { + s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", id, err) + allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal } - // Build response DTO + // Debug: Log jumlah delivery products + s.Log.Infof("Found %d delivery products for marketing %d", len(allDeliveryProducts), id) + + // Jika tidak ada delivery products, buat dummy data untuk testing + if len(allDeliveryProducts) == 0 && len(marketing.Products) > 0 { + s.Log.Infof("Creating dummy delivery products for testing") + for i, product := range marketing.Products { + deliveryDate := marketing.SoDate.AddDate(0, 0, i+7) // 7 hari setelah SO + dummyDeliveryProduct := entity.MarketingDeliveryProduct{ + Id: uint(i + 1), + MarketingProductId: product.Id, + Qty: product.Qty / 2, // Setengah dari qty asli + UnitPrice: product.UnitPrice, + TotalWeight: product.TotalWeight / 2, + AvgWeight: product.AvgWeight, + TotalPrice: (product.Qty / 2) * product.UnitPrice, + DeliveryDate: &deliveryDate, + VehicleNumber: fmt.Sprintf("B%04d%s", (i+1)*1000, "ABC"), + } + allDeliveryProducts = append(allDeliveryProducts, dummyDeliveryProduct) + } + s.Log.Infof("Created %d dummy delivery products", len(allDeliveryProducts)) + } + + // Build response DTO dengan timestamps yang benar deliveryOrderResponse := &entity.DeliveryOrders{ MarketingId: marketing.Id, CreatedUser: &marketing.CreatedUser, Marketing: marketing, DeliveryProducts: allDeliveryProducts, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + } + + // Set delivery_date dari delivery products atau fallback ke marketing date + if len(allDeliveryProducts) > 0 && allDeliveryProducts[0].DeliveryDate != nil { + deliveryOrderResponse.DeliveryDate = *allDeliveryProducts[0].DeliveryDate + } else { + deliveryOrderResponse.DeliveryDate = marketing.SoDate } responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) @@ -255,6 +275,27 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) } + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + actorID := uint(1) // TODO: ambil dari auth context + approvalAction := entity.ApprovalActionCreated + var notes *string + if req.Notes != "" { + notes = &req.Notes + } + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + req.MarketingId, + utils.MarketingDeliveryOrder, + &approvalAction, + actorID, + notes); err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + s.Log.Errorf("Failed to create delivery order approval: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order approval") + } + } + return nil }) @@ -262,7 +303,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - // Fetch marketing dengan delivery products yang sudah di-updated marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") }) @@ -271,18 +311,11 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") } - // Get marketing delivery products - var allDeliveryProducts []entity.MarketingDeliveryProduct - if err := s.MarketingDeliveryProductRepo.DB().WithContext(c.Context()). - Preload("MarketingProduct"). - Where("marketing_product_id IN (?)", - s.MarketingProductRepo.DB().WithContext(c.Context()). - Model(&entity.MarketingProduct{}). - Select("id"). - Where("marketing_id = ?", req.MarketingId)). - Find(&allDeliveryProducts).Error; err != nil { + // Get marketing delivery products menggunakan repository method + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), req.MarketingId) + if err != nil { s.Log.Errorf("Failed to load delivery products: %+v", err) - // Continue tanpa delivery products + allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal } // Build response DTO @@ -298,38 +331,132 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return &responseDTO, nil } -func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) { +func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - updateBody := make(map[string]any) - - if req.DeliveryDate != "" { - deliveryDate, err := time.Parse("2006-01-02", req.DeliveryDate) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid delivery date format") - } - updateBody["delivery_date"] = deliveryDate - } - - if req.Notes != "" { - updateBody["notes"] = req.Notes - } - - if len(updateBody) == 0 { - return s.Repository.GetByID(c.Context(), id, s.withRelations) - } - - if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") - } - s.Log.Errorf("Failed to update deliveryOrders: %+v", err) + // Validate bahwa marketing ID yang di-update ada (id parameter adalah marketing_id untuk delivery orders) + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, + ); err != nil { return nil, err } - return s.Repository.GetByID(c.Context(), id, s.withRelations) + // Validate delivery products jika ada + if len(req.DeliveryProducts) > 0 { + for _, requestedProduct := range req.DeliveryProducts { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists}, + ); err != nil { + return nil, err + } + } + } + + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) + + // Update delivery products jika ada dalam request + if len(req.DeliveryProducts) > 0 { + for _, requestedProduct := range req.DeliveryProducts { + // Validate bahwa marketing product ada untuk marketing ini + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", id)) + } + s.Log.Errorf("Failed to fetch marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") + } + + var foundMarketingProduct *entity.MarketingProduct + for i := range allMarketingProducts { + if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { + foundMarketingProduct = &allMarketingProducts[i] + break + } + } + if foundMarketingProduct == nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) + } + + // Get existing delivery product + deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") + } + + // Parse delivery date + var itemDeliveryDate time.Time + if requestedProduct.DeliveryDate != "" { + parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) + } + itemDeliveryDate = parsedDate + } else if deliveryProduct.DeliveryDate != nil { + itemDeliveryDate = *deliveryProduct.DeliveryDate + } else { + itemDeliveryDate = time.Now() + } + + // Update delivery product + deliveryProduct.Qty = requestedProduct.Qty + deliveryProduct.UnitPrice = requestedProduct.UnitPrice + deliveryProduct.AvgWeight = requestedProduct.AvgWeight + deliveryProduct.TotalWeight = requestedProduct.TotalWeight + deliveryProduct.TotalPrice = requestedProduct.TotalPrice + deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber + + if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { + s.Log.Errorf("Failed to update marketing delivery product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + } + + s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + // Fetch updated marketing with delivery products + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") + }) + if err != nil { + s.Log.Errorf("Failed to fetch marketing after update: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") + } + + // Get marketing delivery products menggunakan repository method + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) + if err != nil { + s.Log.Errorf("Failed to load delivery products: %+v", err) + allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal + } + + // Build response DTO + deliveryOrderResponse := &entity.DeliveryOrders{ + MarketingId: id, + Notes: req.Notes, + CreatedUser: &marketing.CreatedUser, + Marketing: marketing, + DeliveryProducts: allDeliveryProducts, + } + + responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + return &responseDTO, nil } func (s deliveryOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) { diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 1fb36733..74244496 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -64,8 +64,7 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). - Preload("Products.ProductWarehouse.Warehouse"). - Preload("Products.DeliveryProduct") + Preload("Products.ProductWarehouse.Warehouse") } func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { From 903b1143158dddb9ab0a5e06cb79b242450cc235 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 13 Nov 2025 20:27:49 +0700 Subject: [PATCH 35/59] fix[BE]: fixing null project flock ikandang id on lookup --- .../dto/project_flock_kandang.dto.go | 4 ++- .../dto/projectflock_kandang.dto.go | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 5d8e6628..1a076232 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -74,7 +74,9 @@ type ProjectFlockKandangDetailDTO struct { func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO { return ProjectFlockKandangBaseDTO{ - Id: e.Id, + Id: e.Id, + KandangId: e.KandangId, + ProjectFlockId: e.ProjectFlockId, } } diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 233f655b..24e53d28 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -28,12 +28,13 @@ type ProjectFlockWithPivotDTO struct { } type ProjectFlockKandangDTO struct { - Id uint `json:"id"` - - KandangId uint `json:"kandang_id"` - Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` - ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` - AvailableQuantity float64 `json:"available_quantity"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ProjectFlockId uint `json:"project_flock_id"` + KandangId uint `json:"kandang_id"` + Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` + ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` + AvailableQuantity float64 `json:"available_quantity"` } func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { @@ -88,10 +89,12 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD } return ProjectFlockKandangDTO{ - Id: e.Id, - KandangId: e.KandangId, - Kandang: kandang, - ProjectFlock: pf, - AvailableQuantity: 0, + Id: e.Id, + ProjectFlockKandangId: e.Id, + ProjectFlockId: e.ProjectFlockId, + KandangId: e.KandangId, + Kandang: kandang, + ProjectFlock: pf, + AvailableQuantity: 0, } } From 17d3042586134e765d2843c208f2b63ad190adab Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Fri, 14 Nov 2025 16:43:01 +0700 Subject: [PATCH 36/59] fix(BE): kandang capacity and fix err response --- .DS_Store | Bin 6148 -> 6148 bytes internal/common/validation/validation.go | 48 +++++++++-- internal/config/.DS_Store | Bin 6148 -> 6148 bytes ...114084320_update_kandang_capacity.down.sql | 2 + ...51114084320_update_kandang_capacity.up.sql | 2 + internal/database/seed/seeder.go | 77 ++++++++---------- internal/entities/kandang.go | 25 +++--- .../master/kandangs/dto/kandang.dto.go | 2 + internal/modules/master/kandangs/route.go | 4 +- .../kandangs/services/kandang.service.go | 5 ++ .../validations/kandang.validation.go | 22 ++--- internal/modules/master/uoms/dto/uom.dto.go | 6 +- .../recordings/dto/recording.dto.go | 2 + internal/utils/error.go | 4 +- tools/templates/dto.tmpl | 14 ++-- 15 files changed, 129 insertions(+), 84 deletions(-) create mode 100644 internal/database/migrations/20251114084320_update_kandang_capacity.down.sql create mode 100644 internal/database/migrations/20251114084320_update_kandang_capacity.up.sql diff --git a/.DS_Store b/.DS_Store index 4c14efd89e4d913a63e6242a245ab626c5fffe6d..e39247fdff6549a6304ce8065c332c38da11c1a4 100644 GIT binary patch delta 31 ncmZoMXfc@J&nU4mU^g?P#AF_p{LPzLLYOBuSZrqJ_{$Ffpo$73 delta 70 zcmZoMXfc@J&nUSuU^g?PB`mF;Q%yo}wrV0|Nsi1A_nqLokC8gDHaN vD9C^~W@Dl^+hhX~md)H8+#En3ZcO~nJeglak(CK(ox)^85#h}qB72wt$+9$m delta 70 zcmZoMXfc=|#>AjHu~2NHo+1YW5HK<@2y8yc=*G7B0%I2AW_AvK4xj>{$am(+{342+ UKzW7)kiy9(Jj$D6L{=~Z05sDNYXATM diff --git a/internal/database/migrations/20251114084320_update_kandang_capacity.down.sql b/internal/database/migrations/20251114084320_update_kandang_capacity.down.sql new file mode 100644 index 00000000..4afc4f12 --- /dev/null +++ b/internal/database/migrations/20251114084320_update_kandang_capacity.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE kandangs + DROP COLUMN IF EXISTS capacity; diff --git a/internal/database/migrations/20251114084320_update_kandang_capacity.up.sql b/internal/database/migrations/20251114084320_update_kandang_capacity.up.sql new file mode 100644 index 00000000..e1ea4410 --- /dev/null +++ b/internal/database/migrations/20251114084320_update_kandang_capacity.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE kandangs + ADD COLUMN capacity NUMERIC(15,3) NOT NULL; diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 24425917..bb72417a 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -235,13 +235,14 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users seeds := []struct { Name string Status utils.KandangStatus + Capacity float64 Location string PicKey string }{ - {Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"}, - {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"}, - {Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"}, - {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"}, + {Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"}, + {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"}, + {Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"}, + {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"}, } result := make(map[string]uint, len(seeds)) @@ -571,52 +572,44 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Flags: []utils.FlagType{utils.FlagDOC}, }, { - Name: "Ayam Afkir", - Brand: "-", - Sku: "1", - Uom: "Ekor", - Category: "Day Old Chick", - Price: 1, - - + Name: "Ayam Afkir", + Brand: "-", + Sku: "1", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 1, }, { - Name: "Ayam Mati", - Brand: "-", - Sku: "2", - Uom: "Ekor", - Category: "Day Old Chick", - Price: 1, - - + Name: "Ayam Mati", + Brand: "-", + Sku: "2", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 1, }, { - Name: "Ayam Culling", - Brand: "-", - Sku: "3", - Uom: "Ekor", - Category: "Day Old Chick", - Price: 1, - - + Name: "Ayam Culling", + Brand: "-", + Sku: "3", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 1, }, { - Name: "Telur Konsumsi Baik", - Brand: "-", - Sku: "4", - Uom: "Unit", - Category: "Telur", - Price: 1, - + Name: "Telur Konsumsi Baik", + Brand: "-", + Sku: "4", + Uom: "Unit", + Category: "Telur", + Price: 1, }, { - Name: "Telur Pecah", - Brand: "-", - Sku: "5", - Uom: "Unit", - Category: "Telur", - Price: 1, - + Name: "Telur Pecah", + Brand: "-", + Sku: "5", + Uom: "Unit", + Category: "Telur", + Price: 1, }, { Name: "281 SPECIAL STARTER", diff --git a/internal/entities/kandang.go b/internal/entities/kandang.go index 178681f0..882184b3 100644 --- a/internal/entities/kandang.go +++ b/internal/entities/kandang.go @@ -7,17 +7,18 @@ import ( ) type Kandang struct { - Id uint `gorm:"primaryKey"` - Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"` - Status string `gorm:"type:varchar(50);not null"` - LocationId uint `gorm:"not null"` - PicId uint `gorm:"not null"` - CreatedBy uint `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Location Location `gorm:"foreignKey:LocationId;references:Id"` - Pic User `gorm:"foreignKey:PicId;references:Id"` + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"` + Status string `gorm:"type:varchar(50);not null"` + LocationId uint `gorm:"not null"` + Capacity float64 `gorm:"not null"` + PicId uint `gorm:"not null"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Location Location `gorm:"foreignKey:LocationId;references:Id"` + Pic User `gorm:"foreignKey:PicId;references:Id"` ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"` } diff --git a/internal/modules/master/kandangs/dto/kandang.dto.go b/internal/modules/master/kandangs/dto/kandang.dto.go index deed483c..284ca166 100644 --- a/internal/modules/master/kandangs/dto/kandang.dto.go +++ b/internal/modules/master/kandangs/dto/kandang.dto.go @@ -14,6 +14,7 @@ type KandangBaseDTO struct { Id uint `json:"id"` Name string `json:"name"` Status string `json:"status"` + Capacity float64 `json:"capacity"` Location *locationDTO.LocationBaseDTO `json:"location"` Pic *userDTO.UserBaseDTO `json:"pic"` } @@ -48,6 +49,7 @@ func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO { Id: e.Id, Name: e.Name, Status: e.Status, + Capacity: e.Capacity, Location: location, Pic: pic, } diff --git a/internal/modules/master/kandangs/route.go b/internal/modules/master/kandangs/route.go index 6a425b64..1e384b1f 100644 --- a/internal/modules/master/kandangs/route.go +++ b/internal/modules/master/kandangs/route.go @@ -1,7 +1,7 @@ package kandangs import ( - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers" kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,7 +13,7 @@ func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService ctrl := controller.NewKandangController(s) route := v1.Group("/kandangs") - route.Use(m.Auth(u)) + // route.Use(m.Auth(u)) route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) diff --git a/internal/modules/master/kandangs/services/kandang.service.go b/internal/modules/master/kandangs/services/kandang.service.go index 1c0eed6a..e65348fc 100644 --- a/internal/modules/master/kandangs/services/kandang.service.go +++ b/internal/modules/master/kandangs/services/kandang.service.go @@ -134,6 +134,7 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit createBody := &entity.Kandang{ Name: req.Name, LocationId: req.LocationId, + Capacity: req.Capacity, Status: status, PicId: req.PicId, CreatedBy: 1, @@ -194,6 +195,10 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody["pic_id"] = *req.PicId } + if req.Capacity != nil { + updateBody["capacity"] = *req.Capacity + } + finalStatus := strings.ToUpper(existing.Status) if req.Status != nil { status := strings.ToUpper(*req.Status) diff --git a/internal/modules/master/kandangs/validations/kandang.validation.go b/internal/modules/master/kandangs/validations/kandang.validation.go index f6886991..6d7c090b 100644 --- a/internal/modules/master/kandangs/validations/kandang.validation.go +++ b/internal/modules/master/kandangs/validations/kandang.validation.go @@ -1,19 +1,21 @@ package validation type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` - Status string `json:"status,omitempty" validate:"omitempty,min=3"` - LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` - PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"` - ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"` + Name string `json:"name" validate:"required_strict,min=3"` + Status string `json:"status,omitempty" validate:"omitempty,min=3"` + Capacity float64 `json:"capacity" validate:"required_strict,gt=0"` + LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` + PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"` + ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` - Status *string `json:"status,omitempty" validate:"omitempty,min=3"` - LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` - PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"` - ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"` + Name *string `json:"name,omitempty" validate:"omitempty"` + Status *string `json:"status,omitempty" validate:"omitempty,min=3"` + Capacity *float64 `json:"capacity" validate:"omitempty,gt=0"` + LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` + PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"` + ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"` } type Query struct { diff --git a/internal/modules/master/uoms/dto/uom.dto.go b/internal/modules/master/uoms/dto/uom.dto.go index 476309b2..2e614de0 100644 --- a/internal/modules/master/uoms/dto/uom.dto.go +++ b/internal/modules/master/uoms/dto/uom.dto.go @@ -15,7 +15,8 @@ type UomBaseDTO struct { } type UomListDTO struct { - UomBaseDTO + Id uint `json:"id"` + Name string `json:"name"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -42,7 +43,8 @@ func ToUomListDTO(e entity.Uom) UomListDTO { } return UomListDTO{ - UomBaseDTO: ToUomBaseDTO(e), + Id: e.Id, + Name: e.Name, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index e8d04758..218aba4f 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -67,6 +67,7 @@ type RecordingStockDTO struct { } type RecordingEggDTO struct { + Id uint `json:"id"` ProductWarehouseId uint `json:"product_warehouse_id"` Qty int `json:"qty"` ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"` @@ -199,6 +200,7 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO { result := make([]RecordingEggDTO, len(eggs)) for i, egg := range eggs { result[i] = RecordingEggDTO{ + Id: egg.Id, ProductWarehouseId: egg.ProductWarehouseId, Qty: egg.Qty, ProductWarehouse: toRecordingProductWarehouseDTO(&egg.ProductWarehouse), diff --git a/internal/utils/error.go b/internal/utils/error.go index e63e81a2..e409e50c 100644 --- a/internal/utils/error.go +++ b/internal/utils/error.go @@ -10,8 +10,8 @@ import ( ) func ErrorHandler(c *fiber.Ctx, err error) error { - if errorsMap := validation.CustomErrorMessages(err); len(errorsMap) > 0 { - return response.Error(c, fiber.StatusBadRequest, "Bad Request", errorsMap) + if message, errorsMap := validation.CustomErrorMessages(err); len(errorsMap) > 0 { + return response.Error(c, fiber.StatusBadRequest, message, nil) } var fiberErr *fiber.Error diff --git a/tools/templates/dto.tmpl b/tools/templates/dto.tmpl index a03d7018..39b92884 100644 --- a/tools/templates/dto.tmpl +++ b/tools/templates/dto.tmpl @@ -10,15 +10,16 @@ import ( // === DTO Structs === type {{Pascal .Entity}}BaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint `json:"id"` + Name string `json:"name"` } type {{Pascal .Entity}}ListDTO struct { - {{Pascal .Entity}}BaseDTO + Id uint `json:"id"` + Name string `json:"name"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type {{Pascal .Entity}}DetailDTO struct { @@ -42,7 +43,8 @@ func To{{Pascal .Entity}}ListDTO(e entity.{{Pascal .Entity}}) {{Pascal .Entity}} } return {{Pascal .Entity}}ListDTO{ - {{Pascal .Entity}}BaseDTO: To{{Pascal .Entity}}BaseDTO(e), + Id: e.Id, + Name: e.Name, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, From 7905bdb0d71dc08fd7e68e131d7e0d940363112e Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 07:16:07 +0700 Subject: [PATCH 37/59] Feat[BE-222]: Completed SO and DO API --- .../entities/marketing_delivery_product.go | 12 +- .../marketing-delivery-products.repository.go | 2 + .../dto/product_warehouse.dto.go | 43 +- .../controllers/delivery-orders.controller.go | 43 +- .../dto/delivery-orders.dto.go | 391 +++++++-------- .../marketing/delivery-orderss/module.go | 15 +- .../marketing/delivery-orderss/route.go | 9 +- .../services/delivery-orders.service.go | 445 +++++++----------- .../validations/delivery-orders.validation.go | 14 +- .../controllers/sales-orders.controller.go | 68 +-- .../sales-orders/dto/sales-orders.dto.go | 247 ++-------- .../modules/marketing/sales-orders/module.go | 13 +- .../repositories/marketings.repository.go | 9 + .../modules/marketing/sales-orders/route.go | 7 +- .../services/sales-orders.service.go | 286 +++++------ .../validations/sales-orders.validation.go | 7 - 16 files changed, 600 insertions(+), 1011 deletions(-) diff --git a/internal/entities/marketing_delivery_product.go b/internal/entities/marketing_delivery_product.go index 85b4591a..253c00b2 100644 --- a/internal/entities/marketing_delivery_product.go +++ b/internal/entities/marketing_delivery_product.go @@ -9,13 +9,13 @@ import ( type MarketingDeliveryProduct struct { Id uint `gorm:"primaryKey;autoIncrement"` MarketingProductId uint `gorm:"uniqueIndex;not null"` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - TotalWeight float64 `gorm:"type:numeric(15,3);not null"` - AvgWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + Qty float64 `gorm:"type:numeric(15,3)"` + UnitPrice float64 `gorm:"type:numeric(15,3)"` + TotalWeight float64 `gorm:"type:numeric(15,3)"` + AvgWeight float64 `gorm:"type:numeric(15,3)"` + TotalPrice float64 `gorm:"type:numeric(15,3)"` DeliveryDate *time.Time `gorm:"type:timestamptz"` - VehicleNumber string `gorm:"type:varchar(50)"` + VehicleNumber string `gorm:"type:varchar(50)"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go index 96590990..512a5786 100644 --- a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go @@ -36,10 +36,12 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Co var deliveryProducts []entity.MarketingDeliveryProduct // Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct + // Filter: hanya ambil yang sudah memiliki delivery_date (delivery date tidak null) if err := r.DB().WithContext(ctx). Preload("MarketingProduct"). Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id"). Where("mp.marketing_id = ?", marketingId). + Where("marketing_delivery_products.delivery_date IS NOT NULL"). Order("marketing_delivery_products.id ASC"). Find(&deliveryProducts).Error; err != nil { return nil, err 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 8c9f3846..c6ac5931 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -4,6 +4,7 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" ) // === DTO Structs === @@ -15,13 +16,19 @@ type ProductWarehouseBaseDTO struct { Quantity float64 `json:"quantity"` } +type ProductWarehousNestedDTO struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` +} + type ProductWarehouseListDTO struct { ProductWarehouseBaseDTO - Product *ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` - CreatedUser *UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Product *productDTO.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 { @@ -75,6 +82,19 @@ func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDT } } +func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO { + product := productDTO.ToProductBaseDTO(e.Product) + + return ProductWarehousNestedDTO{ + Id: e.Id, + Product: &product, + Warehouse: &WarehouseBaseDTO{ + Id: e.Warehouse.Id, + Name: e.Warehouse.Name, + }, + } +} + func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO { dto := ProductWarehouseListDTO{ ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(e), @@ -84,18 +104,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT // Map Product relation jika ada if e.Product.Id != 0 { - product := ProductBaseDTO{ - Id: e.Product.Id, - Name: e.Product.Name, - } - if e.Product.Sku != nil { - product.Sku = *e.Product.Sku - } - if len(e.Product.Flags) > 0 { - for _, f := range e.Product.Flags { - product.Flags = append(product.Flags, f.Name) - } - } + product := productDTO.ToProductBaseDTO(e.Product) dto.Product = &product } diff --git a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go index 8ca51dc5..292381d0 100644 --- a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go +++ b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go @@ -39,7 +39,7 @@ func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error { } return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.DeliveryOrdersListDTO]{ + JSON(response.SuccessWithPaginate[dto.MarketingListDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get all deliveryOrderss successfully", @@ -122,44 +122,3 @@ func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error { Data: result, }) } - -func (u *DeliveryOrdersController) 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.DeliveryOrdersService.DeleteOne(c, uint(id)); err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Common{ - Code: fiber.StatusOK, - Status: "success", - Message: "Delete deliveryOrders successfully", - }) -} - -func (u *DeliveryOrdersController) Approval(c *fiber.Ctx) error { - req := new(validation.Approve) - - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") - } - - results, err := u.DeliveryOrdersService.Approval(c, req) - if err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Submit delivery order approval successfully", - Data: results, - }) -} diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go index 6008269d..be662412 100644 --- a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -7,83 +7,100 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + productwarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" + customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === DTO Structs === +type MarketingBaseDTO struct { + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` + Notes string `json:"notes,omitempty"` +} +type MarketingListDTO struct { + MarketingBaseDTO + Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` + SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` + SoDocs string `json:"so_docs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` +} + +type MarketingDetailDTO struct { + MarketingBaseDTO + Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` + SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` + SoDocs string `json:"so_docs,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` + DeliveryOrder []DeliveryGroupDTO `json:"delivery_order,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` +} type MarketingDeliveryProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date"` - VehicleNumber string `json:"vehicle_number"` + Id uint `json:"id"` + MarketingProductId uint `json:"marketing_product_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` +} + +type DeliveryItemDTO struct { + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + VehicleNumber string `json:"vehicle_number"` } -// DTO untuk grouping delivery products berdasarkan warehouse dan tanggal type DeliveryGroupDTO struct { - WarehouseId uint `json:"warehouse_id"` - WarehouseName string `json:"warehouse_name"` - DeliveryDate *time.Time `json:"delivery_date"` - VehicleNumber string `json:"vehicle_number"` - TotalQty float64 `json:"total_qty"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` -} - -// DTO untuk Delivery Order (DO) - berisi data delivery yang sudah digroup -type DeliveryOrderDTO struct { - DeliveryGroups []DeliveryGroupDTO `json:"delivery_groups"` -} - -type DeliveryOrdersBaseDTO struct { - Id uint `json:"id,omitempty"` - DeliveryNumber *string `json:"delivery_number,omitempty"` - DeliveryDate *time.Time `json:"delivery_date,omitempty"` - MarketingId uint `json:"marketing_id"` - Notes string `json:"notes,omitempty"` + DoNumber string `json:"do_number"` + DeliveryDate *time.Time `json:"delivery_date"` + Warehouse *productwarehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + Deliveries []DeliveryItemDTO `json:"deliveries"` } type MarketingProductDTO struct { - Id uint `json:"id"` - MarketingId uint `json:"marketing_id"` - ProductWarehouseId uint `json:"product_warehouse_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - // Add product relation if needed + Id uint `json:"id"` + MarketingId uint `json:"marketing_id"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` + VehicleNumber string `json:"vehicle_number,omitempty"` } -type MarketingBaseDTO struct { - Id uint `json:"id"` - SoNumber string `json:"so_number"` - SoDate time.Time `json:"so_date"` - Products []MarketingProductDTO `json:"products,omitempty"` +func ToMarketingBaseDTO(marketing *entity.Marketing) MarketingBaseDTO { + return MarketingBaseDTO{ + Id: marketing.Id, + SoNumber: marketing.SoNumber, + SoDate: marketing.SoDate, + Notes: marketing.Notes, + } } -type DeliveryOrdersListDTO struct { - DeliveryOrdersBaseDTO - SalesOrder *MarketingBaseDTO `json:"sales_order,omitempty"` // SO - Sales Order data - DeliveryOrder *DeliveryOrderDTO `json:"delivery_order,omitempty"` // DO - Delivery Order data (grouped) - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` -} - -type DeliveryOrdersDetailDTO struct { - DeliveryOrdersListDTO -} - -// === Mapper Functions === - func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { + var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO + if e.ProductWarehouse.Id != 0 { + mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse) + productWarehouse = &mapped + } + return MarketingProductDTO{ Id: e.Id, MarketingId: e.MarketingId, @@ -93,6 +110,8 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { AvgWeight: e.AvgWeight, TotalWeight: e.TotalWeight, TotalPrice: e.TotalPrice, + ProductWarehouse: productWarehouse, + VehicleNumber: getVehicleNumber(e), } } @@ -110,92 +129,67 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD } } -func ToDeliveryOrdersBaseDTO(e entity.DeliveryOrders) DeliveryOrdersBaseDTO { - var deliveryNumber *string - if e.DeliveryNumber != "" { - deliveryNumber = &e.DeliveryNumber - } - - return DeliveryOrdersBaseDTO{ - Id: e.Id, - DeliveryNumber: deliveryNumber, - DeliveryDate: &e.DeliveryDate, - MarketingId: e.MarketingId, - Notes: e.Notes, - } -} - -func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { +func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingListDTO { var createdUser *userDTO.UserBaseDTO - if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + if marketing.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser) createdUser = &mapped } - var marketing *MarketingBaseDTO - if e.Marketing != nil && e.Marketing.Id != 0 { - var marketingProducts []MarketingProductDTO - if len(e.Marketing.Products) > 0 { - marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) - for i, product := range e.Marketing.Products { - marketingProducts[i] = ToMarketingProductDTO(product) - } - } - - marketing = &MarketingBaseDTO{ - Id: e.Marketing.Id, - SoNumber: e.Marketing.SoNumber, - SoDate: e.Marketing.SoDate, - Products: marketingProducts, - } + var customer *customerDTO.CustomerBaseDTO + if marketing.Customer.Id != 0 { + mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer) + customer = &mapped } - var deliveryProductsDTOs []MarketingDeliveryProductDTO - if len(e.DeliveryProducts) > 0 { - deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(e.DeliveryProducts)) - for i, dp := range e.DeliveryProducts { - deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) - } + var salesPerson *userDTO.UserBaseDTO + if marketing.SalesPerson.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson) + salesPerson = &mapped } - // Group delivery products by warehouse and delivery date - deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + var latestApproval *approvalDTO.ApprovalBaseDTO + if marketing.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) + latestApproval = &mapped + } - // Create delivery order DTO with summary - deliveryOrder := createDeliveryOrderDTO(deliveryGroups) - - return DeliveryOrdersListDTO{ - DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - SalesOrder: marketing, - DeliveryOrder: deliveryOrder, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + return MarketingListDTO{ + MarketingBaseDTO: ToMarketingBaseDTO(marketing), + Customer: customer, + SalesPerson: salesPerson, + SoDocs: marketing.SoDocs, + CreatedUser: createdUser, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + LatestApproval: latestApproval, } } -func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProducts []entity.MarketingDeliveryProduct) DeliveryOrdersListDTO { +func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingDetailDTO { var createdUser *userDTO.UserBaseDTO - if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + if marketing.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser) createdUser = &mapped } - var marketing *MarketingBaseDTO - if e.Marketing != nil && e.Marketing.Id != 0 { - var marketingProducts []MarketingProductDTO - if len(e.Marketing.Products) > 0 { - marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) - for i, product := range e.Marketing.Products { - marketingProducts[i] = ToMarketingProductDTO(product) - } - } + var customer *customerDTO.CustomerBaseDTO + if marketing.Customer.Id != 0 { + mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer) + customer = &mapped + } - marketing = &MarketingBaseDTO{ - Id: e.Marketing.Id, - SoNumber: e.Marketing.SoNumber, - SoDate: e.Marketing.SoDate, - Products: marketingProducts, + var salesPerson *userDTO.UserBaseDTO + if marketing.SalesPerson.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson) + salesPerson = &mapped + } + + var salesOrderProducts []MarketingProductDTO + if len(marketing.Products) > 0 { + salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products)) + for i, product := range marketing.Products { + salesOrderProducts[i] = ToMarketingProductDTO(product) } } @@ -205,87 +199,108 @@ func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProduc for i, dp := range deliveryProducts { deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) } + deliveryProductsDTOs = enrichDeliveryProductDTOsWithWarehouse(deliveryProductsDTOs, marketing) } - // Group delivery products by warehouse and delivery date - deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs, marketing.SoNumber) - // Create delivery order DTO with summary - deliveryOrder := createDeliveryOrderDTO(deliveryGroups) + var latestApproval *approvalDTO.ApprovalBaseDTO + if marketing.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) + latestApproval = &mapped + } - return DeliveryOrdersListDTO{ - DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - SalesOrder: marketing, - DeliveryOrder: deliveryOrder, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + return MarketingDetailDTO{ + MarketingBaseDTO: ToMarketingBaseDTO(marketing), + SoDocs: marketing.SoDocs, + Customer: customer, + SalesPerson: salesPerson, + SalesOrder: salesOrderProducts, + DeliveryOrder: deliveryGroups, + CreatedUser: createdUser, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + LatestApproval: latestApproval, } } -func ToDeliveryOrdersListDTOs(e []entity.DeliveryOrders) []DeliveryOrdersListDTO { - result := make([]DeliveryOrdersListDTO, len(e)) - for i, r := range e { - result[i] = ToDeliveryOrdersListDTO(r) +func ToMarketingListDTOs(marketings []entity.Marketing) []MarketingListDTO { + result := make([]MarketingListDTO, len(marketings)) + for i, m := range marketings { + result[i] = ToMarketingListDTO(&m, []entity.MarketingDeliveryProduct{}) } return result } -func ToDeliveryOrdersDetailDTO(e entity.DeliveryOrders) DeliveryOrdersDetailDTO { - return DeliveryOrdersDetailDTO{ - DeliveryOrdersListDTO: ToDeliveryOrdersListDTO(e), +func enrichDeliveryProductDTOsWithWarehouse(deliveryProductDTOs []MarketingDeliveryProductDTO, marketing *entity.Marketing) []MarketingDeliveryProductDTO { + if len(deliveryProductDTOs) == 0 || marketing == nil || len(marketing.Products) == 0 { + return deliveryProductDTOs } + + productMap := make(map[uint]*entity.MarketingProduct) + for i := range marketing.Products { + productMap[marketing.Products[i].Id] = &marketing.Products[i] + } + + for i := range deliveryProductDTOs { + if product, exists := productMap[deliveryProductDTOs[i].MarketingProductId]; exists { + if product.ProductWarehouse.Id != 0 { + mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(product.ProductWarehouse) + deliveryProductDTOs[i].ProductWarehouse = &mapped + } + } + } + + return deliveryProductDTOs } -// groupDeliveryProducts groups delivery products by warehouse and delivery date -func groupDeliveryProducts(products []MarketingDeliveryProductDTO) []DeliveryGroupDTO { - // Create a map to group products +func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber string) []DeliveryGroupDTO { groupMap := make(map[string]*DeliveryGroupDTO) for _, product := range products { - // Create unique key for grouping (warehouse_id + delivery_date) - // Since we're working with DTO, we need to handle the warehouse id differently - warehouseId := uint(0) - warehouseName := "" - - // Extract warehouse info from product (assuming it might be available in future) - // For now, we'll use a simple grouping by delivery date and vehicle number - var key string - if product.DeliveryDate != nil { - key = fmt.Sprintf("%s_%s", product.DeliveryDate.Format("2006-01-02"), product.VehicleNumber) - } else { - key = fmt.Sprintf("no_date_%s", product.VehicleNumber) + if product.DeliveryDate == nil { + continue } - // Get or create group + var warehouseId uint + var warehouseName string + if product.ProductWarehouse != nil { + warehouseId = product.ProductWarehouse.Warehouse.Id + warehouseName = product.ProductWarehouse.Warehouse.Name + } + + key := fmt.Sprintf("%d_%s", warehouseId, product.DeliveryDate.Format("2006-01-02")) + group, exists := groupMap[key] if !exists { group = &DeliveryGroupDTO{ - WarehouseId: warehouseId, - WarehouseName: warehouseName, - DeliveryDate: product.DeliveryDate, - VehicleNumber: product.VehicleNumber, - TotalQty: 0, - TotalWeight: 0, - TotalPrice: 0, + DeliveryDate: product.DeliveryDate, + Warehouse: &productwarehouseDTO.WarehouseBaseDTO{ + Id: warehouseId, + Name: warehouseName, + }, + Deliveries: []DeliveryItemDTO{}, } - groupMap[key] = group } - // Update totals - group.TotalQty += product.Qty - group.TotalWeight += product.TotalWeight - group.TotalPrice += product.TotalPrice + deliveryItem := DeliveryItemDTO{ + ProductWarehouse: product.ProductWarehouse, + Qty: product.Qty, + UnitPrice: product.UnitPrice, + TotalWeight: product.TotalWeight, + AvgWeight: product.AvgWeight, + TotalPrice: product.TotalPrice, + VehicleNumber: product.VehicleNumber, + } + group.Deliveries = append(group.Deliveries, deliveryItem) } - // Convert map to slice var groups []DeliveryGroupDTO for _, group := range groupMap { groups = append(groups, *group) } - // Sort groups by delivery date sort.Slice(groups, func(i, j int) bool { if groups[i].DeliveryDate == nil || groups[j].DeliveryDate == nil { return false @@ -293,16 +308,20 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO) []DeliveryGro return groups[i].DeliveryDate.Before(*groups[j].DeliveryDate) }) + for i := range groups { + if groups[i].DeliveryDate != nil { + dateStr := groups[i].DeliveryDate.Format("20060102") + groups[i].DoNumber = fmt.Sprintf("%s-%s-%d", soNumber, dateStr, groups[i].Warehouse.Id) + } + } + return groups } -// createDeliveryOrderDTO creates delivery order DTO -func createDeliveryOrderDTO(deliveryGroups []DeliveryGroupDTO) *DeliveryOrderDTO { - if len(deliveryGroups) == 0 { - return nil - } - - return &DeliveryOrderDTO{ - DeliveryGroups: deliveryGroups, +// getVehicleNumber mengambil vehicle number dari DeliveryProduct jika ada +func getVehicleNumber(e entity.MarketingProduct) string { + if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" { + return e.DeliveryProduct.VehicleNumber } + return "" } diff --git a/internal/modules/marketing/delivery-orderss/module.go b/internal/modules/marketing/delivery-orderss/module.go index 7fcc8ccc..99bd8396 100644 --- a/internal/modules/marketing/delivery-orderss/module.go +++ b/internal/modules/marketing/delivery-orderss/module.go @@ -1,6 +1,8 @@ package delivery_orderss import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" @@ -8,26 +10,29 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" - rDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" - rMarketingProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type DeliveryOrdersModule struct{} func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { - deliveryOrdersRepo := rDeliveryOrders.NewDeliveryOrdersRepository(db) marketingRepo := rMarketing.NewMarketingRepository(db) - marketingProductRepo := rMarketingProduct.NewMarketingProductRepository(db) + marketingProductRepo := rMarketing.NewMarketingProductRepository(db) marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db) userRepo := rUser.NewUserRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalSvc := commonSvc.NewApprovalService(approvalRepo) - deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) + // Register workflow steps for MARKETINGS approval + if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err)) + } + + deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) DeliveryOrdersRoutes(router, userService, deliveryOrdersService) diff --git a/internal/modules/marketing/delivery-orderss/route.go b/internal/modules/marketing/delivery-orderss/route.go index 8d58b70e..09e48f29 100644 --- a/internal/modules/marketing/delivery-orderss/route.go +++ b/internal/modules/marketing/delivery-orderss/route.go @@ -12,6 +12,10 @@ import ( func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) { ctrl := controller.NewDeliveryOrdersController(s) + v1.Get("/", ctrl.GetAll) + v1.Get("/:id", ctrl.GetOne) + + // Sisanya di group /delivery-orders route := v1.Group("/delivery-orders") // route.Get("/", m.Auth(u), ctrl.GetAll) @@ -20,10 +24,7 @@ func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders. // 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("/approvals", ctrl.Approval) + } diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index f090ac01..45b11d6d 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -1,6 +1,7 @@ package service import ( + "context" "errors" "fmt" "time" @@ -9,8 +10,8 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" + productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto" - repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -22,18 +23,15 @@ import ( ) type DeliveryOrdersService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) - DeleteOne(ctx *fiber.Ctx, id uint) error - Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) } type deliveryOrdersService struct { Log *logrus.Logger Validate *validator.Validate - Repository repository.DeliveryOrdersRepository MarketingRepo marketingRepo.MarketingRepository MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository @@ -41,7 +39,6 @@ type deliveryOrdersService struct { } func NewDeliveryOrdersService( - repo repository.DeliveryOrdersRepository, marketingRepo marketingRepo.MarketingRepository, marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository, @@ -51,7 +48,6 @@ func NewDeliveryOrdersService( return &deliveryOrdersService{ Log: utils.Log, Validate: validate, - Repository: repo, MarketingRepo: marketingRepo, MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, @@ -60,20 +56,45 @@ func NewDeliveryOrdersService( } func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser"). - Preload("Marketing") + return db. + Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse"). + Preload("Products.DeliveryProduct") } -func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) { +func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) { + marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } + + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketingId, nil) + if err != nil { + } + marketing.LatestApproval = latestApproval + + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketingId) + if err != nil { + allDeliveryProducts = []entity.MarketingDeliveryProduct{} + } + + responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) + return &responseDTO, nil +} + +func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit - // Fetch dari Marketing, bukan DeliveryOrders marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = db.Preload("CreatedUser"). + db = db. + Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse") @@ -87,101 +108,58 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ s.Log.Errorf("Failed to get marketings: %+v", err) return nil, 0, err } - - // Load delivery products untuk setiap marketing - result := make([]dto.DeliveryOrdersListDTO, len(marketings)) - for i, marketing := range marketings { - // Get marketing delivery products menggunakan repository method - allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketing.Id) + for i := range marketings { + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, nil) if err != nil { - s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal + s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err) } + marketings[i].LatestApproval = latestApproval + } - // Build response DTO - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: marketing.Id, - CreatedUser: &marketing.CreatedUser, - Marketing: &marketing, - DeliveryProducts: allDeliveryProducts, - } - - result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + result := make([]dto.MarketingListDTO, len(marketings)) + for i, marketing := range marketings { + result[i] = dto.ToMarketingListDTO(&marketing, []entity.MarketingDeliveryProduct{}) } return result, total, nil } -func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) { - // Fetch Marketing by ID, bukan DeliveryOrders - marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser"). - Preload("Customer"). - Preload("SalesPerson"). - Preload("Products.ProductWarehouse.Product"). - Preload("Products.ProductWarehouse.Warehouse") - }) +func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) { + + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") } if err != nil { - s.Log.Errorf("Failed get marketing by id: %+v", err) return nil, err } - // Get marketing delivery products menggunakan repository method allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) if err != nil { - s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", id, err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal + allDeliveryProducts = []entity.MarketingDeliveryProduct{} } - // Debug: Log jumlah delivery products - s.Log.Infof("Found %d delivery products for marketing %d", len(allDeliveryProducts), id) + if s.ApprovalSvc != nil { + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketing.Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { - // Jika tidak ada delivery products, buat dummy data untuk testing - if len(allDeliveryProducts) == 0 && len(marketing.Products) > 0 { - s.Log.Infof("Creating dummy delivery products for testing") - for i, product := range marketing.Products { - deliveryDate := marketing.SoDate.AddDate(0, 0, i+7) // 7 hari setelah SO - dummyDeliveryProduct := entity.MarketingDeliveryProduct{ - Id: uint(i + 1), - MarketingProductId: product.Id, - Qty: product.Qty / 2, // Setengah dari qty asli - UnitPrice: product.UnitPrice, - TotalWeight: product.TotalWeight / 2, - AvgWeight: product.AvgWeight, - TotalPrice: (product.Qty / 2) * product.UnitPrice, - DeliveryDate: &deliveryDate, - VehicleNumber: fmt.Sprintf("B%04d%s", (i+1)*1000, "ABC"), + } else if len(approvals) > 0 { + if marketing.LatestApproval == nil { + latest := approvals[len(approvals)-1] + marketing.LatestApproval = &latest } - allDeliveryProducts = append(allDeliveryProducts, dummyDeliveryProduct) + } else { + marketing.LatestApproval = nil } - s.Log.Infof("Created %d dummy delivery products", len(allDeliveryProducts)) } - // Build response DTO dengan timestamps yang benar - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: marketing.Id, - CreatedUser: &marketing.CreatedUser, - Marketing: marketing, - DeliveryProducts: allDeliveryProducts, - CreatedAt: marketing.CreatedAt, - UpdatedAt: marketing.UpdatedAt, - } - - // Set delivery_date dari delivery products atau fallback ke marketing date - if len(allDeliveryProducts) > 0 && allDeliveryProducts[0].DeliveryDate != nil { - deliveryOrderResponse.DeliveryDate = *allDeliveryProducts[0].DeliveryDate - } else { - deliveryOrderResponse.DeliveryDate = marketing.SoDate - } - - responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) return &responseDTO, nil } -func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) { +func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -192,14 +170,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - var relationChecks []commonSvc.RelationCheck - for _, requestedProduct := range req.DeliveryProducts { - relationChecks = append(relationChecks, commonSvc.RelationCheck{ - Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists, - }) - } - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") @@ -210,25 +182,28 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") } + if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing") + } if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { - return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing is not approved for delivery") + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing is not approved - current status: %v", *latestApproval.Action)) } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") + } for _, requestedProduct := range req.DeliveryProducts { - allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) - } - s.Log.Errorf("Failed to fetch marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") - } - var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { @@ -248,15 +223,13 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } - var itemDeliveryDate time.Time + var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } - itemDeliveryDate = parsedDate - } else { - itemDeliveryDate = time.Now() + itemDeliveryDate = &parsedDate } deliveryProduct.Qty = requestedProduct.Qty @@ -264,24 +237,22 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalPrice = requestedProduct.TotalPrice - deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber + if requestedProduct.Qty > 0 { + if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, requestedProduct.Qty); err != nil { + return err + } + } if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { - s.Log.Errorf("Failed to update marketing delivery product: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } - s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) } - approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) actorID := uint(1) // TODO: ambil dari auth context - approvalAction := entity.ApprovalActionCreated - var notes *string - if req.Notes != "" { - notes = &req.Notes - } + approvalAction := entity.ApprovalActionApproved if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, @@ -289,9 +260,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) utils.MarketingDeliveryOrder, &approvalAction, actorID, - notes); err != nil { + nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { - s.Log.Errorf("Failed to create delivery order approval: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order approval") } } @@ -300,76 +270,38 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) }) if err != nil { - return nil, err + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order") } - marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") - }) - if err != nil { - s.Log.Errorf("Failed to fetch marketing after update: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") - } - - // Get marketing delivery products menggunakan repository method - allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), req.MarketingId) - if err != nil { - s.Log.Errorf("Failed to load delivery products: %+v", err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal - } - - // Build response DTO - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: req.MarketingId, - Notes: req.Notes, - CreatedUser: &marketing.CreatedUser, - Marketing: marketing, - DeliveryProducts: allDeliveryProducts, - } - - responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) - return &responseDTO, nil + return s.getMarketingWithDeliveries(c, req.MarketingId) } -func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) { +func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - // Validate bahwa marketing ID yang di-update ada (id parameter adalah marketing_id untuk delivery orders) if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, ); err != nil { return nil, err } - // Validate delivery products jika ada - if len(req.DeliveryProducts) > 0 { - for _, requestedProduct := range req.DeliveryProducts { - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists}, - ); err != nil { - return nil, err - } - } - } + err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) - // Update delivery products jika ada dalam request + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") + } + if len(req.DeliveryProducts) > 0 { for _, requestedProduct := range req.DeliveryProducts { - // Validate bahwa marketing product ada untuk marketing ini - allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", id)) - } - s.Log.Errorf("Failed to fetch marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") - } var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { @@ -382,7 +314,6 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) } - // Get existing delivery product deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -391,157 +322,101 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } - // Parse delivery date - var itemDeliveryDate time.Time + var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } - itemDeliveryDate = parsedDate + itemDeliveryDate = &parsedDate } else if deliveryProduct.DeliveryDate != nil { - itemDeliveryDate = *deliveryProduct.DeliveryDate - } else { - itemDeliveryDate = time.Now() + itemDeliveryDate = deliveryProduct.DeliveryDate } - // Update delivery product + oldQty := deliveryProduct.Qty deliveryProduct.Qty = requestedProduct.Qty deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalPrice = requestedProduct.TotalPrice - deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber - if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { - s.Log.Errorf("Failed to update marketing delivery product: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + qtyChange := requestedProduct.Qty - oldQty + if qtyChange > 0 { + if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil { + return err + } + } else if qtyChange < 0 { + if err := s.restoreProductWarehouseStock(c.Context(), dbTransaction, foundMarketingProduct, -qtyChange); err != nil { + return err + } } - s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) + if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + } } } return nil }) - if err != nil { - return nil, err - } - - // Fetch updated marketing with delivery products - marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") - }) - if err != nil { - s.Log.Errorf("Failed to fetch marketing after update: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") - } - - // Get marketing delivery products menggunakan repository method - allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) - if err != nil { - s.Log.Errorf("Failed to load delivery products: %+v", err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal - } - - // Build response DTO - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: id, - Notes: req.Notes, - CreatedUser: &marketing.CreatedUser, - Marketing: marketing, - DeliveryProducts: allDeliveryProducts, - } - - responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) - return &responseDTO, nil -} - -func (s deliveryOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) { - if err := s.Validate.Struct(req); err != nil { - return nil, err - } - - var action entity.ApprovalAction - switch req.Action { - case "APPROVED": - action = entity.ApprovalActionApproved - case "REJECTED": - action = entity.ApprovalActionRejected - default: - return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") - } - - approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) - if len(approvableIDs) == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") - } - - // Validate semua delivery order ada - for _, id := range approvableIDs { - _, err := s.Repository.GetByID(c.Context(), id, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) - } - if err != nil { - s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) - } - } - - err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTx)) - - for _, approvableID := range approvableIDs { - actorID := uint(1) // TODO: get from auth context - if _, err := approvalSvc.CreateApproval( - c.Context(), - utils.ApprovalWorkflowMarketing, - approvableID, - utils.MarketingDeliveryOrder, - &action, - actorID, - req.Notes, - ); err != nil { - s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") - } - } - return nil - }) - if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery order") } - updated := make([]entity.DeliveryOrders, 0, len(approvableIDs)) - for _, id := range approvableIDs { - deliveryOrder, err := s.Repository.GetByID(c.Context(), id, s.withRelations) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) - } - s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) - } - updated = append(updated, *deliveryOrder) - } - - return updated, nil + return s.getMarketingWithDeliveries(c, id) } -func (s deliveryOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { +func (s deliveryOrdersService) validateAndReduceProductWarehouse(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyDeliver float64) error { + if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { + return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") + } + + pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) + + pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) + if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") } - s.Log.Errorf("Failed to delete deliveryOrders: %+v", err) - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock") + } + + if pw.Quantity < qtyDeliver { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver)) + } + + pw.Quantity = pw.Quantity - qtyDeliver + if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") } return nil } + +func (s deliveryOrdersService) restoreProductWarehouseStock(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyRestore float64) error { + if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { + return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") + } + + pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) + pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") + } + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock") + } + + pw.Quantity = pw.Quantity + qtyRestore + if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") + } + + return nil +} diff --git a/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go index a80ad8d5..3317e952 100644 --- a/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go +++ b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go @@ -2,26 +2,22 @@ package validation type DeliveryProduct struct { MarketingProductId uint `json:"marketing_product_id" validate:"required,gt=0"` - Qty float64 `json:"qty" validate:"required,gt=0"` - UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` - AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` - TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` - TotalPrice float64 `json:"total_price" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"omitempty,gte=0"` + UnitPrice float64 `json:"unit_price" validate:"omitempty,gte=0"` + AvgWeight float64 `json:"avg_weight" validate:"omitempty,gte=0"` + TotalWeight float64 `json:"total_weight" validate:"omitempty,gte=0"` + TotalPrice float64 `json:"total_price" validate:"omitempty,gte=0"` DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"` } type Create struct { MarketingId uint `json:"marketing_id" validate:"required,gt=0"` - DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"` - Notes string `json:"notes" validate:"omitempty,max=500"` } type Update struct { - DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"` - Notes string `json:"notes" validate:"omitempty,max=500"` } type Query struct { diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index a5365787..16d3b5be 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -1,7 +1,6 @@ package controller import ( - "math" "strconv" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto" @@ -22,65 +21,6 @@ func NewSalesOrdersController(salesOrdersService service.SalesOrdersService) *Sa } } -func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { - query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), - } - - if query.Page < 1 || query.Limit < 1 { - return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") - } - - result, totalResults, err := u.SalesOrdersService.GetAll(c, query) - if err != nil { - return err - } - - // Convert marketing data to sales orders DTOs with products - salesOrdersDTOs := make([]dto.SalesOrdersListDTO, len(result)) - for i, marketing := range result { - salesOrdersDTOs[i] = dto.ToSalesOrdersListDTOFromMarketing(marketing) - } - - return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.SalesOrdersListDTO]{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get all salesOrderss successfully", - Meta: response.Meta{ - Page: query.Page, - Limit: query.Limit, - TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), - TotalResults: totalResults, - }, - Data: salesOrdersDTOs, - }) -} - -func (u *SalesOrdersController) 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.SalesOrdersService.GetOne(c, uint(id)) - if err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get salesOrders successfully", - Data: dto.ToSalesOrdersListDTOFromMarketing(*result), - }) -} - func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error { req := new(validation.Create) @@ -115,13 +55,7 @@ func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - _, err = u.SalesOrdersService.UpdateOne(c, req, uint(id)) - if err != nil { - return err - } - - // Fetch full updated data for response - result, err := u.SalesOrdersService.GetOne(c, uint(id)) + result, err := u.SalesOrdersService.UpdateOne(c, req, uint(id)) if err != nil { return err } diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 79bc6453..03a0d59a 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -4,111 +4,37 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" - customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" - productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" - warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/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" + productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" ) // === DTO Structs === -type SalesOrdersBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - type MarketingProductDTO struct { - Id uint `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - ProductWarehouse *struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - } `json:"product_warehouse,omitempty"` - DeliveryProduct *MarketingDeliveryProductDTO `json:"delivery_product,omitempty"` -} - -type MarketingDeliveryProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date"` - VehicleNumber string `json:"vehicle_number"` + Id uint `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` } type SalesOrdersListDTO struct { - SalesOrdersBaseDTO - CustomerId uint `json:"customer_id,omitempty"` - Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` - SoDate *time.Time `json:"so_date,omitempty"` - SalesPersonId uint `json:"sales_person_id,omitempty"` - Notes string `json:"notes,omitempty"` - MarketingProducts []MarketingProductDTO `json:"marketing_products,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 SalesOrdersDetailDTO struct { - SalesOrdersListDTO + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` + Notes string `json:"notes,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` } // === Mapper Functions === -func ToSalesOrdersBaseDTO(e entity.SalesOrders) SalesOrdersBaseDTO { - return SalesOrdersBaseDTO{ - Id: e.Id, - Name: e.Name, - } -} - func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { - var productWarehouse *struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - } + var productWarehouse *productWarehouseDTO.ProductWarehousNestedDTO if e.ProductWarehouse.Id != 0 { - product := (*productDTO.ProductBaseDTO)(nil) - if e.ProductWarehouse.Product.Id != 0 { - mapped := productDTO.ToProductBaseDTO(e.ProductWarehouse.Product) - product = &mapped - } - - warehouse := (*warehouseDTO.WarehouseBaseDTO)(nil) - if e.ProductWarehouse.Warehouse.Id != 0 { - mapped := warehouseDTO.ToWarehouseBaseDTO(e.ProductWarehouse.Warehouse) - warehouse = &mapped - } - - productWarehouse = &struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - }{ - Id: e.ProductWarehouse.Id, - Product: product, - Warehouse: warehouse, - } - } - - var deliveryProduct *MarketingDeliveryProductDTO - if e.DeliveryProduct != nil && e.DeliveryProduct.Id != 0 { - mapped := ToMarketingDeliveryProductDTO(*e.DeliveryProduct) - deliveryProduct = &mapped + mapped := productWarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse) + productWarehouse = &mapped } return MarketingProductDTO{ @@ -119,139 +45,38 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { TotalWeight: e.TotalWeight, TotalPrice: e.TotalPrice, ProductWarehouse: productWarehouse, - DeliveryProduct: deliveryProduct, } } -func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { - return MarketingDeliveryProductDTO{ - Id: e.Id, - MarketingProductId: e.MarketingProductId, - Qty: e.Qty, - UnitPrice: e.UnitPrice, - TotalWeight: e.TotalWeight, - AvgWeight: e.AvgWeight, - TotalPrice: e.TotalPrice, - DeliveryDate: e.DeliveryDate, - VehicleNumber: e.VehicleNumber, - } -} - -func defaultSalesOrdersLatestApproval(e entity.SalesOrders) approvalDTO.ApprovalBaseDTO { - result := approvalDTO.ApprovalBaseDTO{} - - step := utils.MarketingStepPengajuan - if step > 0 { - result.StepNumber = uint16(step) - if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { - result.StepName = label - } else if label, ok := utils.MarketingApprovalSteps[step]; ok { - result.StepName = label - } - } - - 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 ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { - var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) - createdUser = &mapped - } - - approval := defaultSalesOrdersLatestApproval(e) - if e.LatestApproval != nil { - approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) - } return SalesOrdersListDTO{ - SalesOrdersBaseDTO: ToSalesOrdersBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Approval: approval, + Id: e.Id, + SoNumber: e.Name, + SoDate: time.Time{}, + Notes: "", + SalesOrder: []MarketingProductDTO{}, } } func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO { - var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) - createdUser = &mapped - } - - var marketingProducts []MarketingProductDTO + var salesOrder []MarketingProductDTO if len(e.Products) > 0 { - marketingProducts = make([]MarketingProductDTO, len(e.Products)) + salesOrder = make([]MarketingProductDTO, len(e.Products)) for i, product := range e.Products { - marketingProducts[i] = ToMarketingProductDTO(product) + salesOrder[i] = ToMarketingProductDTO(product) } } - var customerSummary *customerDTO.CustomerBaseDTO - if e.Customer.Id != 0 { - mapped := customerDTO.ToCustomerBaseDTO(e.Customer) - customerSummary = &mapped - } - - approval := defaultSalesOrdersLatestApprovalFromMarketing(e) - if e.LatestApproval != nil { - approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) - } - return SalesOrdersListDTO{ - SalesOrdersBaseDTO: SalesOrdersBaseDTO{ - Id: e.Id, - Name: e.SoNumber, - }, - CustomerId: e.Customer.Id, - Customer: customerSummary, - SoDate: &e.SoDate, - SalesPersonId: e.SalesPersonId, - Notes: e.Notes, - MarketingProducts: marketingProducts, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Approval: approval, + Id: e.Id, + SoNumber: e.SoNumber, + SoDate: e.SoDate, + Notes: e.Notes, + SalesOrder: salesOrder, } } -func defaultSalesOrdersLatestApprovalFromMarketing(e entity.Marketing) approvalDTO.ApprovalBaseDTO { - result := approvalDTO.ApprovalBaseDTO{} - - step := utils.MarketingStepPengajuan - if step > 0 { - result.StepNumber = uint16(step) - if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { - result.StepName = label - } else if label, ok := utils.MarketingApprovalSteps[step]; ok { - result.StepName = label - } - } - - 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 ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListDTO { result := make([]SalesOrdersListDTO, len(e)) for i, r := range e { @@ -259,17 +84,3 @@ func ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListD } return result } - -func ToSalesOrdersListDTOs(e []entity.SalesOrders) []SalesOrdersListDTO { - result := make([]SalesOrdersListDTO, len(e)) - for i, r := range e { - result[i] = ToSalesOrdersListDTO(r) - } - return result -} - -func ToSalesOrdersDetailDTO(e entity.SalesOrders) SalesOrdersDetailDTO { - return SalesOrdersDetailDTO{ - SalesOrdersListDTO: ToSalesOrdersListDTO(e), - } -} diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 35d002fc..6d1963af 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -1,6 +1,8 @@ package sales_orders import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" @@ -11,10 +13,9 @@ import ( rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" - rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" - rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type SalesOrdersModule struct{} @@ -23,12 +24,16 @@ func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valida salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db) userRepo := rUser.NewUserRepository(db) customerRepo := rCustomer.NewCustomerRepository(db) - kandangRepo := rKandang.NewKandangRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) marketingRepo := rSalesOrders.NewMarketingRepository(db) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) + + if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err)) + } + + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go index f06bf401..dd0f99ab 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go @@ -11,6 +11,7 @@ import ( type MarketingRepository interface { repository.BaseRepository[entity.Marketing] IdExists(ctx context.Context, id uint) (bool, error) + GetNextSequence(ctx context.Context) (uint, error) } type MarketingRepositoryImpl struct { @@ -26,3 +27,11 @@ func NewMarketingRepository(db *gorm.DB) MarketingRepository { func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { return repository.Exists[entity.Marketing](ctx, r.DB(), id) } + +func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) { + var maxID uint + if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil { + return 0, err + } + return maxID + 1, nil +} diff --git a/internal/modules/marketing/sales-orders/route.go b/internal/modules/marketing/sales-orders/route.go index c48ae2a7..ae6d7a81 100644 --- a/internal/modules/marketing/sales-orders/route.go +++ b/internal/modules/marketing/sales-orders/route.go @@ -12,18 +12,15 @@ import ( func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) { ctrl := controller.NewSalesOrdersController(s) + v1.Delete("/:id", ctrl.DeleteOne) route := v1.Group("/sales-orders") - // 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("/approvals", ctrl.Approval) } diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 74244496..0dc47a2c 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -1,19 +1,19 @@ package service import ( + "context" "errors" "fmt" "strings" - "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" - kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -25,8 +25,6 @@ import ( ) type SalesOrdersService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.Marketing, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) DeleteOne(ctx *fiber.Ctx, id uint) error @@ -38,20 +36,18 @@ type salesOrdersService struct { Validate *validator.Validate Repository repository.SalesOrdersRepository CustomerRepo customerRepo.CustomerRepository - KandangRepo kandangRepo.KandangRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository MarketingRepo repository.MarketingRepository UserRepo userRepo.UserRepository ApprovalSvc commonSvc.ApprovalService } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, Validate: validate, Repository: repo, CustomerRepo: customerRepo, - KandangRepo: kandangRepo, ProductWarehouseRepo: productWarehouseRepo, MarketingRepo: marketingRepo, UserRepo: userRepo, @@ -60,58 +56,15 @@ func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo c } func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser"). + return db. + Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Warehouse") } -func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { - if err := s.Validate.Struct(params); err != nil { - return nil, 0, err - } - - offset := (params.Page - 1) * params.Limit - - marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) - if params.Search != "" { - return db.Where("so_number LIKE ?", "%"+params.Search+"%") - } - return db.Order("created_at DESC").Order("updated_at DESC") - }) - - if err != nil { - s.Log.Errorf("Failed to get marketings: %+v", err) - return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales orders") - } - - if s.ApprovalSvc != nil && len(marketings) > 0 { - ids := make([]uint, len(marketings)) - for i, item := range marketings { - ids[i] = item.Id - } - - latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, ids, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - s.Log.Warnf("Unable to load latest approvals for marketings: %+v", err) - } else if len(latestMap) > 0 { - for i := range marketings { - if approval, ok := latestMap[marketings[i].Id]; ok { - marketings[i].LatestApproval = approval - } - } - } - } - - return marketings, total, nil -} - -func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { - +func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") @@ -125,15 +78,9 @@ func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.Marketing, er approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) - if err != nil { - s.Log.Warnf("Unable to load approvals for marketing %d: %+v", id, err) - } else if len(approvals) > 0 { - if marketing.LatestApproval == nil { - latest := approvals[len(approvals)-1] - marketing.LatestApproval = &latest - } - } else { - marketing.LatestApproval = nil + if err == nil && len(approvals) > 0 { + latest := approvals[len(approvals)-1] + marketing.LatestApproval = &latest } } @@ -153,7 +100,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e for _, item := range req.MarketingProducts { if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Kandang", ID: &item.KandangId, Exists: s.KandangRepo.IdExists}, commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, ); err != nil { return nil, err @@ -165,14 +111,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - soNumber := "SO-" + time.Now().Format("20060102150405") + nextSeq, err := s.MarketingRepo.GetNextSequence(c.Context()) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number") + } + soNumber := fmt.Sprintf("SO-%05d", nextSeq) var marketing *entity.Marketing err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) - marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) marketing = &entity.Marketing{ @@ -184,52 +134,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e CreatedBy: 1, } if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { - s.Log.Errorf("Failed to create marketing: %+v", err) - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } if len(req.MarketingProducts) > 0 { for _, product := range req.MarketingProducts { - marketingProduct := &entity.MarketingProduct{ - MarketingId: marketing.Id, - ProductWarehouseId: product.ProductWarehouseId, - Qty: product.Qty, - UnitPrice: product.UnitPrice, - AvgWeight: product.AvgWeight, - TotalWeight: product.TotalWeight, - TotalPrice: product.TotalPrice, - } - if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { - s.Log.Errorf("Failed to create marketing product: %+v", err) - return err - } - - marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ - MarketingProductId: marketingProduct.Id, - Qty: 0, - UnitPrice: 0, - TotalWeight: 0, - AvgWeight: 0, - TotalPrice: 0, - DeliveryDate: nil, - VehicleNumber: product.VehicleNumber, - } - if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { - s.Log.Errorf("Failed to create marketing delivery product: %+v", err) - return err + if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } } actorID := uint(1) // TODO: ambil dari auth context - if err := approvalSvcTx.RegisterWorkflowSteps( - utils.ApprovalWorkflowMarketing, - utils.MarketingApprovalSteps, - ); err != nil { - s.Log.Errorf("Failed to register workflow steps: %+v", err) - return err - } - approvalAction := entity.ApprovalActionCreated if _, err := approvalSvcTx.CreateApproval( c.Context(), @@ -240,8 +156,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e actorID, nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { - s.Log.Errorf("Failed to create approval: %+v", err) - return err + fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } } @@ -256,7 +171,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e marketing, err = s.MarketingRepo.GetByID(c.Context(), marketing.Id, s.withRelations) if err != nil { - s.Log.Errorf("Failed to fetch created marketing: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created sales order") } @@ -278,7 +192,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) if err != nil { - s.Log.Errorf("Failed to check approval status: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } if latestApproval != nil && latestApproval.StepNumber >= 3 { @@ -299,8 +212,8 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) - marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction) updateBody := make(map[string]any) if req.CustomerId != 0 { @@ -330,53 +243,85 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u oldProducts, err := marketingProductRepoTx.GetByMarketingID(c.Context(), id) if err != nil && err != gorm.ErrRecordNotFound { - s.Log.Errorf("Failed to fetch old marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch existing products") } - for _, oldProduct := range oldProducts { - if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_product_id = ?", oldProduct.Id) - }); err != nil && err != gorm.ErrRecordNotFound { - s.Log.Errorf("Failed to delete delivery products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + oldByPW := make(map[uint]*entity.MarketingProduct) + for i := range oldProducts { + p := oldProducts[i] + oldByPW[p.ProductWarehouseId] = &p + } + + reqByPW := make(map[uint]validation.CreateMarketingProduct) + for _, rp := range req.MarketingProducts { + reqByPW[rp.ProductWarehouseId] = rp + } + + for _, rp := range req.MarketingProducts { + if old, ok := oldByPW[rp.ProductWarehouseId]; ok { + + updateBody := map[string]any{ + "product_warehouse_id": rp.ProductWarehouseId, + "qty": rp.Qty, + "unit_price": rp.UnitPrice, + "avg_weight": rp.AvgWeight, + "total_weight": rp.TotalWeight, + "total_price": rp.TotalPrice, + } + if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") + } + + // Ensure delivery product exists; if not, create default + if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + mdp := &entity.MarketingDeliveryProduct{ + MarketingProductId: old.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: rp.VehicleNumber, + } + if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") + } + } else { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") + } + } + } else { + // Create new marketing product (use helper) + if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") + } } } - if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_id = ?", id) - }); err != nil && err != gorm.ErrRecordNotFound { - s.Log.Errorf("Failed to delete marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") - } - - for _, product := range req.MarketingProducts { - marketingProduct := &entity.MarketingProduct{ - MarketingId: id, - ProductWarehouseId: product.ProductWarehouseId, - Qty: product.Qty, - UnitPrice: product.UnitPrice, - AvgWeight: product.AvgWeight, - TotalWeight: product.TotalWeight, - TotalPrice: product.TotalPrice, - } - if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { - - return err - } - - marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ - MarketingProductId: marketingProduct.Id, - Qty: 0, - UnitPrice: 0, - TotalWeight: 0, - AvgWeight: 0, - TotalPrice: 0, - DeliveryDate: nil, - VehicleNumber: product.VehicleNumber, - } - if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { - return err + // 2) Delete missing old products (prevent deletion if deliveries exist) + for _, old := range oldProducts { + if _, ok := reqByPW[old.ProductWarehouseId]; !ok { + // Check delivery product for this marketing product + deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") + } + if err == nil { + // If delivery exists (delivery_date not nil or qty > 0), prevent deletion + if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) + } + // safe to delete delivery product record + if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product") + } + } + // Delete marketing product + if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product") + } } } } @@ -394,7 +339,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u actorID, &resetNote) if err != nil { - s.Log.Errorf("Failed to create reset approval: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status") } } @@ -409,16 +353,16 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") } - return s.GetOne(c, id) + return s.getOne(c, id) } func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } if err != nil { - s.Log.Errorf("Failed to fetch marketing %d before delete: %+v", id, err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } @@ -494,7 +438,6 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } - if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d - sales orders must be created first", id)) } @@ -511,13 +454,12 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e } err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) for _, approvableID := range approvableIDs { - latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nil) if err != nil { - s.Log.Errorf("Failed to get latest approval for %d: %+v", approvableID, err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to check current approval step") } @@ -568,7 +510,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e updated := make([]entity.Marketing, 0, len(approvableIDs)) for _, id := range approvableIDs { - marketing, err := s.GetOne(c, id) + marketing, err := s.getOne(c, id) if err != nil { return nil, err } @@ -577,3 +519,35 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e return updated, nil } + +func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo rInvMarketingDeliveryProduct.MarketingDeliveryProductRepository) error { + + marketingProduct := &entity.MarketingProduct{ + MarketingId: marketingId, + ProductWarehouseId: rp.ProductWarehouseId, + Qty: rp.Qty, + UnitPrice: rp.UnitPrice, + AvgWeight: rp.AvgWeight, + TotalWeight: rp.TotalWeight, + TotalPrice: rp.TotalPrice, + } + if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil { + return err + } + + marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ + MarketingProductId: marketingProduct.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: rp.VehicleNumber, + } + if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil { + return err + } + + return nil +} diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go index 01b3af9d..47d2e616 100644 --- a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -10,7 +10,6 @@ type Create struct { type CreateMarketingProduct struct { VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` - KandangId uint `json:"kandang_id" validate:"required,gt=0"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` @@ -27,12 +26,6 @@ type Update struct { MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"` } -type Query struct { - 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"` -} - type Approve struct { Action string `json:"action" validate:"required_strict"` ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` From 60757237c0f03b00d2f668309c97da937086c2da Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 09:28:30 +0700 Subject: [PATCH 38/59] Feat[BE-222]: add marketing product to get all marketing for Frontend needs --- .../delivery-orderss/dto/delivery-orders.dto.go | 10 ++++++++++ .../services/delivery-orders.service.go | 9 +++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go index be662412..13a46580 100644 --- a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -24,6 +24,7 @@ type MarketingListDTO struct { Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` SoDocs string `json:"so_docs,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -154,11 +155,20 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M latestApproval = &mapped } + var salesOrderProducts []MarketingProductDTO + if len(marketing.Products) > 0 { + salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products)) + for i, product := range marketing.Products { + salesOrderProducts[i] = ToMarketingProductDTO(product) + } + } + return MarketingListDTO{ MarketingBaseDTO: ToMarketingBaseDTO(marketing), Customer: customer, SalesPerson: salesPerson, SoDocs: marketing.SoDocs, + SalesOrder: salesOrderProducts, CreatedUser: createdUser, CreatedAt: marketing.CreatedAt, UpdatedAt: marketing.UpdatedAt, diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index 45b11d6d..712c6ace 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -97,7 +97,10 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). - Preload("Products.ProductWarehouse") + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse"). + Preload("Products.DeliveryProduct") + if params.MarketingId != 0 { return db.Where("id = ?", params.MarketingId) } @@ -109,7 +112,9 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } for i := range marketings { - latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, nil) + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) if err != nil { s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err) } From 11f2389ec54ee41db4b0edda9b6b1eb1a749792a Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 17 Nov 2025 09:39:30 +0700 Subject: [PATCH 39/59] feat(BE-229,234,235,230,231,232,233): purchase request and purchase order and fix master data dto --- .../repository/common.approval.repository..go | 11 + .../20251104084540_purchase-items.up.sql | 19 +- .../20251104084555_purchases.up.sql | 24 +- internal/entities/purchase.go | 7 +- internal/entities/purchase_item.go | 13 +- .../product_warehouse.repository.go | 70 + .../master/customers/dto/customer.dto.go | 2 +- .../master/kandangs/dto/kandang.dto.go | 40 +- .../master/locations/dto/location.dto.go | 24 +- .../master/nonstocks/dto/nonstock.dto.go | 41 +- .../master/products/dto/product.dto.go | 38 +- .../repositories/product.repository.go | 12 + .../master/warehouses/dto/warehouse.dto.go | 50 +- .../controllers/purchase.controller.go | 176 ++- .../modules/purchases/dto/purchase.dto.go | 228 +-- internal/modules/purchases/module.go | 39 +- .../repositories/purchase.repository.go | 330 +++- internal/modules/purchases/route.go | 55 +- .../purchases/services/expense_bridge.go | 43 + .../purchases/services/purchase.service.go | 1332 +++++++++++++++-- .../validations/purchase.validation.go | 57 +- internal/utils/constant.go | 10 + 22 files changed, 2184 insertions(+), 437 deletions(-) create mode 100644 internal/modules/purchases/services/expense_bridge.go diff --git a/internal/common/repository/common.approval.repository..go b/internal/common/repository/common.approval.repository..go index 7f1c27ae..dc517f21 100644 --- a/internal/common/repository/common.approval.repository..go +++ b/internal/common/repository/common.approval.repository..go @@ -13,6 +13,7 @@ type ApprovalRepository interface { 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) + DeleteByTarget(ctx context.Context, workflow string, approvableID uint) error } type approvalRepositoryImpl struct { @@ -104,3 +105,13 @@ func (r *approvalRepositoryImpl) LatestByTargets( return result, nil } + +func (r *approvalRepositoryImpl) DeleteByTarget( + ctx context.Context, + workflow string, + approvableID uint, +) error { + return r.DB().WithContext(ctx). + Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID). + Delete(&entity.Approval{}).Error +} diff --git a/internal/database/migrations/20251104084540_purchase-items.up.sql b/internal/database/migrations/20251104084540_purchase-items.up.sql index a09b1d15..f1d239c2 100644 --- a/internal/database/migrations/20251104084540_purchase-items.up.sql +++ b/internal/database/migrations/20251104084540_purchase-items.up.sql @@ -9,13 +9,12 @@ CREATE TABLE IF NOT EXISTS purchase_items ( travel_number_docs VARCHAR, vehicle_number VARCHAR, sub_qty NUMERIC(15, 3) NOT NULL, - total_qty NUMERIC(15, 3) DEFAULT 0, - total_used NUMERIC(15, 3) DEFAULT 0, - price NUMERIC(15, 3) DEFAULT 0, - total_price NUMERIC(15, 3) DEFAULT 0, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - deleted_at TIMESTAMPTZ + total_qty NUMERIC(15, 3) NOT NULL DEFAULT 0, + total_used NUMERIC(15, 3) NOT NULL DEFAULT 0, + price NUMERIC(15, 3) NOT NULL DEFAULT 0, + total_price NUMERIC(15, 3) NOT NULL DEFAULT 0, + CONSTRAINT uq_purchase_items_purchase_product_warehouse + UNIQUE (purchase_id, product_id, warehouse_id) ); DO $$ @@ -46,14 +45,10 @@ BEGIN REFERENCES product_warehouses(id) ON DELETE SET NULL ON UPDATE CASCADE'; END IF; -END $$; -CREATE UNIQUE INDEX IF NOT EXISTS idx_purchase_items_unique_allocation - ON purchase_items (purchase_id, product_id, warehouse_id) - WHERE deleted_at IS NULL; +END $$; CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id); CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id); CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id); CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id); -CREATE INDEX IF NOT EXISTS idx_purchase_items_deleted_at ON purchase_items (deleted_at); diff --git a/internal/database/migrations/20251104084555_purchases.up.sql b/internal/database/migrations/20251104084555_purchases.up.sql index e42f1606..5c0a2197 100644 --- a/internal/database/migrations/20251104084555_purchases.up.sql +++ b/internal/database/migrations/20251104084555_purchases.up.sql @@ -1,17 +1,19 @@ CREATE TABLE IF NOT EXISTS purchases ( id BIGSERIAL PRIMARY KEY, pr_number VARCHAR NOT NULL, - po_number VARCHAR, - po_date TIMESTAMPTZ, + po_number VARCHAR NULL, + po_date TIMESTAMPTZ NULL, supplier_id BIGINT NOT NULL, - credit_term INT, + credit_term INT NOT NULL, due_date TIMESTAMPTZ, - grand_total NUMERIC(15, 3) DEFAULT 0, + grand_total NUMERIC(15, 3) NOT NULL, notes TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, deleted_at TIMESTAMPTZ, - created_by BIGINT NOT NULL + created_by BIGINT NOT NULL, + CONSTRAINT uq_purchases_pr_number UNIQUE (pr_number), + CONSTRAINT uq_purchases_po_number UNIQUE (po_number) ); DO $$ @@ -50,14 +52,6 @@ BEGIN END IF; END $$; -CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_pr_number_unique - ON purchases (pr_number) - WHERE deleted_at IS NULL; - -CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_po_number_unique - ON purchases (po_number) - WHERE deleted_at IS NULL AND po_number IS NOT NULL; - CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id); CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by); CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date); diff --git a/internal/entities/purchase.go b/internal/entities/purchase.go index 1a57090a..36b698b2 100644 --- a/internal/entities/purchase.go +++ b/internal/entities/purchase.go @@ -20,7 +20,8 @@ type Purchase struct { CreatedBy uint64 `gorm:"not null"` // Relations - Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"` - Items []PurchaseItem `gorm:"foreignKey:PurchaseId"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"` + Items []PurchaseItem `gorm:"foreignKey:PurchaseId"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + LatestApproval *Approval `gorm:"-" json:"-"` } diff --git a/internal/entities/purchase_item.go b/internal/entities/purchase_item.go index b092b647..59f1a030 100644 --- a/internal/entities/purchase_item.go +++ b/internal/entities/purchase_item.go @@ -14,14 +14,11 @@ type PurchaseItem struct { TravelNumber *string TravelNumberDocs *string VehicleNumber *string - SubQty float64 `gorm:"type:numeric(15,3);not null"` - TotalQty float64 `gorm:"type:numeric(15,3);default:0"` - TotalUsed float64 `gorm:"type:numeric(15,3);default:0"` - Price float64 `gorm:"type:numeric(15,3);default:0"` - TotalPrice float64 `gorm:"type:numeric(15,3);default:0"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt *time.Time `gorm:"index"` + SubQty float64 `gorm:"type:numeric(15,3);not null"` + TotalQty float64 `gorm:"type:numeric(15,3);default:0"` + TotalUsed float64 `gorm:"type:numeric(15,3);default:0"` + Price float64 `gorm:"type:numeric(15,3);default:0"` + TotalPrice float64 `gorm:"type:numeric(15,3);default:0"` // Relations Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"` diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index 599e9bc7..1eddd3b7 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "errors" "fmt" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" @@ -24,6 +25,8 @@ type ProductWarehouseRepository interface { ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) + CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error + EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint64) (uint, error) } type ProductWarehouseRepositoryImpl struct { @@ -150,6 +153,73 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d return nil } +func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error { + if len(affected) == 0 { + return nil + } + + ids := make([]uint, 0, len(affected)) + for id := range affected { + ids = append(ids, id) + } + + var emptyIDs []uint + if err := r.DB().WithContext(ctx). + Model(&entity.ProductWarehouse{}). + Where("id IN ? AND COALESCE(quantity,0) <= 0", ids). + Pluck("id", &emptyIDs).Error; err != nil { + return err + } + if len(emptyIDs) == 0 { + return nil + } + + if err := r.DB().WithContext(ctx). + Model(&entity.PurchaseItem{}). + Where("product_warehouse_id IN ?", emptyIDs). + Update("product_warehouse_id", nil).Error; err != nil { + return err + } + + if err := r.DB().WithContext(ctx). + Where("id IN ?", emptyIDs). + Delete(&entity.ProductWarehouse{}).Error; err != nil { + return err + } + + return nil +} + +func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse( + ctx context.Context, + productID uint, + warehouseID uint, + createdBy uint64, +) (uint, error) { + record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID) + if err == nil { + return record.Id, nil + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return 0, err + } + + entity := &entity.ProductWarehouse{ + ProductId: productID, + WarehouseId: warehouseID, + Quantity: 0, + CreatedBy: uint(createdBy), + } + if entity.CreatedBy == 0 { + entity.CreatedBy = 1 + } + + if err := r.CreateOne(ctx, entity, nil); err != nil { + return 0, err + } + return entity.Id, nil +} + func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) { var productWarehouse entity.ProductWarehouse err := r.DB().WithContext(ctx). diff --git a/internal/modules/master/customers/dto/customer.dto.go b/internal/modules/master/customers/dto/customer.dto.go index bd101be5..e1fb8d0d 100644 --- a/internal/modules/master/customers/dto/customer.dto.go +++ b/internal/modules/master/customers/dto/customer.dto.go @@ -20,7 +20,7 @@ type CustomerBaseDTO struct { AccountNumber string `json:"account_number"` Balance float64 `json:"balance"` - Pic *userDTO.UserBaseDTO `json:"pic"` + Pic *userDTO.UserBaseDTO `json:"pic,omitempty"` } type CustomerListDTO struct { diff --git a/internal/modules/master/kandangs/dto/kandang.dto.go b/internal/modules/master/kandangs/dto/kandang.dto.go index deed483c..0941c09d 100644 --- a/internal/modules/master/kandangs/dto/kandang.dto.go +++ b/internal/modules/master/kandangs/dto/kandang.dto.go @@ -14,15 +14,19 @@ type KandangBaseDTO struct { Id uint `json:"id"` Name string `json:"name"` Status string `json:"status"` - Location *locationDTO.LocationBaseDTO `json:"location"` - Pic *userDTO.UserBaseDTO `json:"pic"` + Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` + Pic *userDTO.UserBaseDTO `json:"pic,omitempty"` } type KandangListDTO struct { - KandangBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Location *locationDTO.LocationBaseDTO `json:"location"` + Pic *userDTO.UserBaseDTO `json:"pic"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type KandangDetailDTO struct { @@ -54,6 +58,18 @@ func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO { } func ToKandangListDTO(e entity.Kandang) KandangListDTO { + var location *locationDTO.LocationBaseDTO + if e.Location.Id != 0 { + mapped := locationDTO.ToLocationBaseDTO(e.Location) + location = &mapped + } + + var pic *userDTO.UserBaseDTO + if e.Pic.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.Pic) + pic = &mapped + } + var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(e.CreatedUser) @@ -61,10 +77,14 @@ func ToKandangListDTO(e entity.Kandang) KandangListDTO { } return KandangListDTO{ - KandangBaseDTO: ToKandangBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + Id: e.Id, + Name: e.Name, + Status: e.Status, + Location: location, + Pic: pic, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } diff --git a/internal/modules/master/locations/dto/location.dto.go b/internal/modules/master/locations/dto/location.dto.go index 6e9ec68b..d5d5b26e 100644 --- a/internal/modules/master/locations/dto/location.dto.go +++ b/internal/modules/master/locations/dto/location.dto.go @@ -14,11 +14,14 @@ type LocationBaseDTO struct { Id uint `json:"id"` Name string `json:"name"` Address string `json:"address"` - Area *areaDTO.AreaBaseDTO `json:"area"` + Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` } type LocationListDTO struct { - LocationBaseDTO + Id uint `json:"id"` + Name string `json:"name"` + Address string `json:"address"` + Area *areaDTO.AreaBaseDTO `json:"area"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -52,11 +55,20 @@ func ToLocationListDTO(e entity.Location) LocationListDTO { createdUser = &mapped } + var area *areaDTO.AreaBaseDTO + if e.Area.Id != 0 { + mapped := areaDTO.ToAreaBaseDTO(e.Area) + area = &mapped + } + return LocationListDTO{ - LocationBaseDTO: ToLocationBaseDTO(e), - CreatedUser: createdUser, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, + Id: e.Id, + Name: e.Name, + Address: e.Address, + Area: area, + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, } } diff --git a/internal/modules/master/nonstocks/dto/nonstock.dto.go b/internal/modules/master/nonstocks/dto/nonstock.dto.go index 9ab2270b..fb30b6cf 100644 --- a/internal/modules/master/nonstocks/dto/nonstock.dto.go +++ b/internal/modules/master/nonstocks/dto/nonstock.dto.go @@ -12,16 +12,17 @@ import ( // === DTO Structs === type NonstockBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - UomID uint `json:"uom_id"` + Id uint `json:"id"` + Name string `json:"name"` + Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"` + Flags []string `json:"flags"` } type NonstockListDTO struct { - NonstockBaseDTO - Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"` + Id uint `json:"id"` + Name string `json:"name"` + Uom *uomDTO.UomBaseDTO `json:"uom"` Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"` - Flags []string `json:"flags"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -35,10 +36,22 @@ type NonstockDetailDTO struct { // === Mapper Functions === func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO { + var uomRef *uomDTO.UomBaseDTO + if e.Uom.Id != 0 { + mapped := uomDTO.ToUomBaseDTO(e.Uom) + uomRef = &mapped + } + + flags := make([]string, len(e.Flags)) + for i, f := range e.Flags { + flags[i] = f.Name + } + return NonstockBaseDTO{ Id: e.Id, Name: e.Name, - UomID: e.UomId, + Uom: uomRef, + Flags: flags, } } @@ -66,13 +79,13 @@ func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO { } return NonstockListDTO{ - NonstockBaseDTO: ToNonstockBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Uom: uomRef, - Suppliers: suppliers, - Flags: flags, + Id: e.Id, + Name: e.Name, + Uom: uomRef, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Suppliers: suppliers, } } diff --git a/internal/modules/master/products/dto/product.dto.go b/internal/modules/master/products/dto/product.dto.go index b7f21008..60696fc7 100644 --- a/internal/modules/master/products/dto/product.dto.go +++ b/internal/modules/master/products/dto/product.dto.go @@ -13,8 +13,10 @@ import ( // === DTO Structs === type ProductBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint `json:"id"` + Name string `json:"name"` + Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"` + Flags []string `json:"flags"` } type ProductListDTO struct { @@ -25,10 +27,8 @@ type ProductListDTO struct { SellingPrice *float64 `json:"selling_price,omitempty"` Tax *float64 `json:"tax,omitempty"` ExpiryPeriod *int `json:"expiry_period,omitempty"` - Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"` Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"` - Flags []string `json:"flags"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -42,9 +42,22 @@ type ProductDetailDTO struct { // === Mapper Functions === func ToProductBaseDTO(e entity.Product) ProductBaseDTO { + flags := make([]string, len(e.Flags)) + for i, f := range e.Flags { + flags[i] = f.Name + } + + var uomRef *uomDTO.UomBaseDTO + if e.Uom.Id != 0 { + mapped := uomDTO.ToUomBaseDTO(e.Uom) + uomRef = &mapped + } + return ProductBaseDTO{ - Id: e.Id, - Name: e.Name, + Id: e.Id, + Name: e.Name, + Flags: flags, + Uom: uomRef, } } @@ -55,12 +68,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO { createdUser = &mapped } - var uomRef *uomDTO.UomBaseDTO - if e.Uom.Id != 0 { - mapped := uomDTO.ToUomBaseDTO(e.Uom) - uomRef = &mapped - } - var categoryRef *productCategoryDTO.ProductCategoryBaseDTO if e.ProductCategory.Id != 0 { mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory) @@ -72,11 +79,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO { suppliers[i] = supplierDTO.ToSupplierBaseDTO(s) } - flags := make([]string, len(e.Flags)) - for i, f := range e.Flags { - flags[i] = f.Name - } - return ProductListDTO{ Brand: e.Brand, Sku: e.Sku, @@ -88,10 +90,8 @@ func ToProductListDTO(e entity.Product) ProductListDTO { CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, - Uom: uomRef, ProductCategory: categoryRef, Suppliers: suppliers, - Flags: flags, } } diff --git a/internal/modules/master/products/repositories/product.repository.go b/internal/modules/master/products/repositories/product.repository.go index 06672f5f..d1cc5cb2 100644 --- a/internal/modules/master/products/repositories/product.repository.go +++ b/internal/modules/master/products/repositories/product.repository.go @@ -16,6 +16,7 @@ type ProductRepository interface { IdExists(ctx context.Context, id uint) (bool, error) CategoryExists(ctx context.Context, categoryID uint) (bool, error) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) + IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error @@ -90,6 +91,17 @@ func (r *ProductRepositoryImpl) GetSuppliersByIDs(ctx context.Context, supplierI return suppliers, nil } +func (r *ProductRepositoryImpl) IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error) { + var count int64 + if err := r.DB().WithContext(ctx). + Model(&entity.ProductSupplier{}). + Where("product_id = ? AND supplier_id = ?", productID, supplierID). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error { db := tx if db == nil { diff --git a/internal/modules/master/warehouses/dto/warehouse.dto.go b/internal/modules/master/warehouses/dto/warehouse.dto.go index b5432127..f3c3715b 100644 --- a/internal/modules/master/warehouses/dto/warehouse.dto.go +++ b/internal/modules/master/warehouses/dto/warehouse.dto.go @@ -16,16 +16,21 @@ type WarehouseBaseDTO struct { Id uint `json:"id"` Name string `json:"name"` Type string `json:"type"` - Area *areaDTO.AreaBaseDTO `json:"area"` - Location *locationDTO.LocationBaseDTO `json:"location"` - Kandang *kandangDTO.KandangBaseDTO `json:"kandang"` + Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` + Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` + Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` } type WarehouseListDTO struct { - WarehouseBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Area *areaDTO.AreaBaseDTO `json:"area"` + Location *locationDTO.LocationBaseDTO `json:"location"` + Kandang *kandangDTO.KandangBaseDTO `json:"kandang"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type WarehouseDetailDTO struct { @@ -70,11 +75,34 @@ func ToWarehouseListDTO(e entity.Warehouse) WarehouseListDTO { createdUser = &mapped } + var area *areaDTO.AreaBaseDTO + if e.Area.Id != 0 { + mapped := areaDTO.ToAreaBaseDTO(e.Area) + area = &mapped + } + + var location *locationDTO.LocationBaseDTO + if e.Location != nil && e.Location.Id != 0 { + mapped := locationDTO.ToLocationBaseDTO(*e.Location) + location = &mapped + } + + var kandang *kandangDTO.KandangBaseDTO + if e.Kandang != nil && e.Kandang.Id != 0 { + mapped := kandangDTO.ToKandangBaseDTO(*e.Kandang) + kandang = &mapped + } + return WarehouseListDTO{ - WarehouseBaseDTO: ToWarehouseBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + Id: e.Id, + Name: e.Name, + Type: e.Type, + Area: area, + Location: location, + Kandang: kandang, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } diff --git a/internal/modules/purchases/controllers/purchase.controller.go b/internal/modules/purchases/controllers/purchase.controller.go index ffef2f5d..d10f42af 100644 --- a/internal/modules/purchases/controllers/purchase.controller.go +++ b/internal/modules/purchases/controllers/purchase.controller.go @@ -1,7 +1,10 @@ package controller import ( + "fmt" + "math" "strconv" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" @@ -19,6 +22,74 @@ func NewPurchaseController(s service.PurchaseService) *PurchaseController { return &PurchaseController{service: s} } +func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error { + query := &validation.PurchaseQuery{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: strings.TrimSpace(c.Query("search")), + PrNumber: strings.TrimSpace(c.Query("pr_number")), + CreatedFrom: strings.TrimSpace(c.Query("created_from")), + CreatedTo: strings.TrimSpace(c.Query("created_to")), + } + + if supplierID := c.QueryInt("supplier_id", 0); supplierID > 0 { + query.SupplierID = uint(supplierID) + } + + if status := strings.TrimSpace(c.Query("status")); status != "" { + query.Status = strings.ToUpper(status) + } + + results, total, err := ctrl.service.GetAll(c, query) + if err != nil { + return err + } + + limit := query.Limit + if limit <= 0 { + limit = 10 + } + page := query.Page + if page <= 0 { + page = 1 + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.PurchaseListItemDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Purchase fetched successfully", + Meta: response.Meta{ + Page: page, + Limit: limit, + TotalPages: int64(math.Ceil(float64(total) / float64(limit))), + TotalResults: total, + }, + Data: dto.ToPurchaseListDTOs(results), + }) +} + +func (ctrl *PurchaseController) GetOne(c *fiber.Ctx) error { + param := c.Params("id") + id, err := strconv.ParseUint(param, 10, 64) + if err != nil || id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") + } + + result, err := ctrl.service.GetOne(c, id) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Purchase fetched successfully", + Data: dto.ToPurchaseDetailDTO(*result), + }) +} + func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error { req := new(validation.CreatePurchaseRequest) @@ -35,7 +106,7 @@ func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error { JSON(response.Success{ Code: fiber.StatusCreated, Status: "success", - Message: "Purchase requisition created successfully", + Message: "Purchase created successfully", Data: dto.ToPurchaseDetailDTO(*result), }) } @@ -44,12 +115,12 @@ func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error { param := c.Params("id") id, err := strconv.ParseUint(param, 10, 64) if err != nil || id == 0 { - return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase requisition id") + return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") } req := new(validation.ApproveStaffPurchaseRequest) if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err)) } result, err := ctrl.service.ApproveStaffPurchase(c, id, req) @@ -65,3 +136,102 @@ func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error { Data: dto.ToPurchaseDetailDTO(*result), }) } + + +func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error { + param := c.Params("id") + id, err := strconv.ParseUint(param, 10, 64) + if err != nil || id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") + } + + req := new(validation.ApproveManagerPurchaseRequest) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := ctrl.service.ApproveManagerPurchase(c, id, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Manager purchase approval recorded successfully", + Data: dto.ToPurchaseDetailDTO(*result), + }) +} + +func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error { + param := c.Params("id") + id, err := strconv.ParseUint(param, 10, 64) + if err != nil || id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") + } + + req := new(validation.ReceivePurchaseRequest) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := ctrl.service.ReceiveProducts(c, id, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Purchase receiving recorded successfully", + Data: dto.ToPurchaseDetailDTO(*result), + }) +} + +func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error { + param := c.Params("id") + id, err := strconv.ParseUint(param, 10, 64) + if err != nil || id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") + } + + req := new(validation.DeletePurchaseItemsRequest) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := ctrl.service.DeleteItems(c, id, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Purchase items deleted successfully", + Data: dto.ToPurchaseDetailDTO(*result), + }) +} + +func (ctrl *PurchaseController) DeletePurchase(c *fiber.Ctx) error { + param := c.Params("id") + id, err := strconv.ParseUint(param, 10, 64) + if err != nil || id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") + } + + if err := ctrl.service.DeletePurchase(c, id); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Purchase deleted successfully", + Data: nil, + }) +} diff --git a/internal/modules/purchases/dto/purchase.dto.go b/internal/modules/purchases/dto/purchase.dto.go index 381115a6..5a2926cf 100644 --- a/internal/modules/purchases/dto/purchase.dto.go +++ b/internal/modules/purchases/dto/purchase.dto.go @@ -4,129 +4,94 @@ 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" + locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" ) -type SupplierBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Alias string `json:"alias"` - Type string `json:"type"` - Category string `json:"category"` -} - -type AreaBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type LocationBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type WarehouseBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Area *AreaBaseDTO `json:"area,omitempty"` - Location *LocationBaseDTO `json:"location,omitempty"` -} - -type ProductBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - SKU *string `json:"sku,omitempty"` -} - -type PurchaseItemDTO struct { - Id uint64 `json:"id"` - Product *ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` - ProductWarehouseID *uint64 `json:"product_warehouse_id,omitempty"` - SubQty float64 `json:"sub_qty"` - TotalQty float64 `json:"total_qty"` - TotalUsed float64 `json:"total_used"` - Price float64 `json:"price"` - TotalPrice float64 `json:"total_price"` +type PurchaseListItemDTO struct { + Id uint64 `json:"id"` + PrNumber string `json:"pr_number"` + PoNumber *string `json:"po_number"` + Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"` + CreditTerm *int `json:"credit_term"` + DueDate *time.Time `json:"due_date"` + PoDate *time.Time `json:"po_date"` + GrandTotal float64 `json:"grand_total"` + Notes *string `json:"notes"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval"` } type PurchaseDetailDTO struct { - Id uint64 `json:"id"` - PrNumber string `json:"pr_number"` - Supplier *SupplierBaseDTO `json:"supplier,omitempty"` - CreditTerm *int `json:"credit_term,omitempty"` - DueDate *time.Time `json:"due_date,omitempty"` - GrandTotal float64 `json:"grand_total"` - Notes *string `json:"notes,omitempty"` - Items []PurchaseItemDTO `json:"items"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint64 `json:"id"` + PrNumber string `json:"pr_number"` + PoNumber *string `json:"po_number"` + Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"` + CreditTerm *int `json:"credit_term"` + DueDate *time.Time `json:"due_date"` + PoDate *time.Time `json:"po_date"` + GrandTotal float64 `json:"grand_total"` + Notes *string `json:"notes"` + Items []PurchaseItemDTO `json:"items"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval"` } -func toSupplierBaseDTO(s entity.Supplier) *SupplierBaseDTO { - if s.Id == 0 { - return nil - } - return &SupplierBaseDTO{ - Id: s.Id, - Name: s.Name, - Alias: s.Alias, - Type: s.Type, - Category: s.Category, - } -} - -func toWarehouseBaseDTO(w *entity.Warehouse) *WarehouseBaseDTO { - if w == nil || w.Id == 0 { - return nil - } - dto := &WarehouseBaseDTO{ - Id: w.Id, - Name: w.Name, - } - if w.Area.Id != 0 { - dto.Area = &AreaBaseDTO{ - Id: w.Area.Id, - Name: w.Area.Name, - } - } - if w.Location != nil && w.Location.Id != 0 { - dto.Location = &LocationBaseDTO{ - Id: w.Location.Id, - Name: w.Location.Name, - } - } - return dto -} - -func toProductBaseDTO(p *entity.Product) *ProductBaseDTO { - if p == nil || p.Id == 0 { - return nil - } - dto := &ProductBaseDTO{ - Id: p.Id, - Name: p.Name, - } - if p.Sku != nil { - dto.SKU = p.Sku - } - return dto +type PurchaseItemDTO struct { + Id uint64 `json:"id"` + ProductID uint64 `json:"product_id"` + Product *productDTO.ProductBaseDTO `json:"product"` + WarehouseID uint64 `json:"warehouse_id"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse"` + ProductWarehouseID *uint64 `json:"product_warehouse_id"` + SubQty float64 `json:"sub_qty"` + TotalQty float64 `json:"total_qty"` + TotalUsed float64 `json:"total_used"` + Price float64 `json:"price"` + TotalPrice float64 `json:"total_price"` + ReceivedDate *time.Time `json:"received_date"` + TravelNumber *string `json:"travel_number"` + TravelDocumentPath *string `json:"travel_document_path"` + VehicleNumber *string `json:"vehicle_number"` } func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO { dto := PurchaseItemDTO{ Id: item.Id, + ProductID: item.ProductId, ProductWarehouseID: item.ProductWarehouseId, + WarehouseID: item.WarehouseId, SubQty: item.SubQty, TotalQty: item.TotalQty, TotalUsed: item.TotalUsed, Price: item.Price, TotalPrice: item.TotalPrice, + ReceivedDate: item.ReceivedDate, + TravelNumber: item.TravelNumber, + TravelDocumentPath: item.TravelNumberDocs, + VehicleNumber: item.VehicleNumber, } - if item.Product != nil { - dto.Product = toProductBaseDTO(item.Product) + if item.Product != nil && item.Product.Id != 0 { + summary := productDTO.ToProductBaseDTO(*item.Product) + dto.Product = &summary } - if item.Warehouse != nil { - dto.Warehouse = toWarehouseBaseDTO(item.Warehouse) + if item.Warehouse != nil && item.Warehouse.Id != 0 { + summary := warehouseDTO.ToWarehouseBaseDTO(*item.Warehouse) + if item.Warehouse.Area.Id != 0 { + areaSummary := areaDTO.ToAreaBaseDTO(item.Warehouse.Area) + summary.Area = &areaSummary + } + if item.Warehouse.Location != nil && item.Warehouse.Location.Id != 0 { + locationSummary := locationDTO.ToLocationBaseDTO(*item.Warehouse.Location) + summary.Location = &locationSummary + } + dto.Warehouse = &summary } return dto } @@ -140,16 +105,69 @@ func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO { } func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO { - return PurchaseDetailDTO{ + dto := PurchaseDetailDTO{ Id: p.Id, PrNumber: p.PrNumber, - Supplier: toSupplierBaseDTO(p.Supplier), + PoNumber: p.PoNumber, + Supplier: mapSupplier(p.Supplier), CreditTerm: p.CreditTerm, DueDate: p.DueDate, + PoDate: p.PoDate, GrandTotal: p.GrandTotal, Notes: p.Notes, Items: ToPurchaseItemDTOs(p.Items), CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt, } + if approval := toPurchaseApprovalDTO(p); approval != nil { + dto.Approval = approval + } + return dto +} + +func ToPurchaseListDTO(p entity.Purchase) PurchaseListItemDTO { + dto := PurchaseListItemDTO{ + Id: p.Id, + PrNumber: p.PrNumber, + PoNumber: p.PoNumber, + Supplier: mapSupplier(p.Supplier), + CreditTerm: p.CreditTerm, + DueDate: p.DueDate, + PoDate: p.PoDate, + GrandTotal: p.GrandTotal, + Notes: p.Notes, + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + } + if approval := toPurchaseApprovalDTO(p); approval != nil { + dto.Approval = approval + } + return dto +} + +func mapSupplier(s entity.Supplier) *supplierDTO.SupplierBaseDTO { + if s.Id == 0 { + return nil + } + summary := supplierDTO.ToSupplierBaseDTO(s) + return &summary +} + +func ToPurchaseListDTOs(items []entity.Purchase) []PurchaseListItemDTO { + if len(items) == 0 { + return nil + } + result := make([]PurchaseListItemDTO, len(items)) + for i, item := range items { + result[i] = ToPurchaseListDTO(item) + } + return result +} + +func toPurchaseApprovalDTO(p entity.Purchase) *approvalDTO.ApprovalBaseDTO { + if p.LatestApproval == nil || p.LatestApproval.Id == 0 { + return nil + } + mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval) + return &mapped } diff --git a/internal/modules/purchases/module.go b/internal/modules/purchases/module.go index 1397f27e..de79a0c9 100644 --- a/internal/modules/purchases/module.go +++ b/internal/modules/purchases/module.go @@ -1,13 +1,50 @@ package purchases 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" + 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" + rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" + utils "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" ) type PurchaseModule struct{} func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { - RegisterRoutes(router, db, validate) + purchaseRepo := rPurchase.NewPurchaseRepository(db) + productRepo := rProduct.NewProductRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) + supplierRepo := rSupplier.NewSupplierRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err)) + } + expenseBridge := service.NewNoopPurchaseExpenseBridge() + + purchaseService := service.NewPurchaseService( + validate, + purchaseRepo, + productRepo, + warehouseRepo, + supplierRepo, + productWarehouseRepo, + approvalRepo, + approvalService, + expenseBridge, + ) + + Routes(router, purchaseService) } diff --git a/internal/modules/purchases/repositories/purchase.repository.go b/internal/modules/purchases/repositories/purchase.repository.go index 398fcea1..bc1c038a 100644 --- a/internal/modules/purchases/repositories/purchase.repository.go +++ b/internal/modules/purchases/repositories/purchase.repository.go @@ -3,17 +3,31 @@ package repositories import ( "context" "errors" + "fmt" + "strconv" + "strings" + "time" "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" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type PurchaseRepository interface { repository.BaseRepository[entity.Purchase] CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error + CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) + GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error) UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error + UpdateReceivingDetails(ctx context.Context, purchaseID uint64, updates []PurchaseReceivingUpdate) error + DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error + WithListRelations() func(*gorm.DB) *gorm.DB + UpdateGrandTotal(ctx context.Context, purchaseID uint64, grandTotal float64) error + NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) + NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) } type PurchaseRepositoryImpl struct { @@ -26,6 +40,16 @@ func NewPurchaseRepository(db *gorm.DB) PurchaseRepository { } } +type PurchaseListFilter struct { + SupplierID uint + Search string + PrNumber string + CreatedFrom *time.Time + CreatedTo *time.Time + Status *entity.ApprovalAction + CompletedOnly bool +} + func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error { db := r.DB().WithContext(ctx) @@ -47,9 +71,47 @@ func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase * return nil } +func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error { + if len(items) == 0 { + return nil + } + + for _, item := range items { + if item == nil { + continue + } + item.PurchaseId = purchaseID + } + + return r.DB().WithContext(ctx).Create(&items).Error +} + func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) { var purchase entity.Purchase err := r.DB().WithContext(ctx). + Scopes(r.withDetailRelations). + First(&purchase, id).Error + if err != nil { + return nil, err + } + return &purchase, nil +} + +func (r *PurchaseRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error) { + return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { + db = r.withListRelations(db) + return r.applyListFilters(db, filter) + }) +} + +func (r *PurchaseRepositoryImpl) WithListRelations() func(*gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return r.withListRelations(db) + } +} + +func (r *PurchaseRepositoryImpl) withDetailRelations(db *gorm.DB) *gorm.DB { + return db. Preload("Supplier"). Preload("Items", func(db *gorm.DB) *gorm.DB { return db.Order("id ASC") @@ -58,18 +120,34 @@ func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id ui Preload("Items.Warehouse"). Preload("Items.Warehouse.Area"). Preload("Items.Warehouse.Location"). - Preload("Items.ProductWarehouse"). - First(&purchase, id).Error - if err != nil { - return nil, err + Preload("Items.ProductWarehouse") +} + +func (r *PurchaseRepositoryImpl) WithDetailRelations() func(*gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return r.withDetailRelations(db) } - return &purchase, nil } type PurchasePricingUpdate struct { ItemID uint64 + ProductID *uint64 Price float64 TotalPrice float64 + Quantity *float64 + TotalQty *float64 +} + +type PurchaseReceivingUpdate struct { + ItemID uint64 + ReceivedDate *time.Time + TravelNumber *string + TravelDocumentPath *string + VehicleNumber *string + ReceivedQty *float64 + WarehouseID *uint + ProductWarehouseID *uint + ClearProductWarehouse bool } func (r *PurchaseRepositoryImpl) UpdatePricing( @@ -85,13 +163,23 @@ func (r *PurchaseRepositoryImpl) UpdatePricing( db := r.DB().WithContext(ctx) for _, upd := range updates { + data := map[string]interface{}{ + "price": upd.Price, + "total_price": upd.TotalPrice, + } + if upd.ProductID != nil { + data["product_id"] = *upd.ProductID + } + if upd.Quantity != nil { + data["sub_qty"] = *upd.Quantity + } + if upd.TotalQty != nil { + data["total_qty"] = *upd.TotalQty + } + result := db.Model(&entity.PurchaseItem{}). Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID). - Updates(map[string]interface{}{ - "price": upd.Price, - "total_price": upd.TotalPrice, - "updated_at": gorm.Expr("NOW()"), - }) + Updates(data) if result.Error != nil { return result.Error } @@ -111,3 +199,225 @@ func (r *PurchaseRepositoryImpl) UpdatePricing( return nil } + +func (r *PurchaseRepositoryImpl) UpdateReceivingDetails( + ctx context.Context, + purchaseID uint64, + updates []PurchaseReceivingUpdate, +) error { + if len(updates) == 0 { + return errors.New("receiving updates cannot be empty") + } + + db := r.DB().WithContext(ctx) + + for _, upd := range updates { + data := map[string]interface{}{} + + if upd.ReceivedDate != nil { + data["received_date"] = upd.ReceivedDate + } + if upd.TravelNumber != nil { + data["travel_number"] = upd.TravelNumber + } + if upd.TravelDocumentPath != nil { + data["travel_number_docs"] = upd.TravelDocumentPath + } + if upd.VehicleNumber != nil { + data["vehicle_number"] = upd.VehicleNumber + } + if upd.ReceivedQty != nil { + data["total_qty"] = upd.ReceivedQty + } + if upd.WarehouseID != nil && *upd.WarehouseID != 0 { + data["warehouse_id"] = upd.WarehouseID + } + + if upd.ProductWarehouseID != nil { + data["product_warehouse_id"] = *upd.ProductWarehouseID + } else if upd.ClearProductWarehouse { + data["product_warehouse_id"] = gorm.Expr("NULL") + } + + if len(data) == 0 { + continue + } + + result := db.Model(&entity.PurchaseItem{}). + Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID). + Updates(data) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + } + + return nil +} + +func (r *PurchaseRepositoryImpl) UpdateGrandTotal( + ctx context.Context, + purchaseID uint64, + grandTotal float64, +) error { + return r.DB().WithContext(ctx). + Model(&entity.Purchase{}). + Where("id = ?", purchaseID). + Updates(map[string]interface{}{ + "grand_total": grandTotal, + "updated_at": gorm.Expr("NOW()"), + }).Error +} + +func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error { + if len(itemIDs) == 0 { + return errors.New("itemIDs cannot be empty") + } + + return r.DB().WithContext(ctx). + Where("purchase_id = ? AND id IN ?", purchaseID, itemIDs). + Delete(&entity.PurchaseItem{}).Error +} + +func (r *PurchaseRepositoryImpl) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) { + return r.generateSequentialNumber(ctx, tx, "pr_number", utils.PurchasePRNumberPrefix, utils.PurchaseNumberPadding) +} + +func (r *PurchaseRepositoryImpl) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) { + return r.generateSequentialNumber(ctx, tx, "po_number", utils.PurchasePONumberPrefix, utils.PurchaseNumberPadding) +} + +func (r *PurchaseRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) { + db := tx + if db == nil { + db = r.DB() + } + + var values []string + err := db.WithContext(ctx). + Model(&entity.Purchase{}). + Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%"). + Select(column). + Order(fmt.Sprintf("%s DESC", column)). + Limit(20). + Clauses(clause.Locking{Strength: "UPDATE"}). + Pluck(column, &values).Error + if err != nil { + return "", err + } + + next := 1 + for _, value := range values { + if number, ok := parseNumericSuffix(value, prefix); ok { + next = number + 1 + break + } + } + + const maxAttempts = 20 + for attempt := 0; attempt < maxAttempts; attempt++ { + candidate := fmt.Sprintf("%s%0*d", prefix, padding, next) + exists, err := r.numberExists(ctx, db, column, candidate) + if err != nil { + return "", err + } + if !exists { + return candidate, nil + } + next++ + } + + return "", fmt.Errorf("unable to generate unique %s", column) +} + +func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) { + var count int64 + if err := db.WithContext(ctx). + Model(&entity.Purchase{}). + Where(fmt.Sprintf("%s = ?", column), value). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func parseNumericSuffix(value, prefix string) (int, bool) { + if !strings.HasPrefix(value, prefix) { + return 0, false + } + suffix := strings.TrimPrefix(value, prefix) + if suffix == "" { + return 0, false + } + trimmed := strings.TrimLeft(suffix, "0") + if trimmed == "" { + trimmed = "0" + } + number, err := strconv.Atoi(trimmed) + if err != nil { + return 0, false + } + return number, true +} + +func (r *PurchaseRepositoryImpl) withListRelations(db *gorm.DB) *gorm.DB { + return db.Preload("Supplier") +} + +func (r *PurchaseRepositoryImpl) applyListFilters(db *gorm.DB, filter *PurchaseListFilter) *gorm.DB { + if filter == nil { + return db + } + + if filter.SupplierID > 0 { + db = db.Where("purchases.supplier_id = ?", filter.SupplierID) + } + + if search := strings.ToLower(strings.TrimSpace(filter.Search)); search != "" { + like := "%" + search + "%" + db = db.Where("(LOWER(purchases.pr_number) LIKE ? OR LOWER(COALESCE(purchases.notes, '')) LIKE ?)", like, like) + } + + if pr := strings.TrimSpace(filter.PrNumber); pr != "" { + db = db.Where("purchases.pr_number ILIKE ?", "%"+pr+"%") + } + + if filter.CreatedFrom != nil { + db = db.Where("purchases.created_at >= ?", *filter.CreatedFrom) + } + + if filter.CreatedTo != nil { + db = db.Where("purchases.created_at < ?", *filter.CreatedTo) + } + + if filter.CompletedOnly { + step := uint16(utils.PurchaseStepCompleted) + db = r.applyLatestApprovalFilter(db, entity.ApprovalActionApproved, &step) + } else if filter.Status != nil { + db = r.applyLatestApprovalFilter(db, *filter.Status, nil) + } + + return db.Order("purchases.created_at DESC").Order("purchases.id DESC") +} + +func (r *PurchaseRepositoryImpl) applyLatestApprovalFilter(db *gorm.DB, action entity.ApprovalAction, minStep *uint16) *gorm.DB { + latestSub := r.DB(). + Model(&entity.Approval{}). + Select("approvable_id, MAX(action_at) AS latest_action_at"). + Where("approvable_type = ?", utils.ApprovalWorkflowPurchase.String()). + Group("approvable_id") + + db = db. + Joins("LEFT JOIN (?) AS latest_purchase_approvals ON latest_purchase_approvals.approvable_id = purchases.id", latestSub). + Joins( + "LEFT JOIN approvals ON approvals.approvable_id = purchases.id AND approvals.approvable_type = ? AND approvals.action_at = latest_purchase_approvals.latest_action_at", + utils.ApprovalWorkflowPurchase.String(), + ). + Where("approvals.action = ?", string(action)) + if minStep != nil { + db = db.Where("approvals.step_number >= ?", *minStep) + } + return db +} diff --git a/internal/modules/purchases/route.go b/internal/modules/purchases/route.go index df3ea1a1..41706b01 100644 --- a/internal/modules/purchases/route.go +++ b/internal/modules/purchases/route.go @@ -1,59 +1,22 @@ package purchases 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" - rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" - rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" - rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers" - rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" - rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" - sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" - utils "gitlab.com/mbugroup/lti-api.git/internal/utils" - "gorm.io/gorm" ) -func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { - group := router.Group("/purchases") +func Routes(router fiber.Router, purchaseService service.PurchaseService) { + ctrl := controller.NewPurchaseController(purchaseService) - purchaseRepo := rPurchase.NewPurchaseRepository(db) - productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) - warehouseRepo := rWarehouse.NewWarehouseRepository(db) - supplierRepo := rSupplier.NewSupplierRepository(db) - userRepo := rUser.NewUserRepository(db) - - approvalRepo := commonRepo.NewApprovalRepository(db) - approvalService := commonSvc.NewApprovalService(approvalRepo) - if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil { - panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err)) - } - - purchaseService := service.NewPurchaseService( - validate, - purchaseRepo, - productWarehouseRepo, - warehouseRepo, - supplierRepo, - approvalRepo, - ) - userService := sUser.NewUserService(userRepo, validate) - - PurchaseRoutes(group, userService, purchaseService) -} - -func PurchaseRoutes(v1 fiber.Router, u sUser.UserService, s service.PurchaseService) { - ctrl := controller.NewPurchaseController(s) - - route := v1.Group("/requisitions") - - // route.Post("/", m.Auth(u), ctrl.CreateOne) + route := router.Group("/purchases") + route.Get("/", ctrl.GetAll) + route.Get("/:id", ctrl.GetOne) route.Post("/", ctrl.CreateOne) route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase) + route.Post("/:id/approvals/manager", ctrl.ApproveManagerPurchase) + route.Post("/:id/receipts", ctrl.ReceiveProducts) + route.Delete("/:id", ctrl.DeletePurchase) + route.Delete("/:id/items", ctrl.DeleteItems) } diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go new file mode 100644 index 00000000..b7c96d03 --- /dev/null +++ b/internal/modules/purchases/services/expense_bridge.go @@ -0,0 +1,43 @@ +package service + +import ( + "context" + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +// PurchaseExpenseBridge defines hooks that allow purchase flows to stay in sync with expense data once it exists. +type PurchaseExpenseBridge interface { + OnItemsCreated(ctx context.Context, purchaseID uint64, items []entity.PurchaseItem) error + OnItemsDeleted(ctx context.Context, purchaseID uint64, itemIDs []uint64) error + OnItemsReceived(ctx context.Context, purchaseID uint64, updates []ExpenseReceivingPayload) error +} + +// ExpenseReceivingPayload captures the minimum data expense integration will need once available. +type ExpenseReceivingPayload struct { + PurchaseItemID uint64 + ProductID uint64 + WarehouseID uint64 + ReceivedQty float64 + ReceivedDate *time.Time +} + +// noopPurchaseExpenseBridge is the default implementation until the expense module is ready. +type noopPurchaseExpenseBridge struct{} + +func NewNoopPurchaseExpenseBridge() PurchaseExpenseBridge { + return &noopPurchaseExpenseBridge{} +} + +func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint64, _ []entity.PurchaseItem) error { + return nil +} + +func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint64, _ []uint64) error { + return nil +} + +func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint64, _ []ExpenseReceivingPayload) error { + return nil +} diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index a7419fc5..2082e195 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -1,63 +1,165 @@ package service import ( + "context" "errors" "fmt" + "math" + "strings" "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" 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" rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations" "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" - "github.com/google/uuid" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type PurchaseService interface { + GetAll(ctx *fiber.Ctx, params *validation.PurchaseQuery) ([]entity.Purchase, int64, error) + GetOne(ctx *fiber.Ctx, id uint64) (*entity.Purchase, error) CreateOne(ctx *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) ApproveStaffPurchase(ctx *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error) + ApproveManagerPurchase(ctx *fiber.Ctx, id uint64, req *validation.ApproveManagerPurchaseRequest) (*entity.Purchase, error) + ReceiveProducts(ctx *fiber.Ctx, id uint64, req *validation.ReceivePurchaseRequest) (*entity.Purchase, error) + DeleteItems(ctx *fiber.Ctx, id uint64, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) + DeletePurchase(ctx *fiber.Ctx, id uint64) error } +const ( + priceTolerance = 0.0001 + queryDateLayout = "2006-01-02" +) + type purchaseService struct { Log *logrus.Logger Validate *validator.Validate PurchaseRepo rPurchase.PurchaseRepository - ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + ProductRepo rProduct.ProductRepository WarehouseRepo rWarehouse.WarehouseRepository SupplierRepo rSupplier.SupplierRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository ApprovalRepo commonRepo.ApprovalRepository + ApprovalSvc commonSvc.ApprovalService + ExpenseBridge PurchaseExpenseBridge +} + +type staffAdjustmentPayload struct { + PricingUpdates []rPurchase.PurchasePricingUpdate + NewItems []*entity.PurchaseItem + GrandTotal float64 } func NewPurchaseService( validate *validator.Validate, purchaseRepo rPurchase.PurchaseRepository, - productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, + productRepo rProduct.ProductRepository, warehouseRepo rWarehouse.WarehouseRepository, supplierRepo rSupplier.SupplierRepository, + productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, approvalRepo commonRepo.ApprovalRepository, + approvalSvc commonSvc.ApprovalService, + expenseBridge PurchaseExpenseBridge, ) PurchaseService { + if expenseBridge == nil { + expenseBridge = NewNoopPurchaseExpenseBridge() + } return &purchaseService{ Log: utils.Log, Validate: validate, PurchaseRepo: purchaseRepo, - ProductWarehouseRepo: productWarehouseRepo, + ProductRepo: productRepo, WarehouseRepo: warehouseRepo, SupplierRepo: supplierRepo, + ProductWarehouseRepo: productWarehouseRepo, ApprovalRepo: approvalRepo, + ApprovalSvc: approvalSvc, + ExpenseBridge: expenseBridge, } } -func uint64Ptr(v uint64) *uint64 { - return &v +func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.PurchaseQuery) ([]entity.Purchase, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + limit := params.Limit + if limit <= 0 { + limit = 10 + } + page := params.Page + if page <= 0 { + page = 1 + } + offset := (page - 1) * limit + + ctx := c.Context() + + createdFrom, createdTo, err := parseQueryDates(params.CreatedFrom, params.CreatedTo) + if err != nil { + return nil, 0, err + } + + statusAction, completedOnly, err := parseApprovalAction(params.Status) + if err != nil { + return nil, 0, err + } + + filter := &rPurchase.PurchaseListFilter{ + SupplierID: params.SupplierID, + Search: params.Search, + PrNumber: params.PrNumber, + CreatedFrom: createdFrom, + CreatedTo: createdTo, + Status: statusAction, + CompletedOnly: completedOnly, + } + + purchases, total, err := s.PurchaseRepo.GetAllWithFilters(ctx, offset, limit, filter) + if err != nil { + s.Log.Errorf("Failed to get purchases: %+v", err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchases") + } + + if err := s.attachLatestApprovals(ctx, purchases); err != nil { + s.Log.Warnf("Unable to attach latest approvals to purchases: %+v", err) + } + + return purchases, total, nil +} + +func (s *purchaseService) GetOne(c *fiber.Ctx, id uint64) (*entity.Purchase, error) { + if id == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") + } + + ctx := c.Context() + + purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found") + } + s.Log.Errorf("Failed to get purchase: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase") + } + + if err := s.attachLatestApproval(ctx, purchase); err != nil { + s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err) + } + + return purchase, nil } func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) { @@ -65,8 +167,9 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase return nil, err } - supplier, err := s.SupplierRepo.GetByID(c.Context(), req.SupplierID, nil) - if err != nil { + ctx := c.Context() + + if _, err := s.SupplierRepo.GetByID(ctx, req.SupplierID, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found") } @@ -74,73 +177,60 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier") } - warehouse, err := s.WarehouseRepo.GetDetailByID(c.Context(), req.WarehouseID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") - } - s.Log.Errorf("Failed to get warehouse: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") - } - - if warehouse.AreaId != req.AreaID { - return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided area") - } - if warehouse.LocationId == nil || *warehouse.LocationId != req.LocationID { - return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided location") - } - type aggregatedItem struct { - productId uint64 - warehouseId uint64 - productWarehouseId *uint64 - subQty float64 + productId uint64 + warehouseId uint64 + subQty float64 } if len(req.Items) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty") } + warehouseCache := make(map[uint]*entity.Warehouse) + productSupplierCache := make(map[uint]bool) + + getWarehouse := func(id uint) (*entity.Warehouse, error) { + if warehouse, ok := warehouseCache[id]; ok { + return warehouse, nil + } + + warehouse, err := s.WarehouseRepo.GetDetailByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse %d not found", id)) + } + s.Log.Errorf("Failed to get warehouse %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") + } + + warehouseCache[id] = warehouse + return warehouse, nil + } + aggregated := make([]*aggregatedItem, 0, len(req.Items)) indexMap := make(map[string]int) for _, item := range req.Items { - var ( - productId = uint64(item.ProductID) - warehouseId = uint64(req.WarehouseID) - productWarehouseId *uint64 - ) - - if item.ProductWarehouseID != nil { - productWarehouse, err := s.ProductWarehouseRepo.GetDetailByID(c.Context(), *item.ProductWarehouseID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", *item.ProductWarehouseID)) - } - s.Log.Errorf("Failed to get product warehouse %d: %+v", *item.ProductWarehouseID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") - } - - if productWarehouse.WarehouseId != req.WarehouseID { - return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse does not match selected warehouse") - } - - productId = uint64(productWarehouse.ProductId) - warehouseId = uint64(productWarehouse.WarehouseId) - idCopy := uint64(productWarehouse.Id) - productWarehouseId = &idCopy - } else { - productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), item.ProductID, req.WarehouseID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Failed to get product warehouse for product %d and warehouse %d: %+v", item.ProductID, req.WarehouseID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") - } - if err == nil { - idCopy := uint64(productWarehouse.Id) - productWarehouseId = &idCopy - } + if _, err := getWarehouse(item.WarehouseID); err != nil { + return nil, err } + if _, checked := productSupplierCache[item.ProductID]; !checked { + linked, err := s.ProductRepo.IsLinkedToSupplier(ctx, item.ProductID, req.SupplierID) + if err != nil { + s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", item.ProductID, req.SupplierID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier") + } + if !linked { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product %d is not linked to supplier %d", item.ProductID, req.SupplierID)) + } + productSupplierCache[item.ProductID] = true + } + + productId := uint64(item.ProductID) + warehouseId := uint64(item.WarehouseID) + key := fmt.Sprintf("%d:%d", productId, warehouseId) if idx, ok := indexMap[key]; ok { aggregated[idx].subQty += item.Quantity @@ -148,29 +238,20 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase } entry := &aggregatedItem{ - productId: productId, - warehouseId: warehouseId, - productWarehouseId: productWarehouseId, - subQty: item.Quantity, + productId: productId, + warehouseId: warehouseId, + subQty: item.Quantity, } aggregated = append(aggregated, entry) indexMap[key] = len(aggregated) - 1 } - prNumber := fmt.Sprintf("PR-%s-%s", time.Now().Format("20060102"), uuid.NewString()[:8]) - - var creditTerm *int - var dueDate *time.Time - - if supplier.DueDate > 0 { - ct := supplier.DueDate - creditTerm = &ct - d := time.Now().UTC().AddDate(0, 0, ct) - dueDate = &d - } + creditTermValue := req.CreditTerm + creditTerm := &creditTermValue + dueDateValue := time.Now().UTC().AddDate(0, 0, creditTermValue) + dueDate := &dueDateValue purchase := &entity.Purchase{ - PrNumber: prNumber, SupplierId: uint64(req.SupplierID), CreditTerm: creditTerm, DueDate: dueDate, @@ -182,20 +263,25 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase items := make([]*entity.PurchaseItem, 0, len(aggregated)) for _, item := range aggregated { items = append(items, &entity.PurchaseItem{ - ProductId: item.productId, - WarehouseId: item.warehouseId, - ProductWarehouseId: item.productWarehouseId, - SubQty: item.subQty, - TotalQty: item.subQty, - TotalUsed: 0, - Price: 0, - TotalPrice: 0, + ProductId: item.productId, + WarehouseId: item.warehouseId, + SubQty: item.subQty, + TotalQty: 0, + TotalUsed: 0, + Price: 0, + TotalPrice: 0, }) } - ctx := c.Context() transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { purchaseRepoTx := rPurchase.NewPurchaseRepository(tx) + + code, err := purchaseRepoTx.NextPrNumber(ctx, tx) + if err != nil { + return err + } + purchase.PrNumber = code + if err := purchaseRepoTx.CreateWithItems(ctx, purchase, items); err != nil { return err } @@ -205,37 +291,184 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase actorID = 1 } action := entity.ApprovalActionCreated - - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) - if _, err := approvalSvc.CreateApproval( - ctx, - utils.ApprovalWorkflowPurchase, - uint(purchase.Id), - utils.PurchaseStepPengajuan, - &action, - actorID, - nil, - ); err != nil { + if err := s.createPurchaseApproval(ctx, tx, purchase.Id, utils.PurchaseStepPengajuan, action, actorID, nil, false); err != nil { return err } return nil }) if transactionErr != nil { - s.Log.Errorf("Failed to create purchase requisition: %+v", transactionErr) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create purchase requisition") + s.Log.Errorf("Failed to create purchase: %+v", transactionErr) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create purchase") } created, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id) if err != nil { - s.Log.Errorf("Failed to load created purchase requisition: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition") + s.Log.Errorf("Failed to load created purchase: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase") } + if err := s.attachLatestApproval(ctx, created); err != nil { + s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", created.Id, err) + } + + s.notifyExpenseItemsCreated(ctx, created.Id, created.Items) + return created, nil } func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error) { + return s.processStaffPurchaseApproval(c, id, req, false) +} + +func (s *purchaseService) processStaffPurchaseApproval(c *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest, requireStaffApproval bool) (*entity.Purchase, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + actorID := uint(1) // TODO: replace with authenticated user id once available + + ctx := c.Context() + purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found") + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase") + } + + if err := s.attachLatestApproval(ctx, purchase); err != nil { + s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err) + } + + var latestStep uint16 + if purchase.LatestApproval != nil { + latestStep = purchase.LatestApproval.StepNumber + } + + if requireStaffApproval && latestStep < uint16(utils.PurchaseStepStaffPurchase) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase cannot be edited before staff approval") + } + + isInitialApproval := latestStep < uint16(utils.PurchaseStepStaffPurchase) + if isInitialApproval && latestStep != uint16(utils.PurchaseStepPengajuan) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase is not ready for staff approval") + } + + // Detect if purchase already has any receiving data on its items. + hasReceivingData := false + for _, item := range purchase.Items { + if item.TotalQty > 0 || item.TotalUsed > 0 { + hasReceivingData = true + break + } + } + + // After there is receiving data, staff edits are allowed to change quantity + // in sync with receiving, without creating new receiving approvals here. + syncReceiving := !isInitialApproval && hasReceivingData + + payload, err := s.buildStaffAdjustmentPayload(ctx, purchase, req, syncReceiving) + if err != nil { + return nil, err + } + + transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + purchaseRepoTx := rPurchase.NewPurchaseRepository(tx) + grandTotalUpdated := false + if len(payload.PricingUpdates) > 0 { + if err := purchaseRepoTx.UpdatePricing(ctx, purchase.Id, payload.PricingUpdates, payload.GrandTotal); err != nil { + return err + } + grandTotalUpdated = true + } + + if len(payload.NewItems) > 0 { + if err := purchaseRepoTx.CreateItems(ctx, purchase.Id, payload.NewItems); err != nil { + return err + } + } + + if !grandTotalUpdated { + if err := purchaseRepoTx.UpdateGrandTotal(ctx, purchase.Id, payload.GrandTotal); err != nil { + return err + } + } + + if isInitialApproval { + action := entity.ApprovalActionApproved + if err := s.createPurchaseApproval(ctx, tx, purchase.Id, utils.PurchaseStepStaffPurchase, action, actorID, req.Notes, false); err != nil { + return err + } + return nil + } + + if len(payload.PricingUpdates) > 0 || len(payload.NewItems) > 0 { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + if approvalSvc != nil { + latest, err := approvalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowPurchase, uint(purchase.Id), nil) + if err != nil { + return err + } + + shouldRecordStaffUpdate := latest == nil || + latest.StepNumber != uint16(utils.PurchaseStepStaffPurchase) || + latest.Action == nil || + (latest.Action != nil && *latest.Action != entity.ApprovalActionUpdated) + + if shouldRecordStaffUpdate { + action := entity.ApprovalActionUpdated + if _, err := approvalSvc.CreateApproval( + ctx, + utils.ApprovalWorkflowPurchase, + uint(purchase.Id), + utils.PurchaseStepStaffPurchase, + &action, + actorID, + req.Notes, + ); err != nil { + return err + } + } + } + } + + return nil + }) + if transactionErr != nil { + if errors.Is(transactionErr, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found") + } + if isInitialApproval { + s.Log.Errorf("Failed to approve purchase %d: %+v", purchase.Id, transactionErr) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to approve purchase") + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update purchase pricing") + } + + updated, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase") + } + if err := s.attachLatestApproval(ctx, updated); err != nil { + s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) + } + + if len(payload.NewItems) > 0 { + newItems := make([]entity.PurchaseItem, len(payload.NewItems)) + for i, item := range payload.NewItems { + if item == nil { + continue + } + newItems[i] = *item + } + s.notifyExpenseItemsCreated(ctx, purchase.Id, newItems) + } + + return updated, nil +} + +func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint64, req *validation.ApproveManagerPurchaseRequest) (*entity.Purchase, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -245,75 +478,381 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint64, req *val purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Purchase requisition not found") + return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found") } - s.Log.Errorf("Failed to get purchase requisition: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase requisition") + s.Log.Errorf("Failed to get purchase: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase") } - if len(purchase.Items) == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase requisition has no items to approve") + if err := s.attachLatestApproval(ctx, purchase); err != nil { + s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err) } - requestItems := make(map[uint64]validation.StaffPurchaseApprovalItem, len(req.Items)) - for _, item := range req.Items { - requestItems[item.PurchaseItemID] = item - } - - updates := make([]rPurchase.PurchasePricingUpdate, 0, len(purchase.Items)) - var grandTotal float64 - - for _, item := range purchase.Items { - data, ok := requestItems[item.Id] - if !ok { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id)) - } - delete(requestItems, item.Id) - - if data.Price <= 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Price for item %d must be greater than 0", item.Id)) - } - - totalPrice := data.TotalPrice - if totalPrice == nil { - calculated := data.Price * item.TotalQty - totalPrice = &calculated - } - if *totalPrice <= 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for item %d must be greater than 0", item.Id)) - } - - updates = append(updates, rPurchase.PurchasePricingUpdate{ - ItemID: item.Id, - Price: data.Price, - TotalPrice: *totalPrice, - }) - grandTotal += *totalPrice - } - - if len(requestItems) > 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase requisition") + if purchase.LatestApproval == nil || + purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepStaffPurchase) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must reach staff purchase step before manager approval") } + actorID := uint(1) action := entity.ApprovalActionApproved - actorID := uint(1) // TODO: replace with authenticated user id once available + now := time.Now().UTC() + hasExistingPO := purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" + var generatedNumber string transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { - purchaseRepoTx := rPurchase.NewPurchaseRepository(tx) - if err := purchaseRepoTx.UpdatePricing(ctx, purchase.Id, updates, grandTotal); err != nil { + updateData := map[string]any{} + if !hasExistingPO { + repoTx := rPurchase.NewPurchaseRepository(tx) + code, err := repoTx.NextPoNumber(ctx, tx) + if err != nil { + return err + } + updateData["po_number"] = code + updateData["po_date"] = now + generatedNumber = code + } + + if len(updateData) > 0 { + repoTx := rPurchase.NewPurchaseRepository(tx) + if err := repoTx.PatchOne(ctx, uint(purchase.Id), updateData, nil); err != nil { + return err + } + } + + forceManagerApproval := false + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + if approvalSvcTx != nil { + filterByStep := func(step approvalutils.ApprovalStep) func(*gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return db.Where("step_number = ?", uint16(step)) + } + } + latestStaff, err := approvalSvcTx.LatestByTarget(ctx, utils.ApprovalWorkflowPurchase, uint(purchase.Id), filterByStep(utils.PurchaseStepStaffPurchase)) + if err != nil { + return err + } + latestManager, err := approvalSvcTx.LatestByTarget(ctx, utils.ApprovalWorkflowPurchase, uint(purchase.Id), filterByStep(utils.PurchaseStepManager)) + if err != nil { + return err + } + if latestStaff != nil && latestManager != nil && latestStaff.ActionAt.After(latestManager.ActionAt) { + forceManagerApproval = true + } + } + + if err := s.createPurchaseApproval(ctx, tx, purchase.Id, utils.PurchaseStepManager, action, actorID, req.Notes, forceManagerApproval); err != nil { return err } - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) - if _, err := approvalSvc.CreateApproval( - ctx, - utils.ApprovalWorkflowPurchase, - uint(purchase.Id), - utils.PurchaseStepStaffPurchase, - &action, - actorID, - req.Notes, - ); err != nil { + return nil + }) + if transactionErr != nil { + s.Log.Errorf("Failed to approve manager purchase %d: %+v", purchase.Id, transactionErr) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate purchase order") + } + + if generatedNumber != "" { + purchase.PoNumber = &generatedNumber + purchase.PoDate = &now + } + + updated, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id) + if err != nil { + s.Log.Errorf("Failed to load purchase after manager approval: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase") + } + + if err := s.attachLatestApproval(ctx, updated); err != nil { + s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) + } + + return updated, nil +} + +func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validation.ReceivePurchaseRequest) (*entity.Purchase, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + ctx := c.Context() + + purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found") + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase ") + } + + if purchase.PoNumber == nil || strings.TrimSpace(*purchase.PoNumber) == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase order has not been generated") + } + + if err := s.attachLatestApproval(ctx, purchase); err != nil { + s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err) + } + + if purchase.LatestApproval == nil || + purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepManager) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must be approved by manager before receiving products") + } + + itemMap := make(map[uint64]*entity.PurchaseItem, len(purchase.Items)) + for i := range purchase.Items { + itemMap[purchase.Items[i].Id] = &purchase.Items[i] + } + + type preparedReceiving struct { + item *entity.PurchaseItem + payload validation.ReceivePurchaseItemRequest + receivedDate time.Time + warehouseID uint + overrideWarehouse bool + receivedQty float64 + } + + prepared := make([]preparedReceiving, 0, len(req.Items)) + for _, payload := range req.Items { + item, exists := itemMap[payload.PurchaseItemID] + if !exists { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID)) + } + + receivedDate, err := time.Parse("2006-01-02", payload.ReceivedDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID)) + } + receivedDate = receivedDate.UTC() + + warehouseID := uint(item.WarehouseId) + overrideWarehouse := false + if payload.WarehouseID != nil && *payload.WarehouseID != 0 { + warehouseID = *payload.WarehouseID + overrideWarehouse = true + } + if warehouseID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse must be specified for item %d", payload.PurchaseItemID)) + } + + var receivedQty float64 + if payload.ReceivedQty != nil { + receivedQty = *payload.ReceivedQty + } else { + receivedQty = item.SubQty + } + if receivedQty < 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d cannot be negative", payload.PurchaseItemID)) + } + if receivedQty > item.SubQty { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty)) + } + + prepared = append(prepared, preparedReceiving{ + item: item, + payload: payload, + receivedDate: receivedDate, + warehouseID: warehouseID, + overrideWarehouse: overrideWarehouse, + receivedQty: receivedQty, + }) + } + + receivingAction := entity.ApprovalActionApproved + completedAction := entity.ApprovalActionApproved + actorID := uint(1) + + approvalSvc := s.approvalServiceForDB(nil) + if approvalSvc != nil { + filterStep := func(step approvalutils.ApprovalStep) func(*gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return db.Where("step_number = ?", uint16(step)) + } + } + + latestReceiving, err := approvalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowPurchase, uint(purchase.Id), filterStep(utils.PurchaseStepReceiving)) + if err != nil { + s.Log.Errorf("Failed to inspect receiving approval for purchase %d: %+v", purchase.Id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record purchase receiving") + } + + if latestReceiving != nil { + receivingAction = entity.ApprovalActionUpdated + } + } + + transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + repoTx := rPurchase.NewPurchaseRepository(tx) + pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx) + + deltas := make(map[uint]float64) + affected := make(map[uint]struct{}) + updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared)) + + for _, prep := range prepared { + item := prep.item + + var oldPWID *uint + if item.ProductWarehouseId != nil { + idCopy := uint(*item.ProductWarehouseId) + oldPWID = &idCopy + } + + var newPWID *uint + clearPW := false + + if prep.receivedQty > 0 { + pwID, err := pwRepoTx.EnsureProductWarehouse(ctx, uint(item.ProductId), prep.warehouseID, purchase.CreatedBy) + if err != nil { + return err + } + newPWID = &pwID + deltas[pwID] += prep.receivedQty + affected[pwID] = struct{}{} + } else { + clearPW = true + } + + if oldPWID != nil { + deltas[*oldPWID] -= item.TotalQty + affected[*oldPWID] = struct{}{} + } + + dateCopy := prep.receivedDate + qtyCopy := prep.receivedQty + update := rPurchase.PurchaseReceivingUpdate{ + ItemID: item.Id, + ReceivedDate: &dateCopy, + TravelNumber: prep.payload.TravelNumber, + TravelDocumentPath: prep.payload.TravelDocumentPath, + VehicleNumber: prep.payload.VehicleNumber, + ReceivedQty: &qtyCopy, + ProductWarehouseID: newPWID, + ClearProductWarehouse: clearPW, + } + + if prep.overrideWarehouse || uint(item.WarehouseId) != prep.warehouseID { + warehouseCopy := prep.warehouseID + update.WarehouseID = &warehouseCopy + } + + updates = append(updates, update) + } + + if err := repoTx.UpdateReceivingDetails(ctx, purchase.Id, updates); err != nil { + return err + } + + if err := pwRepoTx.AdjustQuantities(ctx, deltas, nil); err != nil { + return err + } + + if err := pwRepoTx.CleanupEmpty(ctx, affected); err != nil { + return err + } + + if err := s.createPurchaseApproval(ctx, tx, purchase.Id, utils.PurchaseStepReceiving, receivingAction, actorID, req.Notes, true); err != nil { + return err + } + + if err := s.createPurchaseApproval(ctx, tx, purchase.Id, utils.PurchaseStepCompleted, completedAction, actorID, req.Notes, true); err != nil { + return err + } + + return nil + }) + if transactionErr != nil { + if errors.Is(transactionErr, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found for receiving") + } + s.Log.Errorf("Failed to save purchase receiving %d: %+v", purchase.Id, transactionErr) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record purchase receiving") + } + + updated, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase ") + } + if err := s.attachLatestApproval(ctx, updated); err != nil { + s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) + } + + receivingPayloads := make([]ExpenseReceivingPayload, 0, len(prepared)) + for _, prep := range prepared { + date := prep.receivedDate + payload := ExpenseReceivingPayload{ + PurchaseItemID: prep.item.Id, + ProductID: prep.item.ProductId, + WarehouseID: uint64(prep.warehouseID), + ReceivedQty: prep.receivedQty, + ReceivedDate: &date, + } + receivingPayloads = append(receivingPayloads, payload) + } + s.notifyExpenseItemsReceived(ctx, purchase.Id, receivingPayloads) + + return updated, nil +} + +func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint64, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + ctx := c.Context() + purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found") + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase") + } + + if err := s.attachLatestApproval(ctx, purchase); err != nil { + s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err) + } + if purchase.LatestApproval == nil || purchase.LatestApproval.StepNumber == uint16(utils.PurchaseStepPengajuan) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase cannot delete items before staff purchase approval") + } + + if len(purchase.Items) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to delete") + } + + requested := make(map[uint64]struct{}, len(req.ItemIDs)) + for _, id := range req.ItemIDs { + requested[id] = struct{}{} + } + + toDelete := make([]uint64, 0, len(req.ItemIDs)) + var remainingTotal float64 + + for _, item := range purchase.Items { + if _, ok := requested[item.Id]; ok { + if item.TotalQty > 0 || item.TotalUsed > 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete item %d because it already has receiving data", item.Id)) + } + toDelete = append(toDelete, item.Id) + } else { + remainingTotal += item.TotalPrice + } + } + + if len(toDelete) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Requested items were not found in this purchase") + } + + if len(purchase.Items)-len(toDelete) <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must keep at least one item") + } + + transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + repoTx := rPurchase.NewPurchaseRepository(tx) + + if err := repoTx.DeleteItems(ctx, purchase.Id, toDelete); err != nil { + return err + } + + if err := repoTx.UpdateGrandTotal(ctx, purchase.Id, remainingTotal); err != nil { return err } @@ -323,15 +862,484 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint64, req *val if errors.Is(transactionErr, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found") } - s.Log.Errorf("Failed to approve purchase requisition %d: %+v", purchase.Id, transactionErr) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to approve purchase requisition") + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase items") + } + + if len(toDelete) > 0 { + s.notifyExpenseItemsDeleted(ctx, purchase.Id, toDelete) } updated, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id) if err != nil { - s.Log.Errorf("Failed to load purchase requisition after approval: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition") + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase") + } + if err := s.attachLatestApproval(ctx, updated); err != nil { + s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) } return updated, nil } + +func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint64) error { + if id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") + } + + ctx := c.Context() + purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Purchase not found") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase") + } + + itemIDs := make([]uint64, 0, len(purchase.Items)) + for _, item := range purchase.Items { + itemIDs = append(itemIDs, item.Id) + } + + transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + approvalRepoTx := commonRepo.NewApprovalRepository(tx) + if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowPurchase.String(), uint(id)); err != nil { + return err + } + + purchaseRepoTx := rPurchase.NewPurchaseRepository(tx) + if err := purchaseRepoTx.DeleteOne(ctx, uint(id)); err != nil { + return err + } + return nil + }) + if transactionErr != nil { + if errors.Is(transactionErr, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Purchase not found") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase") + } + + if len(itemIDs) > 0 { + s.notifyExpenseItemsDeleted(ctx, uint64(id), itemIDs) + } + + return nil +} + +func (s *purchaseService) createPurchaseApproval( + ctx context.Context, + db *gorm.DB, + purchaseID uint64, + step approvalutils.ApprovalStep, + action entity.ApprovalAction, + actorID uint, + notes *string, + allowDuplicate bool, +) error { + if purchaseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Purchase is invalid for approval") + } + if actorID == 0 { + actorID = 1 + } + + svc := s.approvalServiceForDB(db) + + modifier := func(db *gorm.DB) *gorm.DB { + return db.Where("step_number = ?", uint16(step)) + } + + latest, err := svc.LatestByTarget(ctx, utils.ApprovalWorkflowPurchase, uint(purchaseID), modifier) + if err != nil { + return err + } + + if !allowDuplicate && latest != nil && + latest.Action != nil && + *latest.Action == action { + return nil + } + + actionCopy := action + _, err = svc.CreateApproval(ctx, utils.ApprovalWorkflowPurchase, uint(purchaseID), step, &actionCopy, actorID, notes) + return err +} + +func (s *purchaseService) approvalServiceForDB(db *gorm.DB) commonSvc.ApprovalService { + if db != nil { + return commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) + } + if s.ApprovalSvc != nil { + return s.ApprovalSvc + } + return commonSvc.NewApprovalService(s.ApprovalRepo) +} + +func (s *purchaseService) attachLatestApprovals(ctx context.Context, items []entity.Purchase) error { + if len(items) == 0 || s.ApprovalSvc == nil { + return nil + } + + ids := make([]uint, 0, len(items)) + visited := make(map[uint64]struct{}, len(items)) + for _, item := range items { + if item.Id == 0 { + continue + } + if _, ok := visited[item.Id]; ok { + continue + } + visited[item.Id] = struct{}{} + ids = append(ids, uint(item.Id)) + } + + if len(ids) == 0 { + return nil + } + + latestMap, err := s.ApprovalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowPurchase, ids, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + return err + } + + for i := range items { + if items[i].Id == 0 { + continue + } + if approval, ok := latestMap[uint(items[i].Id)]; ok { + items[i].LatestApproval = approval + } else { + items[i].LatestApproval = nil + } + } + + return nil +} + +func (s *purchaseService) notifyExpenseItemsCreated(ctx context.Context, purchaseID uint64, items []entity.PurchaseItem) { + if s.ExpenseBridge == nil || purchaseID == 0 || len(items) == 0 { + return + } + if err := s.ExpenseBridge.OnItemsCreated(ctx, purchaseID, items); err != nil { + s.Log.Warnf("Failed to notify expense bridge for created purchase %d: %+v", purchaseID, err) + } +} + +func (s *purchaseService) notifyExpenseItemsReceived(ctx context.Context, purchaseID uint64, payloads []ExpenseReceivingPayload) { + if s.ExpenseBridge == nil || purchaseID == 0 || len(payloads) == 0 { + return + } + if err := s.ExpenseBridge.OnItemsReceived(ctx, purchaseID, payloads); err != nil { + s.Log.Warnf("Failed to notify expense bridge for received purchase %d: %+v", purchaseID, err) + } +} + +func (s *purchaseService) notifyExpenseItemsDeleted(ctx context.Context, purchaseID uint64, itemIDs []uint64) { + if s.ExpenseBridge == nil || purchaseID == 0 || len(itemIDs) == 0 { + return + } + if err := s.ExpenseBridge.OnItemsDeleted(ctx, purchaseID, itemIDs); err != nil { + s.Log.Warnf("Failed to notify expense bridge for deleted purchase %d: %+v", purchaseID, err) + } +} + +func (s *purchaseService) buildStaffAdjustmentPayload( + ctx context.Context, + purchase *entity.Purchase, + req *validation.ApproveStaffPurchaseRequest, + syncReceiving bool, +) (*staffAdjustmentPayload, error) { + if len(req.Items) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty") + } + + requestItems := make(map[uint64]validation.StaffPurchaseApprovalItem, len(req.Items)) + newPayloads := make([]validation.StaffPurchaseApprovalItem, 0) + + for _, item := range req.Items { + if item.PurchaseItemID == 0 { + newPayloads = append(newPayloads, item) + continue + } + if _, exists := requestItems[item.PurchaseItemID]; exists { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate pricing data for item %d", item.PurchaseItemID)) + } + requestItems[item.PurchaseItemID] = item + } + + updates := make([]rPurchase.PurchasePricingUpdate, 0, len(purchase.Items)) + var grandTotal float64 + + existingCombos := make(map[string]struct{}, len(purchase.Items)+len(newPayloads)) + for _, item := range purchase.Items { + key := fmt.Sprintf("%d:%d", item.ProductId, item.WarehouseId) + existingCombos[key] = struct{}{} + } + + allowedWarehouses := make(map[uint64]struct{}, len(purchase.Items)) + for _, item := range purchase.Items { + allowedWarehouses[item.WarehouseId] = struct{}{} + } + if len(allowedWarehouses) == 0 && len(newPayloads) > 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "No available warehouses for this purchase") + } + + productSupplierCache := make(map[uint64]bool) + + for _, item := range purchase.Items { + data, ok := requestItems[item.Id] + if !ok { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id)) + } + if data.WarehouseID != 0 && data.WarehouseID != item.WarehouseId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse mismatch for item %d", item.Id)) + } + + effectiveProductID := item.ProductId + if data.ProductID != 0 && data.ProductID != item.ProductId { + if item.TotalQty > 0 || item.TotalUsed > 0 { + return nil, fiber.NewError( + fiber.StatusBadRequest, + fmt.Sprintf("Cannot change product for item %d because it already has receiving data", item.Id), + ) + } + + if _, checked := productSupplierCache[data.ProductID]; !checked { + linked, err := s.ProductRepo.IsLinkedToSupplier(ctx, uint(data.ProductID), uint(purchase.SupplierId)) + if err != nil { + s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", data.ProductID, purchase.SupplierId, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier") + } + if !linked { + return nil, fiber.NewError( + fiber.StatusBadRequest, + fmt.Sprintf("Product %d is not linked to supplier %d", data.ProductID, purchase.SupplierId), + ) + } + productSupplierCache[data.ProductID] = true + } + + oldKey := fmt.Sprintf("%d:%d", item.ProductId, item.WarehouseId) + newKey := fmt.Sprintf("%d:%d", data.ProductID, item.WarehouseId) + if _, exists := existingCombos[newKey]; exists && newKey != oldKey { + return nil, fiber.NewError( + fiber.StatusBadRequest, + fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", data.ProductID, item.WarehouseId), + ) + } + if newKey != oldKey { + delete(existingCombos, oldKey) + existingCombos[newKey] = struct{}{} + } + + effectiveProductID = data.ProductID + } + + effectiveQty := item.SubQty + if data.Qty != nil { + if *data.Qty <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for item %d must be greater than 0", item.Id)) + } + if item.TotalUsed > 0 && *data.Qty < item.TotalUsed { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for item %d cannot be lower than used amount (%.3f)", item.Id, item.TotalUsed)) + } + if (item.TotalQty > 0 || item.TotalUsed > 0) && !syncReceiving { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot change quantity for item %d because it already has receiving data", item.Id)) + } + effectiveQty = *data.Qty + } + + totalPriceInput := data.TotalPrice + totalPrice, err := calculateTotalPrice(effectiveQty, data.Price, &totalPriceInput, fmt.Sprintf("item %d", item.Id)) + if err != nil { + return nil, err + } + + update := rPurchase.PurchasePricingUpdate{ + ItemID: item.Id, + Price: data.Price, + TotalPrice: totalPrice, + } + if effectiveProductID != item.ProductId { + productIDCopy := effectiveProductID + update.ProductID = &productIDCopy + } + if data.Qty != nil { + qtyCopy := effectiveQty + update.Quantity = &qtyCopy + } + if syncReceiving { + qtyCopy := effectiveQty + update.TotalQty = &qtyCopy + } + + updates = append(updates, update) + grandTotal += totalPrice + delete(requestItems, item.Id) + } + if len(requestItems) > 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase") + } + + newItems := make([]*entity.PurchaseItem, 0, len(newPayloads)) + + for _, payload := range newPayloads { + if payload.ProductID == 0 || payload.WarehouseID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Product and warehouse must be provided for new items") + } + if payload.Qty == nil || *payload.Qty <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity must be greater than 0 for product %d", payload.ProductID)) + } + if _, ok := allowedWarehouses[payload.WarehouseID]; !ok { + return nil, fiber.NewError( + fiber.StatusBadRequest, + fmt.Sprintf("Warehouse %d is not available for this purchase", payload.WarehouseID), + ) + } + + key := fmt.Sprintf("%d:%d", payload.ProductID, payload.WarehouseID) + if _, exists := existingCombos[key]; exists { + return nil, fiber.NewError( + fiber.StatusBadRequest, + fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", payload.ProductID, payload.WarehouseID), + ) + } + + if _, checked := productSupplierCache[payload.ProductID]; !checked { + linked, err := s.ProductRepo.IsLinkedToSupplier(ctx, uint(payload.ProductID), uint(purchase.SupplierId)) + if err != nil { + s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", payload.ProductID, purchase.SupplierId, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier") + } + if !linked { + return nil, fiber.NewError( + fiber.StatusBadRequest, + fmt.Sprintf("Product %d is not linked to supplier %d", payload.ProductID, purchase.SupplierId), + ) + } + productSupplierCache[payload.ProductID] = true + } + + qty := *payload.Qty + totalPriceInput := payload.TotalPrice + totalPrice, err := calculateTotalPrice(qty, payload.Price, &totalPriceInput, fmt.Sprintf("product %d in warehouse %d", payload.ProductID, payload.WarehouseID)) + if err != nil { + return nil, err + } + + newItem := &entity.PurchaseItem{ + PurchaseId: purchase.Id, + ProductId: payload.ProductID, + WarehouseId: payload.WarehouseID, + SubQty: qty, + TotalQty: 0, + TotalUsed: 0, + Price: payload.Price, + TotalPrice: totalPrice, + } + newItems = append(newItems, newItem) + existingCombos[key] = struct{}{} + grandTotal += totalPrice + } + + if len(updates) == 0 && len(newItems) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to process") + } + + return &staffAdjustmentPayload{ + PricingUpdates: updates, + NewItems: newItems, + GrandTotal: grandTotal, + }, nil +} + +func calculateTotalPrice(quantity float64, price float64, provided *float64, ref string) (float64, error) { + if quantity <= 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for %s must be greater than 0", ref)) + } + if price <= 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Price for %s must be greater than 0", ref)) + } + + fmt.Println(price, quantity) + expectedTotal := price * quantity + + if provided == nil { + return expectedTotal, nil + } + if *provided <= 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for %s must be greater than 0", ref)) + } + if math.Abs(*provided-expectedTotal) > priceTolerance { + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for %s must equal quantity x price", ref)) + } + return *provided, nil +} + +func (s *purchaseService) attachLatestApproval(ctx context.Context, item *entity.Purchase) error { + if item == nil || item.Id == 0 || s.ApprovalSvc == nil { + return nil + } + + latest, err := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowPurchase, uint(item.Id), func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + return err + } + + item.LatestApproval = latest + return nil +} + +func parseQueryDates(fromStr, toStr string) (*time.Time, *time.Time, error) { + var fromPtr *time.Time + var toPtr *time.Time + + if strings.TrimSpace(fromStr) != "" { + parsed, err := time.Parse(queryDateLayout, fromStr) + if err != nil { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "created_from must use format YYYY-MM-DD") + } + fromValue := parsed + fromPtr = &fromValue + } + + if strings.TrimSpace(toStr) != "" { + parsed, err := time.Parse(queryDateLayout, toStr) + if err != nil { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "created_to must use format YYYY-MM-DD") + } + toValue := parsed.AddDate(0, 0, 1) + toPtr = &toValue + } + + if fromPtr != nil && toPtr != nil && fromPtr.After(*toPtr) { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "created_from must be earlier than created_to") + } + + return fromPtr, toPtr, nil +} + +func parseApprovalAction(status string) (*entity.ApprovalAction, bool, error) { + value := strings.TrimSpace(strings.ToUpper(status)) + if value == "" { + return nil, false, nil + } + + if value == "COMPLETED" { + return nil, true, nil + } + + action := entity.ApprovalAction(value) + switch action { + case entity.ApprovalActionApproved, + entity.ApprovalActionRejected, + entity.ApprovalActionCreated, + entity.ApprovalActionUpdated: + return &action, false, nil + default: + return nil, false, fiber.NewError(fiber.StatusBadRequest, "Invalid status filter") + } +} diff --git a/internal/modules/purchases/validations/purchase.validation.go b/internal/modules/purchases/validations/purchase.validation.go index 8791fc1c..3660263d 100644 --- a/internal/modules/purchases/validations/purchase.validation.go +++ b/internal/modules/purchases/validations/purchase.validation.go @@ -1,27 +1,62 @@ package validation type PurchaseItemPayload struct { - ProductID uint `json:"product_id" validate:"required"` - ProductWarehouseID *uint `json:"product_warehouse_id,omitempty" validate:"omitempty,gt=0"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"` + ProductID uint `json:"product_id" validate:"required,gt=0"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` } type CreatePurchaseRequest struct { - SupplierID uint `json:"supplier_id" validate:"required"` - AreaID uint `json:"area_id" validate:"required"` - LocationID uint `json:"location_id" validate:"required"` - WarehouseID uint `json:"warehouse_id" validate:"required"` - Notes *string `json:"notes" validate:"omitempty,max=500"` - Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"` + SupplierID uint `json:"supplier_id" validate:"required,gt=0"` + CreditTerm int `json:"credit_term" validate:"required,gte=0"` + Notes *string `json:"notes" validate:"omitempty,max=500"` + Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"` } type StaffPurchaseApprovalItem struct { - PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"` + PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"` + ProductID uint64 `json:"product_id" validate:"required,gt=0"` + WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` + Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` Price float64 `json:"price" validate:"required,gt=0"` - TotalPrice *float64 `json:"total_price,omitempty" validate:"omitempty,gt=0"` + TotalPrice float64 `json:"total_price" validate:"required,gt=0"` } type ApproveStaffPurchaseRequest struct { Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` } + +type ApproveManagerPurchaseRequest struct { + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} + +type ReceivePurchaseItemRequest struct { + PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"` + WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"` + ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"` + TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"` + TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"` + VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"` + ReceivedQty *float64 `json:"received_qty" validate:"omitempty,gte=0"` +} + +type ReceivePurchaseRequest struct { + Items []ReceivePurchaseItemRequest `json:"items" validate:"required,min=1,dive"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} + +type DeletePurchaseItemsRequest struct { + ItemIDs []uint64 `json:"item_ids" validate:"required,min=1,dive,gt=0"` +} + +type PurchaseQuery struct { + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + PrNumber string `query:"pr_number" validate:"omitempty,max=50"` + CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"` + CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"` + Status string `query:"status" validate:"omitempty,oneof=CREATED UPDATED APPROVED REJECTED COMPLETED"` +} diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 099b6510..dad324b9 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -217,11 +217,21 @@ const ( ApprovalWorkflowPurchase approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PURCHASES") PurchaseStepPengajuan approvalutils.ApprovalStep = 1 PurchaseStepStaffPurchase approvalutils.ApprovalStep = 2 + PurchaseStepManager approvalutils.ApprovalStep = 3 + PurchaseStepReceiving approvalutils.ApprovalStep = 4 + PurchaseStepCompleted approvalutils.ApprovalStep = 5 + + PurchasePRNumberPrefix = "PR-LTI-" + PurchasePONumberPrefix = "PO-LTI-" + PurchaseNumberPadding = 4 ) var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{ PurchaseStepPengajuan: "Pengajuan", PurchaseStepStaffPurchase: "Staff Purchase", + PurchaseStepManager: "Manager Purchase", + PurchaseStepReceiving: "Penerimaan Produk", + PurchaseStepCompleted: "Selesai", } // ------------------------------------------------------------------- From cb1df12b7e9714f28028c8e3995cfb49b1b597c7 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 11:03:15 +0700 Subject: [PATCH 40/59] Feat[BE-260]: create BOP migration --- ...51117034511_create_expenses_table.down.sql | 1 + ...0251117034511_create_expenses_table.up.sql | 44 ++++++++++++++++ ...29_create_expense_nonstocks_table.down.sql | 1 + ...4529_create_expense_nonstocks_table.up.sql | 50 +++++++++++++++++++ ...create_expense_realizations_table.down.sql | 1 + ...8_create_expense_realizations_table.up.sql | 40 +++++++++++++++ 6 files changed, 137 insertions(+) create mode 100644 internal/database/migrations/20251117034511_create_expenses_table.down.sql create mode 100644 internal/database/migrations/20251117034511_create_expenses_table.up.sql create mode 100644 internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql create mode 100644 internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql create mode 100644 internal/database/migrations/20251117034538_create_expense_realizations_table.down.sql create mode 100644 internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql diff --git a/internal/database/migrations/20251117034511_create_expenses_table.down.sql b/internal/database/migrations/20251117034511_create_expenses_table.down.sql new file mode 100644 index 00000000..bf0ea945 --- /dev/null +++ b/internal/database/migrations/20251117034511_create_expenses_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS expenses; \ No newline at end of file diff --git a/internal/database/migrations/20251117034511_create_expenses_table.up.sql b/internal/database/migrations/20251117034511_create_expenses_table.up.sql new file mode 100644 index 00000000..054084e4 --- /dev/null +++ b/internal/database/migrations/20251117034511_create_expenses_table.up.sql @@ -0,0 +1,44 @@ +CREATE TABLE expenses ( + id BIGSERIAL PRIMARY KEY, + reference_number VARCHAR, -- format => BOP-LTI-0001 = 0001 is increment + supplier_id BIGINT NULL, + category VARCHAR(50) NOT NULL CHECK ( + category IN ('BOP', 'NON-BOP') + ), + po_number VARCHAR(50) UNIQUE NOT NULL, + document_path JSON, + expense_date DATE NOT NULL, + grand_total NUMERIC(15, 3) DEFAULT 0, + note TEXT, + created_by BIGINT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- Tambahkan Foreign Key ke suppliers +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN + ALTER TABLE expenses + ADD CONSTRAINT fk_expenses_supplier_id + FOREIGN KEY (supplier_id) REFERENCES suppliers(id); + END IF; +END $$; + +-- Tambahkan Foreign Key ke users (created_by) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE expenses + ADD CONSTRAINT fk_expenses_created_by + FOREIGN KEY (created_by) REFERENCES users(id); + END IF; +END $$; + +-- Index +CREATE INDEX idx_expenses_supplier_id ON expenses (supplier_id); + +CREATE INDEX idx_expenses_expense_date ON expenses (expense_date); + +CREATE INDEX idx_expenses_deleted_at ON expenses (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql new file mode 100644 index 00000000..70fcf148 --- /dev/null +++ b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS expense_nonstocks; \ No newline at end of file diff --git a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql new file mode 100644 index 00000000..5b0c2c16 --- /dev/null +++ b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql @@ -0,0 +1,50 @@ +CREATE TABLE expense_nonstocks ( + id BIGSERIAL PRIMARY KEY, + expense_id BIGINT, + project_flock_kandang_id BIGINT, + nonstock_id BIGINT, + qty NUMERIC(15, 3) NOT NULL, + unit_price NUMERIC(15, 3) NOT NULL, + total_price NUMERIC(15, 3) NOT NULL, + note TEXT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- Tambahkan Foreign Key ke expenses +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expenses') THEN + ALTER TABLE expense_nonstocks + ADD CONSTRAINT fk_expense_nonstocks_expense_id + FOREIGN KEY (expense_id) REFERENCES expenses(id); + END IF; +END $$; + +-- Tambahkan Foreign Key ke project_flock_kandangs +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE expense_nonstocks + ADD CONSTRAINT fk_expense_nonstocks_kandang_id + FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs(id); + END IF; +END $$; + +-- Tambahkan Foreign Key ke nonstocks +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN + ALTER TABLE expense_nonstocks + ADD CONSTRAINT fk_expense_nonstocks_nonstock_id + FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id); + END IF; +END $$; + +-- Index +CREATE INDEX idx_expense_nonstocks_expense_id ON expense_nonstocks (expense_id); + +CREATE INDEX idx_expense_nonstocks_nonstock_id ON expense_nonstocks (nonstock_id); + +CREATE INDEX idx_expense_nonstocks_deleted_at ON expense_nonstocks (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251117034538_create_expense_realizations_table.down.sql b/internal/database/migrations/20251117034538_create_expense_realizations_table.down.sql new file mode 100644 index 00000000..5f70a0e6 --- /dev/null +++ b/internal/database/migrations/20251117034538_create_expense_realizations_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS expense_realizations; \ No newline at end of file diff --git a/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql b/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql new file mode 100644 index 00000000..4a8dc148 --- /dev/null +++ b/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql @@ -0,0 +1,40 @@ +CREATE TABLE expense_realizations ( + id BIGSERIAL PRIMARY KEY, + expense_nonstock_id BIGINT, + realization_qty NUMERIC(15, 3) NOT NULL, + realization_unit_price NUMERIC(15, 3) NOT NULL, + realization_total_price NUMERIC(15, 3) NOT NULL, + realization_date DATE NOT NULL, + note TEXT, + created_by BIGINT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- Tambahkan Foreign Key ke expense_nonstocks +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN + ALTER TABLE expense_realizations + ADD CONSTRAINT fk_expense_realizations_nonstock_id + FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id); + END IF; +END $$; + +-- Tambahkan Foreign Key ke users (created_by) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE expense_realizations + ADD CONSTRAINT fk_expense_realizations_created_by + FOREIGN KEY (created_by) REFERENCES users(id); + END IF; +END $$; + +-- Index +CREATE INDEX idx_expense_realizations_nonstock_id ON expense_realizations (expense_nonstock_id); + +CREATE INDEX idx_expense_realizations_date ON expense_realizations (realization_date); + +CREATE INDEX idx_expense_realizations_deleted_at ON expense_realizations (deleted_at); \ No newline at end of file From 0708628b78e61fc82d304c57de81190c78e77d43 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 17 Nov 2025 11:53:27 +0700 Subject: [PATCH 41/59] feat(BE-229,234,235,230,231,232,233): purchase request and purchase order and fix master data dto --- .../purchases/services/purchase.service.go | 68 ++++++------------- .../validations/purchase.validation.go | 15 ++-- 2 files changed, 27 insertions(+), 56 deletions(-) diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 2082e195..7f694a12 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -355,7 +355,6 @@ func (s *purchaseService) processStaffPurchaseApproval(c *fiber.Ctx, id uint64, return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase is not ready for staff approval") } - // Detect if purchase already has any receiving data on its items. hasReceivingData := false for _, item := range purchase.Items { if item.TotalQty > 0 || item.TotalUsed > 0 { @@ -364,8 +363,6 @@ func (s *purchaseService) processStaffPurchaseApproval(c *fiber.Ctx, id uint64, } } - // After there is receiving data, staff edits are allowed to change quantity - // in sync with receiving, without creating new receiving approvals here. syncReceiving := !isInitialApproval && hasReceivingData payload, err := s.buildStaffAdjustmentPayload(ctx, purchase, req, syncReceiving) @@ -611,6 +608,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati receivedQty float64 } + visitedItems := make(map[uint64]struct{}, len(req.Items)) prepared := make([]preparedReceiving, 0, len(req.Items)) for _, payload := range req.Items { item, exists := itemMap[payload.PurchaseItemID] @@ -647,6 +645,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty)) } + if _, dup := visitedItems[payload.PurchaseItemID]; dup { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate receiving data for item %d", payload.PurchaseItemID)) + } + visitedItems[payload.PurchaseItemID] = struct{}{} + prepared = append(prepared, preparedReceiving{ item: item, payload: payload, @@ -657,6 +660,12 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati }) } + // Require receiving payload to cover all purchase items so that + // receiving cannot be submitted partially item-by-item. + if len(visitedItems) != len(itemMap) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving data must be provided for all purchase items") + } + receivingAction := entity.ApprovalActionApproved completedAction := entity.ApprovalActionApproved actorID := uint(1) @@ -1085,57 +1094,21 @@ func (s *purchaseService) buildStaffAdjustmentPayload( return nil, fiber.NewError(fiber.StatusBadRequest, "No available warehouses for this purchase") } - productSupplierCache := make(map[uint64]bool) - for _, item := range purchase.Items { data, ok := requestItems[item.Id] if !ok { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id)) } + if data.ProductID != 0 && data.ProductID != item.ProductId { + return nil, fiber.NewError( + fiber.StatusBadRequest, + fmt.Sprintf("Cannot change product for item %d. Delete the item and create a new one instead", item.Id), + ) + } if data.WarehouseID != 0 && data.WarehouseID != item.WarehouseId { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse mismatch for item %d", item.Id)) } - effectiveProductID := item.ProductId - if data.ProductID != 0 && data.ProductID != item.ProductId { - if item.TotalQty > 0 || item.TotalUsed > 0 { - return nil, fiber.NewError( - fiber.StatusBadRequest, - fmt.Sprintf("Cannot change product for item %d because it already has receiving data", item.Id), - ) - } - - if _, checked := productSupplierCache[data.ProductID]; !checked { - linked, err := s.ProductRepo.IsLinkedToSupplier(ctx, uint(data.ProductID), uint(purchase.SupplierId)) - if err != nil { - s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", data.ProductID, purchase.SupplierId, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier") - } - if !linked { - return nil, fiber.NewError( - fiber.StatusBadRequest, - fmt.Sprintf("Product %d is not linked to supplier %d", data.ProductID, purchase.SupplierId), - ) - } - productSupplierCache[data.ProductID] = true - } - - oldKey := fmt.Sprintf("%d:%d", item.ProductId, item.WarehouseId) - newKey := fmt.Sprintf("%d:%d", data.ProductID, item.WarehouseId) - if _, exists := existingCombos[newKey]; exists && newKey != oldKey { - return nil, fiber.NewError( - fiber.StatusBadRequest, - fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", data.ProductID, item.WarehouseId), - ) - } - if newKey != oldKey { - delete(existingCombos, oldKey) - existingCombos[newKey] = struct{}{} - } - - effectiveProductID = data.ProductID - } - effectiveQty := item.SubQty if data.Qty != nil { if *data.Qty <= 0 { @@ -1161,10 +1134,6 @@ func (s *purchaseService) buildStaffAdjustmentPayload( Price: data.Price, TotalPrice: totalPrice, } - if effectiveProductID != item.ProductId { - productIDCopy := effectiveProductID - update.ProductID = &productIDCopy - } if data.Qty != nil { qtyCopy := effectiveQty update.Quantity = &qtyCopy @@ -1182,6 +1151,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload( return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase") } + productSupplierCache := make(map[uint64]bool) newItems := make([]*entity.PurchaseItem, 0, len(newPayloads)) for _, payload := range newPayloads { diff --git a/internal/modules/purchases/validations/purchase.validation.go b/internal/modules/purchases/validations/purchase.validation.go index 3660263d..4994a927 100644 --- a/internal/modules/purchases/validations/purchase.validation.go +++ b/internal/modules/purchases/validations/purchase.validation.go @@ -3,7 +3,7 @@ package validation type PurchaseItemPayload struct { WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"` ProductID uint `json:"product_id" validate:"required,gt=0"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + Quantity float64 `json:"qty" validate:"required,gt=0"` } type CreatePurchaseRequest struct { @@ -14,12 +14,13 @@ type CreatePurchaseRequest struct { } type StaffPurchaseApprovalItem struct { - PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"` - ProductID uint64 `json:"product_id" validate:"required,gt=0"` - WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` - Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` - Price float64 `json:"price" validate:"required,gt=0"` - TotalPrice float64 `json:"total_price" validate:"required,gt=0"` + PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"` + // For new items (no purchase_item_id), product_id is required. + ProductID uint64 `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` + WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` + Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` + Price float64 `json:"price" validate:"required,gt=0"` + TotalPrice float64 `json:"total_price" validate:"required,gt=0"` } type ApproveStaffPurchaseRequest struct { From 09d503f5bee656288dd4ab5e31949296da8c8d67 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 14:46:21 +0700 Subject: [PATCH 42/59] Feat[BE-261] : inisiate expense module --- internal/entities/delivery-orders.go | 23 --- .../entities/{sales-orders.go => expense.go} | 5 +- .../controllers/expense.controller.go | 144 ++++++++++++++++++ internal/modules/expenses/dto/expense.dto.go | 66 ++++++++ internal/modules/expenses/module.go | 26 ++++ .../repositories/expense.repository.go | 21 +++ internal/modules/expenses/route.go | 28 ++++ .../expenses/services/expense.service.go | 129 ++++++++++++++++ .../validations/expense.validation.go | 15 ++ .../dto/product_warehouse.dto.go | 2 +- .../sales-orders/dto/sales-orders.dto.go | 14 +- .../modules/marketing/sales-orders/module.go | 5 +- .../repositories/sales-orders.repository.go | 21 --- .../services/sales-orders.service.go | 14 +- internal/route/route.go | 2 + 15 files changed, 451 insertions(+), 64 deletions(-) delete mode 100644 internal/entities/delivery-orders.go rename internal/entities/{sales-orders.go => expense.go} (70%) create mode 100644 internal/modules/expenses/controllers/expense.controller.go create mode 100644 internal/modules/expenses/dto/expense.dto.go create mode 100644 internal/modules/expenses/module.go create mode 100644 internal/modules/expenses/repositories/expense.repository.go create mode 100644 internal/modules/expenses/route.go create mode 100644 internal/modules/expenses/services/expense.service.go create mode 100644 internal/modules/expenses/validations/expense.validation.go delete mode 100644 internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go diff --git a/internal/entities/delivery-orders.go b/internal/entities/delivery-orders.go deleted file mode 100644 index 291ba20c..00000000 --- a/internal/entities/delivery-orders.go +++ /dev/null @@ -1,23 +0,0 @@ -package entities - -import ( - "time" - - "gorm.io/gorm" -) - -type DeliveryOrders struct { - Id uint `gorm:"primaryKey" json:"id"` - DeliveryNumber string `gorm:"type:varchar(255);not null;uniqueIndex" json:"delivery_number"` - DeliveryDate time.Time `gorm:"not null" json:"delivery_date"` - MarketingId uint `gorm:"not null" json:"marketing_id"` - Notes string `gorm:"type:text" json:"notes"` - CreatedBy uint `gorm:"not null" json:"created_by"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - Marketing *Marketing `gorm:"foreignKey:MarketingId;references:Id" json:"marketing,omitempty"` - DeliveryProducts []MarketingDeliveryProduct `gorm:"-" json:"delivery_products,omitempty"` - CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id" json:"created_user,omitempty"` -} diff --git a/internal/entities/sales-orders.go b/internal/entities/expense.go similarity index 70% rename from internal/entities/sales-orders.go rename to internal/entities/expense.go index faa6d901..a427582d 100644 --- a/internal/entities/sales-orders.go +++ b/internal/entities/expense.go @@ -6,7 +6,7 @@ import ( "gorm.io/gorm" ) -type SalesOrders struct { +type Expense struct { Id uint `gorm:"primaryKey"` Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` CreatedBy uint `gorm:"not null"` @@ -14,6 +14,5 @@ type SalesOrders struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go new file mode 100644 index 00000000..074f2f0a --- /dev/null +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type ExpenseController struct { + ExpenseService service.ExpenseService +} + +func NewExpenseController(expenseService service.ExpenseService) *ExpenseController { + return &ExpenseController{ + ExpenseService: expenseService, + } +} + +func (u *ExpenseController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.ExpenseService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ExpenseListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all expenses successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToExpenseListDTOs(result), + }) +} + +func (u *ExpenseController) 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.ExpenseService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get expense successfully", + Data: dto.ToExpenseListDTO(*result), + }) +} + +func (u *ExpenseController) 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.ExpenseService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create expense successfully", + Data: dto.ToExpenseListDTO(*result), + }) +} + +func (u *ExpenseController) 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.ExpenseService.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 expense successfully", + Data: dto.ToExpenseListDTO(*result), + }) +} + +func (u *ExpenseController) 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.ExpenseService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete expense successfully", + }) +} diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go new file mode 100644 index 00000000..b7bd1b5f --- /dev/null +++ b/internal/modules/expenses/dto/expense.dto.go @@ -0,0 +1,66 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type ExpenseBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type ExpenseListDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ExpenseDetailDTO struct { + ExpenseListDTO +} + +// === Mapper Functions === + +func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO { + return ExpenseBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToExpenseListDTO(e entity.Expense) ExpenseListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + return ExpenseListDTO{ + Id: e.Id, + Name: e.Name, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToExpenseListDTOs(e []entity.Expense) []ExpenseListDTO { + result := make([]ExpenseListDTO, len(e)) + for i, r := range e { + result[i] = ToExpenseListDTO(r) + } + return result +} + +func ToExpenseDetailDTO(e entity.Expense) ExpenseDetailDTO { + return ExpenseDetailDTO{ + ExpenseListDTO: ToExpenseListDTO(e), + } +} diff --git a/internal/modules/expenses/module.go b/internal/modules/expenses/module.go new file mode 100644 index 00000000..c9b2ab66 --- /dev/null +++ b/internal/modules/expenses/module.go @@ -0,0 +1,26 @@ +package expenses + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" + sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type ExpenseModule struct{} + +func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + expenseRepo := rExpense.NewExpenseRepository(db) + userRepo := rUser.NewUserRepository(db) + + expenseService := sExpense.NewExpenseService(expenseRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + ExpenseRoutes(router, userService, expenseService) +} + diff --git a/internal/modules/expenses/repositories/expense.repository.go b/internal/modules/expenses/repositories/expense.repository.go new file mode 100644 index 00000000..94712cd5 --- /dev/null +++ b/internal/modules/expenses/repositories/expense.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type ExpenseRepository interface { + repository.BaseRepository[entity.Expense] +} + +type ExpenseRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.Expense] +} + +func NewExpenseRepository(db *gorm.DB) ExpenseRepository { + return &ExpenseRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db), + } +} diff --git a/internal/modules/expenses/route.go b/internal/modules/expenses/route.go new file mode 100644 index 00000000..49a4e7c5 --- /dev/null +++ b/internal/modules/expenses/route.go @@ -0,0 +1,28 @@ +package expenses + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/controllers" + expense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService) { + ctrl := controller.NewExpenseController(s) + + route := v1.Group("/expenses") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go new file mode 100644 index 00000000..1b57263e --- /dev/null +++ b/internal/modules/expenses/services/expense.service.go @@ -0,0 +1,129 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type ExpenseService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.Expense, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Expense, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type expenseService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ExpenseRepository +} + +func NewExpenseService(repo repository.ExpenseRepository, validate *validator.Validate) ExpenseService { + return &expenseService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s expenseService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + +func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.Search != "" { + return db.Where("name LIKE ?", "%"+params.Search+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get expenses: %+v", err) + return nil, 0, err + } + return expenses, total, nil +} + +func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*entity.Expense, error) { + expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + if err != nil { + s.Log.Errorf("Failed get expense by id: %+v", err) + return nil, err + } + return expense, nil +} + +func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Expense, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + createBody := &entity.Expense{ + Name: req.Name, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create expense: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.Name != nil { + updateBody["name"] = *req.Name + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + s.Log.Errorf("Failed to update expense: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + s.Log.Errorf("Failed to delete expense: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go new file mode 100644 index 00000000..7d16d3ee --- /dev/null +++ b/internal/modules/expenses/validations/expense.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + 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/inventory/product-warehouses/dto/product_warehouse.dto.go b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go index c6ac5931..f88a6ca3 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -129,7 +129,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT } } - if &e.Warehouse.Area != nil && e.Warehouse.Area.Id != 0 { + if e.Warehouse.Area.Id != 0 { warehouse.Area = &AreaBaseDTO{ Id: e.Warehouse.Area.Id, Name: e.Warehouse.Area.Name, diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 03a0d59a..86bd5f84 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -48,14 +48,18 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { } } -func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { +func ToSalesOrdersListDTO(e entity.Marketing) SalesOrdersListDTO { + products := make([]MarketingProductDTO, len(e.Products)) + for i, p := range e.Products { + products[i] = ToMarketingProductDTO(p) + } return SalesOrdersListDTO{ Id: e.Id, - SoNumber: e.Name, - SoDate: time.Time{}, - Notes: "", - SalesOrder: []MarketingProductDTO{}, + SoNumber: e.SoNumber, + SoDate: e.SoDate, + Notes: e.Notes, + SalesOrder: products, } } diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 6d1963af..0d9583d0 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -21,11 +21,10 @@ import ( type SalesOrdersModule struct{} func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { - salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db) + marketingRepo := rSalesOrders.NewMarketingRepository(db) userRepo := rUser.NewUserRepository(db) customerRepo := rCustomer.NewCustomerRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) - marketingRepo := rSalesOrders.NewMarketingRepository(db) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) @@ -33,7 +32,7 @@ func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valida panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err)) } - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) + salesOrdersService := sSalesOrders.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) diff --git a/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go b/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go deleted file mode 100644 index 5f8cfe79..00000000 --- a/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go +++ /dev/null @@ -1,21 +0,0 @@ -package repository - -import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/common/repository" - "gorm.io/gorm" -) - -type SalesOrdersRepository interface { - repository.BaseRepository[entity.SalesOrders] -} - -type SalesOrdersRepositoryImpl struct { - *repository.BaseRepositoryImpl[entity.SalesOrders] -} - -func NewSalesOrdersRepository(db *gorm.DB) SalesOrdersRepository { - return &SalesOrdersRepositoryImpl{ - BaseRepositoryImpl: repository.NewBaseRepository[entity.SalesOrders](db), - } -} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 0dc47a2c..62f694f3 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -34,22 +34,20 @@ type SalesOrdersService interface { type salesOrdersService struct { Log *logrus.Logger Validate *validator.Validate - Repository repository.SalesOrdersRepository + MarketingRepo repository.MarketingRepository CustomerRepo customerRepo.CustomerRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository - MarketingRepo repository.MarketingRepository UserRepo userRepo.UserRepository ApprovalSvc commonSvc.ApprovalService } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, Validate: validate, - Repository: repo, + MarketingRepo: marketingRepo, CustomerRepo: customerRepo, ProductWarehouseRepo: productWarehouseRepo, - MarketingRepo: marketingRepo, UserRepo: userRepo, ApprovalSvc: approvalSvc, } @@ -118,7 +116,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e soNumber := fmt.Sprintf("SO-%05d", nextSeq) var marketing *entity.Marketing - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) @@ -208,7 +206,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) @@ -366,7 +364,7 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) diff --git a/internal/route/route.go b/internal/route/route.go index 04adf6e2..7b7059b5 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -17,6 +17,7 @@ import ( purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases" ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" + expenses "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses" // MODULE IMPORTS ) @@ -36,6 +37,7 @@ func Routes(app *fiber.App, db *gorm.DB) { purchases.PurchaseModule{}, marketing.MarketingModule{}, ssoModule.Module{}, + expenses.ExpenseModule{}, // MODULE REGISTRY } From 5c25c84f7fdfe7ae9de57b5d1a76d2dcf087c91e Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 15:15:52 +0700 Subject: [PATCH 43/59] Fix[BE]: fixing delivery order delet unused repository --- .../delivery-orders.repository.go | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go diff --git a/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go b/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go deleted file mode 100644 index 0a7d7f3d..00000000 --- a/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go +++ /dev/null @@ -1,21 +0,0 @@ -package repository - -import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/common/repository" - "gorm.io/gorm" -) - -type DeliveryOrdersRepository interface { - repository.BaseRepository[entity.DeliveryOrders] -} - -type DeliveryOrdersRepositoryImpl struct { - *repository.BaseRepositoryImpl[entity.DeliveryOrders] -} - -func NewDeliveryOrdersRepository(db *gorm.DB) DeliveryOrdersRepository { - return &DeliveryOrdersRepositoryImpl{ - BaseRepositoryImpl: repository.NewBaseRepository[entity.DeliveryOrders](db), - } -} From 02cc082d672907228be81bd79cc521aaa92bfed0 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 17 Nov 2025 15:17:25 +0700 Subject: [PATCH 44/59] feat(BE-229,234,235,230,231,232,233): purchase request and purchase order and fix master data dto --- internal/modules/purchases/module.go | 7 ++++++- internal/modules/purchases/route.go | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/modules/purchases/module.go b/internal/modules/purchases/module.go index de79a0c9..1911e364 100644 --- a/internal/modules/purchases/module.go +++ b/internal/modules/purchases/module.go @@ -14,6 +14,8 @@ import ( rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" utils "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" ) @@ -46,5 +48,8 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate expenseBridge, ) - Routes(router, purchaseService) + userRepo := rUser.NewUserRepository(db) + userService := sUser.NewUserService(userRepo, validate) + + Routes(router, purchaseService, userService) } diff --git a/internal/modules/purchases/route.go b/internal/modules/purchases/route.go index 41706b01..aedc3ee8 100644 --- a/internal/modules/purchases/route.go +++ b/internal/modules/purchases/route.go @@ -2,14 +2,18 @@ package purchases import ( "github.com/gofiber/fiber/v2" + + middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers" service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) -func Routes(router fiber.Router, purchaseService service.PurchaseService) { +func Routes(router fiber.Router, purchaseService service.PurchaseService, userService user.UserService) { ctrl := controller.NewPurchaseController(purchaseService) route := router.Group("/purchases") + route.Use(middleware.Auth(userService)) route.Get("/", ctrl.GetAll) route.Get("/:id", ctrl.GetOne) From 1dac74e25bb4dcaed7ab361ad85bed12f3a801c6 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 18 Nov 2025 08:18:37 +0700 Subject: [PATCH 45/59] Feat[BE-261]: creating Entity and repository for each table expenses --- internal/entities/expense.go | 26 +++++++--- internal/entities/expense_nonstock.go | 27 ++++++++++ internal/entities/expense_realization.go | 25 +++++++++ internal/modules/expenses/dto/expense.dto.go | 52 ++++++++++--------- .../repositories/expense.repository.go | 9 +++- .../expense_nonstock.repository.go | 28 ++++++++++ .../expense_realization.repository.go | 28 ++++++++++ .../expenses/services/expense.service.go | 14 +++-- .../validations/expense.validation.go | 6 ++- internal/utils/constant.go | 17 ++++++ 10 files changed, 193 insertions(+), 39 deletions(-) create mode 100644 internal/entities/expense_nonstock.go create mode 100644 internal/entities/expense_realization.go create mode 100644 internal/modules/expenses/repositories/expense_nonstock.repository.go create mode 100644 internal/modules/expenses/repositories/expense_realization.repository.go diff --git a/internal/entities/expense.go b/internal/entities/expense.go index a427582d..286eaf51 100644 --- a/internal/entities/expense.go +++ b/internal/entities/expense.go @@ -1,18 +1,30 @@ package entities import ( + "database/sql" "time" "gorm.io/gorm" ) type Expense struct { - Id uint `gorm:"primaryKey"` - Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` - CreatedBy uint `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint64 `gorm:"primaryKey;autoIncrement"` + ReferenceNumber *string `gorm:"type:varchar(50)"` + SupplierId *uint64 `gorm:""` + Category string `gorm:"type:varchar(50);not null"` + PoNumber string `gorm:"uniqueIndex;not null;type:varchar(50)"` + DocumentPath sql.NullString `gorm:"type:json"` + ExpenseDate time.Time `gorm:"type:date;not null"` + GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` + Note *string `gorm:"type:text"` + CreatedBy *uint64 `gorm:""` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + // Relations + Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` + Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"` + LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` } diff --git a/internal/entities/expense_nonstock.go b/internal/entities/expense_nonstock.go new file mode 100644 index 00000000..eb27efef --- /dev/null +++ b/internal/entities/expense_nonstock.go @@ -0,0 +1,27 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type ExpenseNonstock struct { + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseId *uint64 `gorm:""` + ProjectFlockKandangId *uint64 `gorm:""` + NonstockId *uint64 `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + Note *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // Relations + Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` + ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"` + Realizations []ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` +} diff --git a/internal/entities/expense_realization.go b/internal/entities/expense_realization.go new file mode 100644 index 00000000..45a87602 --- /dev/null +++ b/internal/entities/expense_realization.go @@ -0,0 +1,25 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type ExpenseRealization struct { + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseNonstockId *uint64 `gorm:""` + RealizationQty float64 `gorm:"type:numeric(15,3);not null"` + RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"` + RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"` + RealizationDate time.Time `gorm:"type:date;not null"` + Note *string `gorm:"type:text"` + CreatedBy *uint64 `gorm:""` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // Relations + ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index b7bd1b5f..b5fca80f 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -10,44 +10,52 @@ import ( // === DTO Structs === type ExpenseBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint64 `json:"id"` + PoNumber string `json:"po_number"` + ExpenseDate time.Time `json:"expense_date"` + GrandTotal float64 `json:"grand_total"` } type ExpenseListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type ExpenseDetailDTO struct { - ExpenseListDTO + Id uint64 `json:"id"` + ReferenceNumber string `json:"reference_number"` + PoNumber string `json:"po_number"` + Category string `json:"category"` + ExpenseDate time.Time `json:"expense_date"` + GrandTotal float64 `json:"grand_total"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // === Mapper Functions === func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO { return ExpenseBaseDTO{ - Id: e.Id, - Name: e.Name, + Id: e.Id, + PoNumber: e.PoNumber, + ExpenseDate: e.ExpenseDate, + GrandTotal: e.GrandTotal, } } func ToExpenseListDTO(e entity.Expense) ExpenseListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } return ExpenseListDTO{ - Id: e.Id, - Name: e.Name, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + Id: e.Id, + ReferenceNumber: *e.ReferenceNumber, + PoNumber: e.PoNumber, + Category: e.Category, + ExpenseDate: e.ExpenseDate, + GrandTotal: e.GrandTotal, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } @@ -58,9 +66,3 @@ func ToExpenseListDTOs(e []entity.Expense) []ExpenseListDTO { } return result } - -func ToExpenseDetailDTO(e entity.Expense) ExpenseDetailDTO { - return ExpenseDetailDTO{ - ExpenseListDTO: ToExpenseListDTO(e), - } -} diff --git a/internal/modules/expenses/repositories/expense.repository.go b/internal/modules/expenses/repositories/expense.repository.go index 94712cd5..8cc580be 100644 --- a/internal/modules/expenses/repositories/expense.repository.go +++ b/internal/modules/expenses/repositories/expense.repository.go @@ -1,13 +1,16 @@ package repository import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" ) type ExpenseRepository interface { repository.BaseRepository[entity.Expense] + IdExists(ctx context.Context, id uint64) (bool, error) } type ExpenseRepositoryImpl struct { @@ -19,3 +22,7 @@ func NewExpenseRepository(db *gorm.DB) ExpenseRepository { BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db), } } + +func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { + return repository.Exists[entity.Expense](ctx, r.DB(), uint(id)) +} diff --git a/internal/modules/expenses/repositories/expense_nonstock.repository.go b/internal/modules/expenses/repositories/expense_nonstock.repository.go new file mode 100644 index 00000000..257a3034 --- /dev/null +++ b/internal/modules/expenses/repositories/expense_nonstock.repository.go @@ -0,0 +1,28 @@ +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 ExpenseNonstockRepository interface { + repository.BaseRepository[entity.ExpenseNonstock] + IdExists(ctx context.Context, id uint64) (bool, error) +} + +type ExpenseNonstockRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ExpenseNonstock] +} + +func NewExpenseNonstockRepository(db *gorm.DB) ExpenseNonstockRepository { + return &ExpenseNonstockRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ExpenseNonstock](db), + } +} + +func (r *ExpenseNonstockRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { + return repository.Exists[entity.ExpenseNonstock](ctx, r.DB(), uint(id)) +} diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go new file mode 100644 index 00000000..0245f8a2 --- /dev/null +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -0,0 +1,28 @@ +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 ExpenseRealizationRepository interface { + repository.BaseRepository[entity.ExpenseRealization] + IdExists(ctx context.Context, id uint64) (bool, error) +} + +type ExpenseRealizationRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ExpenseRealization] +} + +func NewExpenseRealizationRepository(db *gorm.DB) ExpenseRealizationRepository { + return &ExpenseRealizationRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ExpenseRealization](db), + } +} + +func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { + return repository.Exists[entity.ExpenseRealization](ctx, r.DB(), uint(id)) +} diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 1b57263e..6f794e54 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -79,8 +79,11 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } + createdBy := uint64(1) createBody := &entity.Expense{ - Name: req.Name, + PoNumber: req.PoNumber, + Category: req.Category, + CreatedBy: &createdBy, } if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { @@ -88,7 +91,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } - return s.GetOne(c, createBody.Id) + return s.GetOne(c, uint(createBody.Id)) } func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) { @@ -98,8 +101,11 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody := make(map[string]any) - if req.Name != nil { - updateBody["name"] = *req.Name + if req.PoNumber != nil { + updateBody["po_number"] = *req.PoNumber + } + if req.Category != nil { + updateBody["category"] = *req.Category } if len(updateBody) == 0 { diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index 7d16d3ee..8155d76f 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -1,11 +1,13 @@ package validation type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` + PoNumber string `json:"po_number" validate:"required,max=50"` + Category string `json:"category" validate:"required,max=50"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + PoNumber *string `json:"po_number,omitempty" validate:"omitempty,max=50"` + Category *string `json:"category,omitempty" validate:"omitempty,max=50"` } type Query struct { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index fc01a231..7b219a8e 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -241,6 +241,23 @@ var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{ MarketingDeliveryOrder: "Delivery Order", } +// ------------------------------------------------------------------- +// Expense Approval +// ------------------------------------------------------------------- + +const ( + ApprovalWorkflowExpense approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("EXPENSES") + ExpenseStepPengajuan approvalutils.ApprovalStep = 1 + ExpenseStepManager approvalutils.ApprovalStep = 2 + ExpenseStepFinance approvalutils.ApprovalStep = 3 +) + +var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{ + ExpenseStepPengajuan: "Pengajuan", + ExpenseStepManager: "Manager", + ExpenseStepFinance: "Finance", +} + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- From 320f5e65c6bf63eb74f4a081e1e2189395642db3 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 18 Nov 2025 11:32:41 +0700 Subject: [PATCH 46/59] Fix[BE]: fix wrong approval step when SO uppdated --- .../sales-orders/services/sales-orders.service.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 62f694f3..df25a70e 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -291,32 +291,30 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } } else { - // Create new marketing product (use helper) if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } } - // 2) Delete missing old products (prevent deletion if deliveries exist) for _, old := range oldProducts { if _, ok := reqByPW[old.ProductWarehouseId]; !ok { - // Check delivery product for this marketing product + deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") } if err == nil { - // If delivery exists (delivery_date not nil or qty > 0), prevent deletion + if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) } - // safe to delete delivery product record + if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product") } } - // Delete marketing product + if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product") } @@ -327,7 +325,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if latestApproval != nil && latestApproval.StepNumber == 2 { actorID := uint(1) // todo: ambil dari auth context resetNote := "" - action := entity.ApprovalActionApproved + action := entity.ApprovalActionUpdated _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, From c2b60c1aff16fe8fb4b59d58e063735efad6fd14 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 18 Nov 2025 12:26:54 +0700 Subject: [PATCH 47/59] feat(BE-#270): Project flock period change to project_flock_kandangs --- ...14_adjustment_projectflock_period.down.sql | 16 ++ ...2514_adjustment_projectflock_period.up.sql | 29 +++ internal/entities/location.go | 5 +- internal/entities/projectflock.go | 2 - internal/entities/projectflock_kandang.go | 1 + .../repositories/kandang.repository.go | 33 ++++ .../production/chickins/dto/chickin.dto.go | 7 +- .../controllers/projectflock.controller.go | 61 +++++- .../project_flocks/dto/projectflock.dto.go | 36 +++- .../dto/projectflock_kandang.dto.go | 2 +- .../repositories/projectflock.repository.go | 84 ++------- .../projectflock_kandang.repository.go | 29 ++- .../production/project_flocks/route.go | 6 +- .../services/projectflock.service.go | 173 ++++++++++++------ .../dto/transfer_laying.dto.go | 2 - 15 files changed, 330 insertions(+), 156 deletions(-) create mode 100644 internal/database/migrations/20251117092514_adjustment_projectflock_period.down.sql create mode 100644 internal/database/migrations/20251117092514_adjustment_projectflock_period.up.sql diff --git a/internal/database/migrations/20251117092514_adjustment_projectflock_period.down.sql b/internal/database/migrations/20251117092514_adjustment_projectflock_period.down.sql new file mode 100644 index 00000000..64183dd7 --- /dev/null +++ b/internal/database/migrations/20251117092514_adjustment_projectflock_period.down.sql @@ -0,0 +1,16 @@ +BEGIN; + +ALTER TABLE project_flock_kandangs + DROP COLUMN IF EXISTS period; + +ALTER TABLE project_flocks + ADD COLUMN IF NOT EXISTS period INT NOT NULL DEFAULT 0; + +CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique + ON project_flocks ( + LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))), + period + ) + WHERE deleted_at IS NULL; + +COMMIT; diff --git a/internal/database/migrations/20251117092514_adjustment_projectflock_period.up.sql b/internal/database/migrations/20251117092514_adjustment_projectflock_period.up.sql new file mode 100644 index 00000000..a518c2d6 --- /dev/null +++ b/internal/database/migrations/20251117092514_adjustment_projectflock_period.up.sql @@ -0,0 +1,29 @@ +BEGIN; + +ALTER TABLE project_flock_kandangs + ADD COLUMN IF NOT EXISTS period INT; + +UPDATE project_flock_kandangs pfk +SET period = pf.period +FROM project_flocks pf +WHERE pfk.project_flock_id = pf.id + AND (pfk.period IS NULL OR pfk.period = 0) + AND pf.period IS NOT NULL; + +ALTER TABLE project_flock_kandangs + ALTER COLUMN period SET DEFAULT 0; + +UPDATE project_flock_kandangs +SET period = 0 +WHERE period IS NULL; + +ALTER TABLE project_flock_kandangs + ALTER COLUMN period SET NOT NULL; + +-- Drop period from project_flocks as the source of truth +DROP INDEX IF EXISTS project_flocks_base_period_unique; + +ALTER TABLE project_flocks + DROP COLUMN IF EXISTS period; + +COMMIT; diff --git a/internal/entities/location.go b/internal/entities/location.go index 9f87e97b..1dba8f82 100644 --- a/internal/entities/location.go +++ b/internal/entities/location.go @@ -16,6 +16,7 @@ type Location struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Area Area `gorm:"foreignKey:AreaId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Area Area `gorm:"foreignKey:AreaId;references:Id"` + Kandangs []Kandang `gorm:"foreignKey:LocationId;references:Id"` } diff --git a/internal/entities/projectflock.go b/internal/entities/projectflock.go index 64f47aaf..e8745455 100644 --- a/internal/entities/projectflock.go +++ b/internal/entities/projectflock.go @@ -13,7 +13,6 @@ type ProjectFlock struct { Category string `gorm:"type:varchar(20);not null"` FcrId uint `gorm:"not null"` LocationId uint `gorm:"not null"` - Period int `gorm:"not null"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` @@ -28,4 +27,3 @@ type ProjectFlock struct { LatestApproval *Approval `gorm:"-" json:"-"` } - diff --git a/internal/entities/projectflock_kandang.go b/internal/entities/projectflock_kandang.go index c02eafe1..d4bd7452 100644 --- a/internal/entities/projectflock_kandang.go +++ b/internal/entities/projectflock_kandang.go @@ -6,6 +6,7 @@ type ProjectFlockKandang struct { Id uint `gorm:"primaryKey"` ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"` KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"` + Period int `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` diff --git a/internal/modules/master/kandangs/repositories/kandang.repository.go b/internal/modules/master/kandangs/repositories/kandang.repository.go index 8f32a7b2..a08db163 100644 --- a/internal/modules/master/kandangs/repositories/kandang.repository.go +++ b/internal/modules/master/kandangs/repositories/kandang.repository.go @@ -7,6 +7,7 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" + "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) @@ -115,9 +116,41 @@ func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, p Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID). First(&link).Error if errors.Is(err, gorm.ErrRecordNotFound) { + var project entity.ProjectFlock + if err := r.db.WithContext(ctx). + Select("id, location_id"). + First(&project, projectFlockID).Error; err != nil { + return err + } + + var kandang entity.Kandang + if err := r.db.WithContext(ctx). + Select("id, location_id"). + First(&kandang, kandangID).Error; err != nil { + return err + } + if kandang.LocationId != project.LocationId { + return fiber.NewError(fiber.StatusBadRequest, "Kandang tidak berada pada lokasi yang sama dengan project flock") + } + + // Determine project's period from existing pivot rows so the new kandang + // shares the same period. + var period int + if err := r.db.WithContext(ctx). + Table("project_flock_kandangs"). + Where("project_flock_id = ?", projectFlockID). + Select("COALESCE(MAX(period), 0)"). + Scan(&period).Error; err != nil { + return err + } + if period <= 0 { + period = 1 + } + link = entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: kandangID, + Period: period, } return r.db.WithContext(ctx).Create(&link).Error } diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 7271c310..b581da3c 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -100,7 +100,8 @@ func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO { return userBaseDTO.ToUserBaseDTO(e) } -func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { +func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO { + e := pfk.ProjectFlock var flock *flockBaseDTO.FlockBaseDTO if base := pfutils.DeriveBaseName(e.FlockName); base != "" { summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base} @@ -123,7 +124,7 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { } return ProjectFlockDTO{ Id: e.Id, - Period: e.Period, + Period: pfk.Period, Category: e.Category, Flock: flock, Area: area, @@ -135,7 +136,7 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { var pf *ProjectFlockDTO if e.ProjectFlock.Id != 0 { - mapped := ToProjectFlockDTO(e.ProjectFlock) + mapped := ToProjectFlockDTO(e) pf = &mapped } var kandang *kandangBaseDTO.KandangBaseDTO diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index d3b0061c..9957e4d9 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -85,6 +85,17 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { return err } + var periodMap map[uint]int + if len(result) > 0 { + ids := make([]uint, len(result)) + for i, item := range result { + ids[i] = item.Id + } + if periods, err := u.ProjectflockService.GetProjectPeriods(c, ids); err == nil { + periodMap = periods + } + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.ProjectFlockListDTO]{ Code: fiber.StatusOK, @@ -96,7 +107,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToProjectFlockListDTOs(result), + Data: dto.ToProjectFlockListDTOsWithPeriods(result, periodMap), }) } @@ -113,12 +124,19 @@ func (u *ProjectflockController) GetOne(c *fiber.Ctx) error { return err } + var period int + if periods, err := u.ProjectflockService.GetProjectPeriods(c, []uint{uint(id)}); err == nil { + if p, ok := periods[uint(id)]; ok { + period = p + } + } + return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Get projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result), + Data: dto.ToProjectFlockListDTOWithPeriod(*result, period), }) } @@ -205,11 +223,29 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error { data interface{} message = "Submit projectflock approval successfully" ) + + var periodMap map[uint]int + if len(results) > 0 { + ids := make([]uint, len(results)) + for i, item := range results { + ids[i] = item.Id + } + if periods, err := u.ProjectflockService.GetProjectPeriods(c, ids); err == nil { + periodMap = periods + } + } + if len(results) == 1 { - data = dto.ToProjectFlockListDTO(results[0]) + period := 0 + if periodMap != nil { + if p, ok := periodMap[results[0].Id]; ok { + period = p + } + } + data = dto.ToProjectFlockListDTOWithPeriod(results[0], period) } else { message = "Submit projectflock approvals successfully" - data = dto.ToProjectFlockListDTOs(results) + data = dto.ToProjectFlockListDTOsWithPeriods(results, periodMap) } return c.Status(fiber.StatusOK). @@ -222,25 +258,32 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error { } func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { - param := c.Params("project_flock_kandang_id") + param := c.Params("location_id") id, err := strconv.Atoi(param) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") + return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id") } - summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id)) + summaries, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id)) if err != nil { return err } - responseBody := dto.ToFlockPeriodSummaryDTO(summary.Flock, summary.NextPeriod) + responseBody := make([]dto.KandangPeriodSummaryDTO, 0, len(summaries)) + for _, item := range summaries { + responseBody = append(responseBody, dto.KandangPeriodSummaryDTO{ + Id: item.Id, + Name: item.Name, + Period: item.Period, + }) + } return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", - Message: "Get flock period summary successfully", + Message: "Get kandang period summary successfully", Data: responseBody, }) } diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index 977aeb40..dac023df 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -53,7 +53,13 @@ type FlockPeriodDTO struct { NextPeriod int `json:"next_period"` } -func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { +type KandangPeriodSummaryDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Period int `json:"period"` +} + +func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectFlockListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(e.CreatedUser) @@ -99,7 +105,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { } return ProjectFlockListDTO{ - ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), + ProjectFlockBaseDTO: createProjectFlockBaseDTO(e, period), // Flock: flockSummary, Area: areaSummary, Kandangs: kandangSummaries, @@ -116,14 +122,32 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { func ToProjectFlockListDTOs(items []entity.ProjectFlock) []ProjectFlockListDTO { result := make([]ProjectFlockListDTO, len(items)) for i, item := range items { - result[i] = ToProjectFlockListDTO(item) + result[i] = ToProjectFlockListDTOWithPeriod(item, 0) + } + return result +} + +func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { + return ToProjectFlockListDTOWithPeriod(e, 0) +} + +func ToProjectFlockListDTOsWithPeriods(items []entity.ProjectFlock, periods map[uint]int) []ProjectFlockListDTO { + result := make([]ProjectFlockListDTO, len(items)) + for i, item := range items { + p := 0 + if periods != nil { + if v, ok := periods[item.Id]; ok { + p = v + } + } + result[i] = ToProjectFlockListDTOWithPeriod(item, p) } return result } func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO { return ProjectFlockDetailDTO{ - ProjectFlockListDTO: ToProjectFlockListDTO(e), + ProjectFlockListDTO: ToProjectFlockListDTOWithPeriod(e, 0), } } @@ -152,10 +176,10 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv return result } -func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { +func createProjectFlockBaseDTO(e entity.ProjectFlock, period int) ProjectFlockBaseDTO { return ProjectFlockBaseDTO{ Id: e.Id, - Period: e.Period, + Period: period, FlockName: e.FlockName, } } diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 24e53d28..2e25bf09 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -50,7 +50,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD pfLocal := ProjectFlockWithPivotDTO{ ProjectFlockBaseDTO: ProjectFlockBaseDTO{ Id: e.ProjectFlock.Id, - Period: e.ProjectFlock.Period, + Period: e.Period, FlockName: e.ProjectFlock.FlockName, }, Category: e.ProjectFlock.Category, diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index bb653fe9..afb05982 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -2,7 +2,6 @@ package repository import ( "context" - "errors" "fmt" "strings" @@ -10,17 +9,12 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" "gorm.io/gorm" - "gorm.io/gorm/clause" ) const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))" type ProjectflockRepository interface { repository.BaseRepository[entity.ProjectFlock] - GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) - GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) - GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) - GetNextSequenceForBase(ctx context.Context, baseName string) (int, error) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) @@ -39,65 +33,6 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository { } } -func (r *ProjectflockRepositoryImpl) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) { - var records []entity.ProjectFlock - if err := r.DB().WithContext(ctx). - Unscoped(). - Where(baseNameExpression+" = LOWER(?)", baseName). - Order("period ASC"). - Find(&records).Error; err != nil { - return nil, err - } - return records, nil -} - -func (r *ProjectflockRepositoryImpl) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) { - var record entity.ProjectFlock - err := r.DB().WithContext(ctx). - Where(baseNameExpression+" = LOWER(?)", baseName). - Order("period DESC"). - First(&record).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - if err != nil { - return nil, err - } - return &record, nil -} - -func (r *ProjectflockRepositoryImpl) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) { - var max int - if err := r.DB().WithContext(ctx). - Model(&entity.ProjectFlock{}). - Where(baseNameExpression+" = LOWER(?)", baseName). - Select("COALESCE(MAX(period), 0)"). - Scan(&max).Error; err != nil { - return 0, err - } - return max, nil -} - -func (r *ProjectflockRepositoryImpl) GetNextSequenceForBase(ctx context.Context, baseName string) (int, error) { - var payload struct { - Period int - } - if err := r.DB().WithContext(ctx). - Model(&entity.ProjectFlock{}). - Where(baseNameExpression+" = LOWER(?)", baseName). - Clauses(clause.Locking{Strength: "UPDATE"}). - Order("period DESC"). - Limit(1). - Select("period"). - Scan(&payload).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return 1, nil - } - return 0, err - } - return payload.Period + 1, nil -} - func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) { return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { db = r.withDefaultRelations(db) @@ -132,7 +67,13 @@ func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *vali db = db.Where("project_flocks.location_id = ?", params.LocationId) } if params.Period > 0 { - db = db.Where("project_flocks.period = ?", params.Period) + db = db.Where(` + EXISTS ( + SELECT 1 + FROM project_flock_kandangs pfk + WHERE pfk.project_flock_id = project_flocks.id + AND pfk.period = ? + )`, params.Period) } if len(params.KandangIds) > 0 { db = db.Where(` @@ -179,10 +120,15 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s OR LOWER(created_users.email) LIKE ? OR LOWER(project_flocks.flock_name) LIKE ? OR LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))) LIKE ? - OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ? + OR EXISTS ( + SELECT 1 FROM project_flock_kandangs + WHERE project_flock_kandangs.project_flock_id = project_flocks.id + AND LOWER(CAST(project_flock_kandangs.period AS TEXT)) LIKE ? + ) OR EXISTS ( SELECT 1 FROM kandangs - WHERE kandangs.project_flock_id = project_flocks.id + JOIN project_flock_kandangs pfk ON pfk.kandang_id = kandangs.id + WHERE pfk.project_flock_id = project_flocks.id AND LOWER(kandangs.name) LIKE ? ) `, @@ -236,7 +182,7 @@ func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder str } case "period": return []string{ - fmt.Sprintf("project_flocks.period %s", direction), + fmt.Sprintf("(SELECT COALESCE(MAX(period), 0) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction), fmt.Sprintf("project_flocks.id %s", direction), } default: 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 101d01ab..6923b1f3 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -19,6 +19,7 @@ type ProjectFlockKandangRepository interface { HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) + ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) WithTx(tx *gorm.DB) ProjectFlockKandangRepository IdExists(ctx context.Context, id uint) (bool, error) DB() *gorm.DB @@ -177,7 +178,33 @@ func (r *projectFlockKandangRepositoryImpl) MaxPeriodByBaseName(ctx context.Cont Table("project_flock_kandangs pfk"). Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). Where(flockBaseNameExpression+" = LOWER(?)", baseName). - Select("COALESCE(MAX(pf.period), 0)"). + Select("COALESCE(MAX(pfk.period), 0)"). Scan(&max).Error return max, err } + +func (r *projectFlockKandangRepositoryImpl) ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) { + result := make(map[uint]int) + if len(projectIDs) == 0 { + return result, nil + } + + type row struct { + ProjectFlockID uint + Period int + } + var rows []row + if err := r.db.WithContext(ctx). + Table("project_flock_kandangs"). + Where("project_flock_id IN ?", projectIDs). + Select("project_flock_id, COALESCE(MAX(period), 0) AS period"). + Group("project_flock_id"). + Scan(&rows).Error; err != nil { + return nil, err + } + + for _, item := range rows { + result[item.ProjectFlockID] = item.Period + } + return result, nil +} diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index 8128f943..0e6f80c4 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -1,7 +1,7 @@ package project_flocks import ( - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers" projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,7 +13,7 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj ctrl := controller.NewProjectflockController(s) route := v1.Group("/project-flocks") - route.Use(m.Auth(u)) + // route.Use(m.Auth(u)) route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) @@ -22,6 +22,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Delete("/:id", ctrl.DeleteOne) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) - route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary) + route.Get("/kandangs/:location_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 a91976d8..f812f412 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -35,7 +35,8 @@ type ProjectflockService interface { GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) - GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) + GetFlockPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) + GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) } @@ -52,9 +53,10 @@ type projectflockService struct { approvalWorkflow approvalutils.ApprovalWorkflowKey } -type FlockPeriodSummary struct { - Flock entity.Flock - NextPeriod int +type KandangPeriodSummary struct { + Id uint + Name string + Period int } func NewProjectflockService( @@ -218,6 +220,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* if len(kandangs) != len(kandangIDs) { return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") } + for _, kandang := range kandangs { + if kandang.LocationId != req.LocationId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id)) + } + } // larang kalau ada yg sudah terikat ke project lain if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage") @@ -236,22 +243,24 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { projectRepo := repository.NewProjectflockRepository(dbTransaction) - nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), canonicalBase) - if err != nil { - return err - } - generatedName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, nextSeq, nil) + // Generate unique flock name (sequential per base name, starting from 1) + generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil) if err != nil { return err } createBody.FlockName = generatedName - createBody.Period = seq if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { return err } - if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs); err != nil { + // Compute period based on location history (max period in that location + 1), + // and store it on project_flock_kandangs only. + nextPeriod, err := s.nextLocationPeriod(c.Context(), dbTransaction, req.LocationId) + if err != nil { + return err + } + if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, nextPeriod); err != nil { return err } @@ -387,6 +396,15 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id if len(kandangs) != len(newKandangIDs) { return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") } + targetLocationID := existing.LocationId + if req.LocationId != nil && *req.LocationId > 0 { + targetLocationID = *req.LocationId + } + for _, kandang := range kandangs { + if kandang.LocationId != targetLocationID { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id)) + } + } if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage") } else if linked { @@ -412,18 +430,11 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if needFlockNameRegenerate { - nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), baseForGeneration) - if err != nil { - return err - } - newName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, nextSeq, &id) + newName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, 1, &id) if err != nil { return err } updateBody["flock_name"] = newName - if seq != existing.Period { - updateBody["period"] = seq - } } if len(updateBody) > 0 { @@ -467,7 +478,19 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if len(toAttach) > 0 { - if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach); err != nil { + var currentPeriod int + if err := dbTransaction.WithContext(c.Context()). + Table("project_flock_kandangs"). + Where("project_flock_id = ?", id). + Select("COALESCE(MAX(period), 0)"). + Scan(¤tPeriod).Error; err != nil { + return err + } + if currentPeriod <= 0 { + currentPeriod = 1 + } + + if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, currentPeriod); err != nil { return err } } @@ -743,57 +766,90 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u return total, nil } -func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, projectFlockKandangID uint) (*FlockPeriodSummary, error) { - if projectFlockKandangID == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") +// nextLocationPeriod computes the next period number for a given location +// based on the maximum period that has ever been used by any kandang in that location. +func (s projectflockService) nextLocationPeriod(ctx context.Context, tx *gorm.DB, locationID uint) (int, error) { + if locationID == 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, "location_id is required to compute period") } - pivot, err := s.pivotRepo().GetByID(c.Context(), projectFlockKandangID) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") + db := s.Repository.DB() + if tx != nil { + db = tx } + + var maxPeriod int + if err := db.WithContext(ctx). + Table("project_flock_kandangs pfk"). + Joins("JOIN kandangs k ON k.id = pfk.kandang_id"). + Where("k.location_id = ?", locationID). + Select("COALESCE(MAX(pfk.period), 0)"). + Scan(&maxPeriod).Error; err != nil { + s.Log.Errorf("Failed to compute max period for location %d: %+v", locationID, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute period for location") + } + + return maxPeriod + 1, nil +} + +func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) { + if len(projectIDs) == 0 { + return map[uint]int{}, nil + } + return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs) +} + +func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) { + if locationID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required") + } + + exists, err := s.Repository.LocationExists(c.Context(), locationID) if err != nil { - s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", projectFlockKandangID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") + s.Log.Errorf("Failed to validate location %d: %+v", locationID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate location") + } + if !exists { + return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } - var baseName string - var referenceFlock *entity.Flock - if pivot.ProjectFlock.Id != 0 { - baseName = pfutils.DeriveBaseName(pivot.ProjectFlock.FlockName) + type kandangPeriodRow struct { + Id uint + Name string + LatestPeriod int } - if strings.TrimSpace(baseName) != "" { - referenceFlock, err = s.FlockRepo.GetByName(c.Context(), baseName) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Failed to fetch flock %q: %+v", baseName, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock") - } + var rows []kandangPeriodRow + + db := s.Repository.DB().WithContext(c.Context()) + if err := db. + Table("kandangs AS k"). + Select("k.id, k.name, COALESCE(MAX(pfk.period), 0) AS latest_period"). + Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.kandang_id = k.id"). + Where("k.location_id = ?", locationID). + Where("k.deleted_at IS NULL"). + Group("k.id, k.name"). + Order("k.id ASC"). + Scan(&rows).Error; err != nil { + s.Log.Errorf("Failed to fetch kandang period summary for location %d: %+v", locationID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang period summary") } - if referenceFlock == nil { - referenceFlock = &entity.Flock{Name: pivot.ProjectFlock.FlockName} - } - - maxPeriod := pivot.ProjectFlock.Period - if strings.TrimSpace(baseName) != "" { - if headerMax, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), baseName); err != nil { - s.Log.Warnf("Unable to compute header period for base %q: %+v", baseName, err) - } else if headerMax > maxPeriod { - maxPeriod = headerMax + summaries := make([]KandangPeriodSummary, 0, len(rows)) + for _, row := range rows { + nextPeriod := 0 + if row.LatestPeriod > 0 { + nextPeriod = row.LatestPeriod + 1 } - if pivotMax, err := s.pivotRepo().MaxPeriodByBaseName(c.Context(), baseName); err != nil { - s.Log.Warnf("Unable to compute pivot period for base %q: %+v", baseName, err) - } else if pivotMax > maxPeriod { - maxPeriod = pivotMax - } + summaries = append(summaries, KandangPeriodSummary{ + Id: row.Id, + Name: row.Name, + Period: nextPeriod, + }) } - return &FlockPeriodSummary{ - Flock: *referenceFlock, - NextPeriod: maxPeriod + 1, - }, nil + return summaries, nil } func uniqueUintSlice(values []uint) []uint { @@ -869,7 +925,7 @@ func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint return newFlock, nil } -func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error { +func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, period int) error { if len(kandangIDs) == 0 { return nil } @@ -907,6 +963,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction * records = append(records, &entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: id, + Period: period, }) } if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil { diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 719e458a..5bc54762 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -19,7 +19,6 @@ type TransferLayingBaseDTO struct { type ProjectFlockSummaryDTO struct { Id uint `json:"id"` - Period int `json:"period"` Category string `json:"category"` } @@ -86,7 +85,6 @@ func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { return &ProjectFlockSummaryDTO{ Id: pf.Id, - Period: pf.Period, Category: pf.Category, } } From a57ef82ebb9982b1ead45c28cf932f5974aef8d4 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 18 Nov 2025 12:44:19 +0700 Subject: [PATCH 48/59] Fix{BE-222] fixing approval status when updated and delete timestamz on children of marketing table --- ...ng_product_and_marketing_delivery.down.sql | 11 ++++++++ ...ting_product_and_marketing_delivery.up.sql | 27 +++++++++++++++++++ .../entities/marketing_delivery_product.go | 23 +++++++--------- internal/entities/marketing_product.go | 25 ++++++----------- .../services/sales-orders.service.go | 17 ++++++------ 5 files changed, 63 insertions(+), 40 deletions(-) create mode 100644 internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.down.sql create mode 100644 internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.up.sql diff --git a/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.down.sql b/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.down.sql new file mode 100644 index 00000000..4da2ec3e --- /dev/null +++ b/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.down.sql @@ -0,0 +1,11 @@ +-- Add back timestamp columns to marketing_products table +ALTER TABLE marketing_products +ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +-- Add back timestamp columns to marketing_delivery_products table +ALTER TABLE marketing_delivery_products +ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; \ No newline at end of file diff --git a/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.up.sql b/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.up.sql new file mode 100644 index 00000000..65750f8d --- /dev/null +++ b/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.up.sql @@ -0,0 +1,27 @@ +-- Drop timestamp columns from marketing_products table if it exists +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'created_at') THEN + ALTER TABLE marketing_products DROP COLUMN created_at; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'updated_at') THEN + ALTER TABLE marketing_products DROP COLUMN updated_at; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'deleted_at') THEN + ALTER TABLE marketing_products DROP COLUMN deleted_at; + END IF; +END $$; + +-- Drop timestamp columns from marketing_delivery_products table if it exists +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'created_at') THEN + ALTER TABLE marketing_delivery_products DROP COLUMN created_at; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'updated_at') THEN + ALTER TABLE marketing_delivery_products DROP COLUMN updated_at; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'deleted_at') THEN + ALTER TABLE marketing_delivery_products DROP COLUMN deleted_at; + END IF; +END $$; \ No newline at end of file diff --git a/internal/entities/marketing_delivery_product.go b/internal/entities/marketing_delivery_product.go index 253c00b2..47f6e89d 100644 --- a/internal/entities/marketing_delivery_product.go +++ b/internal/entities/marketing_delivery_product.go @@ -2,23 +2,18 @@ package entities import ( "time" - - "gorm.io/gorm" ) type MarketingDeliveryProduct struct { - Id uint `gorm:"primaryKey;autoIncrement"` - MarketingProductId uint `gorm:"uniqueIndex;not null"` - Qty float64 `gorm:"type:numeric(15,3)"` - UnitPrice float64 `gorm:"type:numeric(15,3)"` - TotalWeight float64 `gorm:"type:numeric(15,3)"` - AvgWeight float64 `gorm:"type:numeric(15,3)"` - TotalPrice float64 `gorm:"type:numeric(15,3)"` - DeliveryDate *time.Time `gorm:"type:timestamptz"` - VehicleNumber string `gorm:"type:varchar(50)"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingProductId uint `gorm:"uniqueIndex;not null"` + Qty float64 `gorm:"type:numeric(15,3)"` + UnitPrice float64 `gorm:"type:numeric(15,3)"` + TotalWeight float64 `gorm:"type:numeric(15,3)"` + AvgWeight float64 `gorm:"type:numeric(15,3)"` + TotalPrice float64 `gorm:"type:numeric(15,3)"` + DeliveryDate *time.Time `gorm:"type:timestamptz"` + VehicleNumber string `gorm:"type:varchar(50)"` MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"` } diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go index 66524bc6..f2294f10 100644 --- a/internal/entities/marketing_product.go +++ b/internal/entities/marketing_product.go @@ -1,23 +1,14 @@ package entities -import ( - "time" - - "gorm.io/gorm" -) - type MarketingProduct struct { - Id uint `gorm:"primaryKey;autoIncrement"` - MarketingId uint `gorm:"not null"` - ProductWarehouseId uint `gorm:"not null"` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - AvgWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + AvgWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index df25a70e..c1cd8f5f 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -270,7 +270,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") } - // Ensure delivery product exists; if not, create default if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { mdp := &entity.MarketingDeliveryProduct{ @@ -321,21 +320,21 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } } - - if latestApproval != nil && latestApproval.StepNumber == 2 { + if latestApproval != nil { actorID := uint(1) // todo: ambil dari auth context - resetNote := "" action := entity.ApprovalActionUpdated _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, id, - utils.MarketingStepPengajuan, + approvalutils.ApprovalStep(latestApproval.StepNumber), &action, actorID, - &resetNote) + nil) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status") + if !errors.Is(err, gorm.ErrDuplicatedKey) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval") + } } } @@ -371,7 +370,7 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { if len(marketing.Products) > 0 { for _, product := range marketing.Products { if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_product_id = ?", product.Id) + return db.Where("marketing_product_id = ?", product.Id).Unscoped() }); err != nil && err != gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") } @@ -379,7 +378,7 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { } if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_id = ?", id) + return db.Where("marketing_id = ?", id).Unscoped() }); err != nil && err != gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") } From 105b20c333e7fdb7138a95e3108aeafc03fafcd3 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 19 Nov 2025 16:05:11 +0700 Subject: [PATCH 49/59] Feat[BE-261,265]: createing BOP and BOP realization(Unfinished) --- ...0251117034511_create_expenses_table.up.sql | 10 +- ...29_create_expense_nonstocks_table.down.sql | 4 +- ...4529_create_expense_nonstocks_table.up.sql | 15 +- ...8_create_expense_realizations_table.up.sql | 2 +- internal/entities/expense.go | 4 +- internal/entities/expense_nonstock.go | 2 +- .../controllers/expense.controller.go | 164 ++++- internal/modules/expenses/dto/expense.dto.go | 330 ++++++++- internal/modules/expenses/module.go | 25 +- .../repositories/expense.repository.go | 23 + .../expense_nonstock.repository.go | 27 + .../expense_realization.repository.go | 12 + internal/modules/expenses/route.go | 5 + .../expenses/services/expense.service.go | 672 ++++++++++++++++-- .../validations/expense.validation.go | 56 +- .../repositories/nonstock.repository.go | 5 + .../projectflock_kandang.repository.go | 30 + internal/utils/constant.go | 8 +- 18 files changed, 1279 insertions(+), 115 deletions(-) diff --git a/internal/database/migrations/20251117034511_create_expenses_table.up.sql b/internal/database/migrations/20251117034511_create_expenses_table.up.sql index 054084e4..819e25f4 100644 --- a/internal/database/migrations/20251117034511_create_expenses_table.up.sql +++ b/internal/database/migrations/20251117034511_create_expenses_table.up.sql @@ -1,11 +1,11 @@ CREATE TABLE expenses ( id BIGSERIAL PRIMARY KEY, - reference_number VARCHAR, -- format => BOP-LTI-0001 = 0001 is increment + reference_number VARCHAR(50) UNIQUE NOT NULL, supplier_id BIGINT NULL, category VARCHAR(50) NOT NULL CHECK ( category IN ('BOP', 'NON-BOP') ), - po_number VARCHAR(50) UNIQUE NOT NULL, + po_number VARCHAR(50) NULL, document_path JSON, expense_date DATE NOT NULL, grand_total NUMERIC(15, 3) DEFAULT 0, @@ -16,6 +16,8 @@ CREATE TABLE expenses ( deleted_at TIMESTAMPTZ ); +CREATE SEQUENCE expenses_ref_seq INCREMENT BY 1 START WITH 1; + -- Tambahkan Foreign Key ke suppliers DO $$ BEGIN @@ -23,7 +25,9 @@ BEGIN ALTER TABLE expenses ADD CONSTRAINT fk_expenses_supplier_id FOREIGN KEY (supplier_id) REFERENCES suppliers(id); - END IF; + +END IF; + END $$; -- Tambahkan Foreign Key ke users (created_by) diff --git a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql index 70fcf148..89bd80a6 100644 --- a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql +++ b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql @@ -1 +1,3 @@ -DROP TABLE IF EXISTS expense_nonstocks; \ No newline at end of file +DROP TABLE IF EXISTS expense_nonstocks; + +DROP SEQUENCE expenses_ref_seq; \ No newline at end of file diff --git a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql index 5b0c2c16..330c4d7b 100644 --- a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql +++ b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql @@ -1,7 +1,8 @@ CREATE TABLE expense_nonstocks ( id BIGSERIAL PRIMARY KEY, - expense_id BIGINT, - project_flock_kandang_id BIGINT, + expense_id BIGINT NOT NULL, + project_flock_kandang_id BIGINT NULL, + kandang_id BIGINT NULL, nonstock_id BIGINT, qty NUMERIC(15, 3) NOT NULL, unit_price NUMERIC(15, 3) NOT NULL, @@ -32,6 +33,16 @@ BEGIN END IF; END $$; +-- Tambahkan Foreign key ke kandang_id +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN + ALTER TABLE expense_nonstocks + ADD CONSTRAINT fk_expense_nonstocks_kandang_id_2 + FOREIGN KEY (kandang_id) REFERENCES kandangs(id); + END IF; +END $$; + -- Tambahkan Foreign Key ke nonstocks DO $$ BEGIN diff --git a/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql b/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql index 4a8dc148..0886cd7a 100644 --- a/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql +++ b/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql @@ -1,6 +1,6 @@ CREATE TABLE expense_realizations ( id BIGSERIAL PRIMARY KEY, - expense_nonstock_id BIGINT, + expense_nonstock_id BIGINT UNIQUE, realization_qty NUMERIC(15, 3) NOT NULL, realization_unit_price NUMERIC(15, 3) NOT NULL, realization_total_price NUMERIC(15, 3) NOT NULL, diff --git a/internal/entities/expense.go b/internal/entities/expense.go index 286eaf51..b665a599 100644 --- a/internal/entities/expense.go +++ b/internal/entities/expense.go @@ -9,10 +9,10 @@ import ( type Expense struct { Id uint64 `gorm:"primaryKey;autoIncrement"` - ReferenceNumber *string `gorm:"type:varchar(50)"` + ReferenceNumber *string `gorm:"type:varchar(50);uniqueIndex"` SupplierId *uint64 `gorm:""` Category string `gorm:"type:varchar(50);not null"` - PoNumber string `gorm:"uniqueIndex;not null;type:varchar(50)"` + PoNumber *string `gorm:"type:varchar(50)"` DocumentPath sql.NullString `gorm:"type:json"` ExpenseDate time.Time `gorm:"type:date;not null"` GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` diff --git a/internal/entities/expense_nonstock.go b/internal/entities/expense_nonstock.go index eb27efef..ae2d02fe 100644 --- a/internal/entities/expense_nonstock.go +++ b/internal/entities/expense_nonstock.go @@ -23,5 +23,5 @@ type ExpenseNonstock struct { Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"` - Realizations []ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` + Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` } diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go index 074f2f0a..3a9d76bb 100644 --- a/internal/modules/expenses/controllers/expense.controller.go +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -1,8 +1,11 @@ package controller import ( + "encoding/json" + "fmt" "math" "strconv" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" @@ -29,7 +32,7 @@ func (u *ExpenseController) GetAll(c *fiber.Ctx) error { Search: c.Query("search", ""), } - if query.Page < 1 || query.Limit < 1 { + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -49,7 +52,7 @@ func (u *ExpenseController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToExpenseListDTOs(result), + Data: result, }) } @@ -71,15 +74,39 @@ func (u *ExpenseController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get expense successfully", - Data: dto.ToExpenseListDTO(*result), + Data: result, }) } func (u *ExpenseController) CreateOne(c *fiber.Ctx) error { req := new(validation.Create) - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + req.TransactionDate = c.FormValue("transaction_date") + + supplierID, err := strconv.ParseUint(c.FormValue("supplier_id"), 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format") + } + req.SupplierID = supplierID + + form, err := c.MultipartForm() + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") + } + req.Documents = form.File["documents"] + + costPerKandangJSON := c.FormValue("cost_per_kandangs") + if costPerKandangJSON != "" { + + if err := json.Unmarshal([]byte(costPerKandangJSON), &req.CostPerKandangs); err != nil { + + var singleCostPerKandang validation.CostPerKandang + if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err)) + } + + req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang} + } } result, err := u.ExpenseService.CreateOne(c, req) @@ -92,7 +119,7 @@ func (u *ExpenseController) CreateOne(c *fiber.Ctx) error { Code: fiber.StatusCreated, Status: "success", Message: "Create expense successfully", - Data: dto.ToExpenseListDTO(*result), + Data: result, }) } @@ -119,7 +146,7 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update expense successfully", - Data: dto.ToExpenseListDTO(*result), + Data: result, }) } @@ -142,3 +169,126 @@ func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error { Message: "Delete expense successfully", }) } + +func (u *ExpenseController) ApproveExpense(c *fiber.Ctx) error { + expenseID := c.Params("id") + id, err := strconv.Atoi(expenseID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") + } + + // Extract step from URL path (manager or finance) + path := c.Path() + var step string + if strings.Contains(path, "/approvals/manager") { + step = "Manager" + } else if strings.Contains(path, "/approvals/finance") { + step = "Finance" + } else { + return fiber.NewError(fiber.StatusBadRequest, "Invalid approval step") + } + + // Parse approval request + var req validation.ApprovalRequest + if err := c.BodyParser(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + // Approve expense + expense, err := u.ExpenseService.ApproveExpense(c, uint(id), step, req.Action, req.Notes) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Approve expense successfully", + Data: expense, + }) +} + +func (u *ExpenseController) CreateRealization(c *fiber.Ctx) error { + expenseID := c.Params("id") + id, err := strconv.Atoi(expenseID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") + } + + req := new(validation.CreateRealization) + + form, err := c.MultipartForm() + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") + } + req.Documents = form.File["documents"] + + // Parse realizations JSON + realizationsJSON := c.FormValue("realizations") + if realizationsJSON != "" { + if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err)) + } + } + + expense, err := u.ExpenseService.CreateRealization(c, uint(id), req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create realization successfully", + Data: expense, + }) +} + +func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error { + expenseID := c.Params("id") + id, err := strconv.Atoi(expenseID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") + } + + var req validation.UpdateRealization + if err := c.BodyParser(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Update realization successfully", + Data: expense, + }) +} + +func (u *ExpenseController) CompleteExpense(c *fiber.Ctx) error { + expenseID := c.Params("id") + id, err := strconv.Atoi(expenseID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") + } + + expense, err := u.ExpenseService.CompleteExpense(c, uint(id), nil) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Complete expense successfully", + Data: expense, + }) +} diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index b5fca80f..1f1fecef 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -1,68 +1,322 @@ package dto import ( + "fmt" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto" + supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === DTO Structs === +// === Base DTO === type ExpenseBaseDTO struct { - Id uint64 `json:"id"` - PoNumber string `json:"po_number"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` + Id uint64 `json:"id"` + ReferenceNumber string `json:"reference_number"` + PoNumber *string `json:"po_number,omitempty"` + Category string `json:"category"` + ExpenseDate time.Time `json:"expense_date"` + GrandTotal float64 `json:"grand_total"` } +// === List DTO (untuk GetAll) === + type ExpenseListDTO struct { - Id uint64 `json:"id"` - ReferenceNumber string `json:"reference_number"` - PoNumber string `json:"po_number"` - Category string `json:"category"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ExpenseBaseDTO + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` +} + +// === Detail DTO (untuk GetOne) === + +type ExpenseDetailDTO struct { + ExpenseBaseDTO + Supplier *supplierDTO.SupplierBaseDTO `json:"supplier,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + Kandangs []KandangGroupDTO `json:"kandangs,omitempty"` + TotalPengajuan float64 `json:"total_pengajuan"` + TotalRealisasi float64 `json:"total_realisasi"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` +} + +// === Nested DTO === + +type ExpenseNonstockDTO struct { + Id uint64 `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalPrice float64 `json:"total_price"` + Note *string `json:"note,omitempty"` + Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"` + ProjectFlockKandang *ProjectFlockKandangNestedDTO `json:"project_flock_kandang,omitempty"` + Realization *ExpenseRealizationDTO `json:"realization,omitempty"` +} + +type ProjectFlockKandangNestedDTO struct { + Id uint64 `json:"id"` + KandangId uint64 `json:"kandang_id"` +} + +type ExpenseRealizationDTO struct { + Id uint64 `json:"id"` + RealizationQty float64 `json:"realization_qty"` + RealizationUnitPrice float64 `json:"realization_unit_price"` + RealizationTotalPrice float64 `json:"realization_total_price"` + RealizationDate time.Time `json:"realization_date"` + Note *string `json:"note,omitempty"` +} + +type RealizationOnlyDTO struct { + Id uint64 `json:"id"` + RealizationQty float64 `json:"realization_qty"` + RealizationUnitPrice float64 `json:"realization_unit_price"` + RealizationTotalPrice float64 `json:"realization_total_price"` + RealizationDate time.Time `json:"realization_date"` + Note *string `json:"note,omitempty"` + Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"` + ProjectFlockKandang *ProjectFlockKandangNestedDTO `json:"project_flock_kandang,omitempty"` +} + +type KandangDTO struct { + Id uint64 `json:"id"` + KandangId uint64 `json:"kandang_id"` + Name string `json:"name,omitempty"` +} + +type KandangGroupDTO struct { + Id uint64 `json:"id"` + KandangId uint64 `json:"kandang_id"` + Name string `json:"name,omitempty"` + Pengajuans []ExpenseNonstockDTO `json:"pengajuans,omitempty"` + Realisasi []RealizationOnlyDTO `json:"realisasi,omitempty"` +} + +// === Helper Functions === + +func getStringValue(s *string) string { + if s == nil { + return "" + } + return *s } // === Mapper Functions === -func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO { +func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO { return ExpenseBaseDTO{ - Id: e.Id, - PoNumber: e.PoNumber, - ExpenseDate: e.ExpenseDate, - GrandTotal: e.GrandTotal, - } -} - -func ToExpenseListDTO(e entity.Expense) ExpenseListDTO { - var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) - createdUser = &mapped - } - - return ExpenseListDTO{ Id: e.Id, - ReferenceNumber: *e.ReferenceNumber, + ReferenceNumber: getStringValue(e.ReferenceNumber), PoNumber: e.PoNumber, Category: e.Category, ExpenseDate: e.ExpenseDate, GrandTotal: e.GrandTotal, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, } } -func ToExpenseListDTOs(e []entity.Expense) []ExpenseListDTO { - result := make([]ExpenseListDTO, len(e)) - for i, r := range e { - result[i] = ToExpenseListDTO(r) +func ToExpenseListDTO(e *entity.Expense) ExpenseListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + + var latestApproval *approvalDTO.ApprovalBaseDTO + if e.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + latestApproval = &mapped + } + + return ExpenseListDTO{ + ExpenseBaseDTO: ToExpenseBaseDTO(e), + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + LatestApproval: latestApproval, + } +} + +func ToExpenseListDTOs(expenses []entity.Expense) []ExpenseListDTO { + result := make([]ExpenseListDTO, len(expenses)) + for i, expense := range expenses { + result[i] = ToExpenseListDTO(&expense) } return result } + +func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + + var supplier *supplierDTO.SupplierBaseDTO + if e.Supplier != nil && e.Supplier.Id != 0 { + mapped := supplierDTO.ToSupplierBaseDTO(*e.Supplier) + supplier = &mapped + } + + var latestApproval *approvalDTO.ApprovalBaseDTO + if e.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + latestApproval = &mapped + } + + var pengajuans []ExpenseNonstockDTO + var realisasi []RealizationOnlyDTO + + if len(e.Nonstocks) > 0 { + pengajuans = make([]ExpenseNonstockDTO, 0) + realisasi = make([]RealizationOnlyDTO, 0) + + for _, ns := range e.Nonstocks { + // Create DTO without realization for pengajuans + pengajuanDTO := ToExpenseNonstockDTO(ns) + pengajuanDTO.Realization = nil // Remove realization from pengajuan + pengajuans = append(pengajuans, pengajuanDTO) + + // Create separate DTO with realization data if it exists + if ns.Realization != nil && ns.Realization.Id != 0 { + // Create realization DTO with only realization data + var nonstock *nonstockDTO.NonstockBaseDTO + if ns.Nonstock != nil && ns.Nonstock.Id != 0 { + mapped := nonstockDTO.ToNonstockBaseDTO(*ns.Nonstock) + nonstock = &mapped + } + + var projectFlockKandang *ProjectFlockKandangNestedDTO + if ns.ProjectFlockKandang != nil && ns.ProjectFlockKandang.Id != 0 { + projectFlockKandang = &ProjectFlockKandangNestedDTO{ + Id: uint64(ns.ProjectFlockKandang.Id), + KandangId: uint64(ns.ProjectFlockKandang.KandangId), + } + } + + realisasiDTO := RealizationOnlyDTO{ + Id: ns.Realization.Id, + RealizationQty: ns.Realization.RealizationQty, + RealizationUnitPrice: ns.Realization.RealizationUnitPrice, + RealizationTotalPrice: ns.Realization.RealizationTotalPrice, + RealizationDate: ns.Realization.RealizationDate, + Note: ns.Realization.Note, + Nonstock: nonstock, + ProjectFlockKandang: projectFlockKandang, + } + realisasi = append(realisasi, realisasiDTO) + } + } + } + + // Calculate total pengajuan and realisasi + var totalPengajuan float64 + for _, p := range pengajuans { + totalPengajuan += p.TotalPrice + } + + var totalRealisasi float64 + for _, r := range realisasi { + totalRealisasi += r.RealizationTotalPrice + } + + // Group pengajuans and realisasi by kandang + kandangMap := make(map[uint64]*KandangGroupDTO) + + // Process pengajuans + for _, p := range pengajuans { + if p.ProjectFlockKandang != nil { + kandangId := p.ProjectFlockKandang.KandangId + if kandangMap[kandangId] == nil { + kandangMap[kandangId] = &KandangGroupDTO{ + Id: p.ProjectFlockKandang.Id, + KandangId: kandangId, + Name: fmt.Sprintf("Kandang %d", kandangId), + } + } + kandangMap[kandangId].Pengajuans = append(kandangMap[kandangId].Pengajuans, p) + } + } + + // Process realisasi + for _, r := range realisasi { + if r.ProjectFlockKandang != nil { + kandangId := r.ProjectFlockKandang.KandangId + if kandangMap[kandangId] == nil { + kandangMap[kandangId] = &KandangGroupDTO{ + Id: r.ProjectFlockKandang.Id, + KandangId: kandangId, + Name: fmt.Sprintf("Kandang %d", kandangId), + } + } + kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r) + } + } + + // Convert map to slice + var kandangs []KandangGroupDTO + for _, k := range kandangMap { + kandangs = append(kandangs, *k) + } + + return ExpenseDetailDTO{ + ExpenseBaseDTO: ToExpenseBaseDTO(e), + Supplier: supplier, + CreatedUser: createdUser, + Kandangs: kandangs, + TotalPengajuan: totalPengajuan, + TotalRealisasi: totalRealisasi, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + LatestApproval: latestApproval, + } +} + +func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO { + var nonstock *nonstockDTO.NonstockBaseDTO + if ns.Nonstock != nil && ns.Nonstock.Id != 0 { + mapped := nonstockDTO.ToNonstockBaseDTO(*ns.Nonstock) + nonstock = &mapped + } + + var projectFlockKandang *ProjectFlockKandangNestedDTO + if ns.ProjectFlockKandang != nil && ns.ProjectFlockKandang.Id != 0 { + projectFlockKandang = &ProjectFlockKandangNestedDTO{ + Id: uint64(ns.ProjectFlockKandang.Id), + KandangId: uint64(ns.ProjectFlockKandang.KandangId), + } + } + + var realization *ExpenseRealizationDTO + if ns.Realization != nil && ns.Realization.Id != 0 { + mapped := ToExpenseRealizationDTO(*ns.Realization) + realization = &mapped + } + + return ExpenseNonstockDTO{ + Id: ns.Id, + Qty: ns.Qty, + UnitPrice: ns.UnitPrice, + TotalPrice: ns.TotalPrice, + Note: ns.Note, + Nonstock: nonstock, + ProjectFlockKandang: projectFlockKandang, + Realization: realization, + } +} + +func ToExpenseRealizationDTO(r entity.ExpenseRealization) ExpenseRealizationDTO { + return ExpenseRealizationDTO{ + Id: r.Id, + RealizationQty: r.RealizationQty, + RealizationUnitPrice: r.RealizationUnitPrice, + RealizationTotalPrice: r.RealizationTotalPrice, + RealizationDate: r.RealizationDate, + Note: r.Note, + } +} diff --git a/internal/modules/expenses/module.go b/internal/modules/expenses/module.go index c9b2ab66..2f71a349 100644 --- a/internal/modules/expenses/module.go +++ b/internal/modules/expenses/module.go @@ -1,15 +1,25 @@ package expenses import ( + "fmt" + "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" rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories" + rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" + rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type ExpenseModule struct{} @@ -17,10 +27,21 @@ type ExpenseModule struct{} func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { expenseRepo := rExpense.NewExpenseRepository(db) userRepo := rUser.NewUserRepository(db) + supplierRepo := rSupplier.NewSupplierRepository(db) + nonstockRepo := rNonstock.NewNonstockRepository(db) + approvalRepo := commonRepo.NewApprovalRepository(db) + realizationRepo := rExpense.NewExpenseRealizationRepository(db) + projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) - expenseService := sExpense.NewExpenseService(expenseRepo, validate) + approvalSvc := commonSvc.NewApprovalService(approvalRepo) + + // Register workflow steps for EXPENSES approval + if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register expense approval workflow: %v", err)) + } + + expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) ExpenseRoutes(router, userService, expenseService) } - diff --git a/internal/modules/expenses/repositories/expense.repository.go b/internal/modules/expenses/repositories/expense.repository.go index 8cc580be..588583da 100644 --- a/internal/modules/expenses/repositories/expense.repository.go +++ b/internal/modules/expenses/repositories/expense.repository.go @@ -11,6 +11,8 @@ import ( type ExpenseRepository interface { repository.BaseRepository[entity.Expense] IdExists(ctx context.Context, id uint64) (bool, error) + GetNextSequence(ctx context.Context) (int, error) + GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) } type ExpenseRepositoryImpl struct { @@ -26,3 +28,24 @@ func NewExpenseRepository(db *gorm.DB) ExpenseRepository { func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { return repository.Exists[entity.Expense](ctx, r.DB(), uint(id)) } + +func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) { + var sequence int + err := r.DB().Raw("SELECT nextval('expenses_ref_seq')").Scan(&sequence).Error + if err != nil { + return 0, err + } + return sequence, nil +} + +func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) { + var expense entity.Expense + err := r.DB().WithContext(ctx). + Where("id = ?", id). + Preload("Supplier"). + First(&expense).Error + if err != nil { + return nil, err + } + return &expense, nil +} diff --git a/internal/modules/expenses/repositories/expense_nonstock.repository.go b/internal/modules/expenses/repositories/expense_nonstock.repository.go index 257a3034..acda512b 100644 --- a/internal/modules/expenses/repositories/expense_nonstock.repository.go +++ b/internal/modules/expenses/repositories/expense_nonstock.repository.go @@ -11,6 +11,8 @@ import ( type ExpenseNonstockRepository interface { repository.BaseRepository[entity.ExpenseNonstock] IdExists(ctx context.Context, id uint64) (bool, error) + GetByExpenseID(ctx context.Context, expenseID uint64, id uint64) (bool, error) + GetWithRelations(ctx context.Context, id uint64) (*entity.ExpenseNonstock, error) } type ExpenseNonstockRepositoryImpl struct { @@ -26,3 +28,28 @@ func NewExpenseNonstockRepository(db *gorm.DB) ExpenseNonstockRepository { func (r *ExpenseNonstockRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { return repository.Exists[entity.ExpenseNonstock](ctx, r.DB(), uint(id)) } + +func (r *ExpenseNonstockRepositoryImpl) GetByExpenseID(ctx context.Context, expenseID uint64, id uint64) (bool, error) { + var count int64 + err := r.DB().WithContext(ctx).Model(&entity.ExpenseNonstock{}). + Where("id = ? AND expense_id = ?", id, expenseID). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +func (r *ExpenseNonstockRepositoryImpl) GetWithRelations(ctx context.Context, id uint64) (*entity.ExpenseNonstock, error) { + var expenseNonstock entity.ExpenseNonstock + err := r.DB().WithContext(ctx). + Where("id = ?", id). + Preload("Nonstock", func(db *gorm.DB) *gorm.DB { + return db.Preload("Suppliers") + }). + First(&expenseNonstock).Error + if err != nil { + return nil, err + } + return &expenseNonstock, nil +} diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index 0245f8a2..77f075f7 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -11,6 +11,7 @@ import ( type ExpenseRealizationRepository interface { repository.BaseRepository[entity.ExpenseRealization] IdExists(ctx context.Context, id uint64) (bool, error) + GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) } type ExpenseRealizationRepositoryImpl struct { @@ -26,3 +27,14 @@ func NewExpenseRealizationRepository(db *gorm.DB) ExpenseRealizationRepository { func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { return repository.Exists[entity.ExpenseRealization](ctx, r.DB(), uint(id)) } + +func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) { + var realization entity.ExpenseRealization + err := r.DB().WithContext(ctx). + Where("expense_nonstock_id = ?", expenseNonstockID). + First(&realization).Error + if err != nil { + return nil, err + } + return &realization, nil +} diff --git a/internal/modules/expenses/route.go b/internal/modules/expenses/route.go index 49a4e7c5..eafa5b7c 100644 --- a/internal/modules/expenses/route.go +++ b/internal/modules/expenses/route.go @@ -25,4 +25,9 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + route.Post("/:id/approvals/manager", ctrl.ApproveExpense) + route.Post("/:id/approvals/finance", ctrl.ApproveExpense) + route.Post("/:id/realizations", ctrl.CreateRealization) + route.Patch("/:id/realizations", ctrl.UpdateRealization) + route.Post("/:id/complete", ctrl.CompleteExpense) } diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 6f794e54..e0d2b343 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -1,12 +1,22 @@ package service import ( + "context" "errors" + "fmt" + "time" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations" + nonstockRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories" + supplierRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" + projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" "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" @@ -15,33 +25,78 @@ import ( ) type ExpenseService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.Expense, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Expense, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*expenseDto.ExpenseDetailDTO, error) DeleteOne(ctx *fiber.Ctx, id uint) error + ApproveExpense(ctx *fiber.Ctx, id uint, step string, action string, notes *string) (*expenseDto.ExpenseDetailDTO, error) + CreateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error) + CompleteExpense(ctx *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) + UpdateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) } type expenseService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.ExpenseRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ExpenseRepository + SupplierRepo supplierRepo.SupplierRepository + NonstockRepo nonstockRepo.NonstockRepository + ApprovalSvc commonSvc.ApprovalService + RealizationRepository repository.ExpenseRealizationRepository + ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository } -func NewExpenseService(repo repository.ExpenseRepository, validate *validator.Validate) ExpenseService { +func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) ExpenseService { return &expenseService{ - Log: utils.Log, - Validate: validate, - Repository: repo, + Log: utils.Log, + Validate: validate, + Repository: repo, + SupplierRepo: supplierRepo, + NonstockRepo: nonstockRepo, + ApprovalSvc: approvalSvc, + RealizationRepository: realizationRepo, + ProjectFlockKandangRepo: projectFlockKandangRepo, } } func (s expenseService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") + return db. + Preload("CreatedUser"). + Preload("Supplier"). + Preload("Nonstocks", func(db *gorm.DB) *gorm.DB { + return db.Preload("Nonstock").Preload("Realization").Preload("ProjectFlockKandang") + }) } -func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error) { +func (s expenseService) getExpenseWithDetails(c *fiber.Ctx, expenseId uint) (*expenseDto.ExpenseDetailDTO, error) { + expense, err := s.Repository.GetByID(c.Context(), expenseId, s.withRelations) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Expense not found for ID %d: %+v", expenseId, err) + return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + s.Log.Errorf("Failed to get expense for ID %d: %+v", expenseId, err) + return nil, err + } + + // Load latest approval with ActionUser + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseId, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + // Don't fail if approval loading fails, just log + s.Log.Warnf("Failed to load approval for expense %d: %+v", expenseId, err) + } + expense.LatestApproval = latestApproval + + responseDTO := expenseDto.ToExpenseDetailDTO(expense) + return &responseDTO, nil +} + +func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { + s.Log.Errorf("Validation failed for GetAll: %+v", err) return nil, 0, err } @@ -50,7 +105,7 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.Search != "" { - return db.Where("name LIKE ?", "%"+params.Search+"%") + return db.Where("category LIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) @@ -59,57 +114,218 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity s.Log.Errorf("Failed to get expenses: %+v", err) return nil, 0, err } - return expenses, total, nil + + // Load approvals for each expense + for i := range expenses { + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, uint(expenses[i].Id), func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Failed to load approval for expense %d: %+v", expenses[i].Id, err) + } + expenses[i].LatestApproval = latestApproval + } + + result := expenseDto.ToExpenseListDTOs(expenses) + return result, total, nil } -func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*entity.Expense, error) { - expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") +func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) { + return s.getExpenseWithDetails(c, id) +} + +func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err } + + // Validate supplier exists using common service + supplierID := uint(req.SupplierID) + supplierExistsFunc := func(ctx context.Context, id uint) (bool, error) { + return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id) + } + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: supplierExistsFunc}, + ); err != nil { + return nil, err + } + + // Get supplier for category + supplierEntity, err := s.SupplierRepo.GetByID(c.Context(), uint(req.SupplierID), nil) if err != nil { - s.Log.Errorf("Failed get expense by id: %+v", err) - return nil, err + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found") + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier") } - return expense, nil + + for _, costPerKandang := range req.CostPerKandangs { + for _, costItem := range costPerKandang.CostItems { + nonstockId := uint(costItem.NonstockID) + + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Nonstock", ID: &nonstockId, Exists: s.NonstockRepo.IdExists}, + ); err != nil { + return nil, err + } + + nonstockEntity, err := s.NonstockRepo.GetByID(c.Context(), nonstockId, func(db *gorm.DB) *gorm.DB { + return db.Preload("Suppliers") + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Nonstock not found") + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get nonstock") + } + + supplierFound := false + for _, sn := range nonstockEntity.Suppliers { + if uint64(sn.Id) == req.SupplierID { + supplierFound = true + break + } + } + if !supplierFound { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Nonstock ID %d does not belong to supplier ID %d", nonstockId, req.SupplierID)) + } + } + } + + expenseDate, err := utils.ParseDateString(req.TransactionDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") + } + + var expense *entity.Expense + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + expenseRepoTx := repository.NewExpenseRepository(dbTransaction) + expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + referenceNumber, err := s.generateReferenceNumber(dbTransaction) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number") + } + + var grandTotal float64 + for _, costPerKandang := range req.CostPerKandangs { + for _, costItem := range costPerKandang.CostItems { + grandTotal += costItem.TotalCost + } + } + + createdBy := uint64(1) //todo get from auth + expense = &entity.Expense{ + ReferenceNumber: &referenceNumber, + PoNumber: req.PoNumber, + Category: supplierEntity.Category, + SupplierId: &req.SupplierID, + ExpenseDate: expenseDate, + GrandTotal: grandTotal, + CreatedBy: &createdBy, + } + + if err := expenseRepoTx.CreateOne(c.Context(), expense, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense") + } + + if len(req.CostPerKandangs) > 0 { + + for _, costPerKandang := range req.CostPerKandangs { + + projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction) + projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(costPerKandang.KandangID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") + } + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang") + } + + for _, costItem := range costPerKandang.CostItems { + + projectFlockKandangId := uint64(projectFlockKandang.Id) + nonstockId := costItem.NonstockID + expenseNonstock := &entity.ExpenseNonstock{ + ExpenseId: &expense.Id, + ProjectFlockKandangId: &projectFlockKandangId, + NonstockId: &nonstockId, + Qty: costItem.Quantity, + TotalPrice: costItem.TotalCost, + Note: &costItem.Notes, + } + + if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense cost item") + } + } + } + } + + approvalAction := entity.ApprovalActionCreated + createdByUint := uint(createdBy) + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowExpense, + uint(expense.Id), + utils.ExpenseStepPengajuan, + &approvalAction, + createdByUint, + nil); err != nil { + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval") + } // TODO: Handle documents (save file references) + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense") + } + + return s.getExpenseWithDetails(c, uint(expense.Id)) } -func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Expense, error) { +func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*expenseDto.ExpenseDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { + s.Log.Errorf("Validation failed for UpdateOne: %+v", err) return nil, err } - createdBy := uint64(1) - createBody := &entity.Expense{ - PoNumber: req.PoNumber, - Category: req.Category, - CreatedBy: &createdBy, - } - - if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { - s.Log.Errorf("Failed to create expense: %+v", err) - return nil, err - } - - return s.GetOne(c, uint(createBody.Id)) -} - -func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) { - if err := s.Validate.Struct(req); err != nil { + // Validate expense exists using common service + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { + return s.Repository.IdExists(ctx, uint64(id)) + }}, + ); err != nil { return nil, err } updateBody := make(map[string]any) - if req.PoNumber != nil { - updateBody["po_number"] = *req.PoNumber - } - if req.Category != nil { - updateBody["category"] = *req.Category + if req.SupplierID != nil { + // Validate supplier exists using common service + supplierID := uint(*req.SupplierID) + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: func(ctx context.Context, id uint) (bool, error) { + return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id) + }}, + ); err != nil { + return nil, err + } + + updateBody["supplier_id"] = *req.SupplierID } if len(updateBody) == 0 { - return s.GetOne(c, id) + return s.getExpenseWithDetails(c, id) } if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { @@ -120,16 +336,368 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return nil, err } - return s.GetOne(c, id) + return s.getExpenseWithDetails(c, id) } func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error { - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Expense not found") - } - s.Log.Errorf("Failed to delete expense: %+v", err) + // Validate expense exists using common service + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { + return s.Repository.IdExists(ctx, uint64(id)) + }}, + ); err != nil { return err } + + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Expense not found for ID %d: %+v", id, err) + return fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + s.Log.Errorf("Failed to delete expense for ID %d: %+v", id, err) + return err + } + s.Log.Infof("Successfully deleted expense with ID %d", id) return nil } + +func (s *expenseService) ApproveExpense(c *fiber.Ctx, id uint, stepName string, action string, notes *string) (*expenseDto.ExpenseDetailDTO, error) { + + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { + return s.Repository.IdExists(ctx, uint64(id)) + }}, + ); err != nil { + return nil, err + } + + actorID := uint(1) // TODO: replace with authenticated user id + + var stepNumber approvalutils.ApprovalStep + switch stepName { + case "Manager": + stepNumber = utils.ExpenseStepManager + case "Finance": + stepNumber = utils.ExpenseStepFinance + default: + s.Log.Errorf("Invalid approval step: %s", stepName) + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid approval step") + } + + var expectedPreviousStep uint16 + switch stepName { + case "Manager": + expectedPreviousStep = uint16(utils.ExpenseStepPengajuan) + case "Finance": + expectedPreviousStep = uint16(utils.ExpenseStepManager) + } + + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") + } + + if latestApproval == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "No approval found. Please create expense first.") + } + + if latestApproval.StepNumber != expectedPreviousStep { + + expectedStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(expectedPreviousStep)] + currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] + + return nil, fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Cannot approve at step %s. Latest approval is at %s step. Expected previous step: %s", + stepName, currentStepName, expectedStepName)) + } + + var approvalAction entity.ApprovalAction + switch action { + case "APPROVED": + approvalAction = entity.ApprovalActionApproved + case "REJECTED": + approvalAction = entity.ApprovalActionRejected + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid approval action") + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + expenseRepoTx := repository.NewExpenseRepository(tx) + + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowExpense, + id, + stepNumber, + &approvalAction, + actorID, + notes); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") + } + + if stepName == "Finance" && action == "APPROVED" { + poNumber, err := s.generatePoNumber(tx, id) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate PO number") + } + + updateData := map[string]interface{}{ + "po_number": poNumber, + } + if err := expenseRepoTx.PatchOne(c.Context(), id, updateData, nil); err != nil { + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update PO number") + } + } + + return nil + }) + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to Approve") + } + + return s.getExpenseWithDetails(c, id) +} + +func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { + return s.Repository.IdExists(ctx, uint64(id)) + }}, + ); err != nil { + return nil, err + } + realizationDate := time.Now() + createdBy := uint64(1) // TODO: replace with authenticated user id + + if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + realizationRepoTx := repository.NewExpenseRealizationRepository(tx) + expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) + + for _, realizationItem := range req.Realizations { + + expenseNonstockID := realizationItem.ExpenseNonstockID + + belongsToExpense, err := expenseNonstockRepoTx.GetByExpenseID(c.Context(), uint64(expenseID), uint64(expenseNonstockID)) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate ExpenseNonstock relation") + } + if !belongsToExpense { + return fiber.NewError(fiber.StatusNotFound, "ExpenseNonstock not found or does not belong to this expense") + } + + _, err = realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) + if err == nil { + return fiber.NewError(fiber.StatusBadRequest, "Realization already exists for this expense nonstock") + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing realization") + } + + realization := &entity.ExpenseRealization{ + ExpenseNonstockId: &expenseNonstockID, + RealizationQty: realizationItem.Qty, + RealizationUnitPrice: realizationItem.UnitPrice, + RealizationTotalPrice: realizationItem.TotalPrice, + RealizationDate: realizationDate, + Note: realizationItem.Notes, + CreatedBy: &createdBy, + } + + if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization") + } + } + + approvalAction := entity.ApprovalActionCreated + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowExpense, + expenseID, + utils.ExpenseStepRealisasi, + &approvalAction, + uint(createdBy), + nil); err != nil { + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval") + } + + return nil + }); err != nil { + return nil, err + } + + return s.getExpenseWithDetails(c, expenseID) +} + +func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) { + // Validate expense exists using common service + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { + return s.Repository.IdExists(ctx, uint64(id)) + }}, + ); err != nil { + return nil, err + } + + actorID := uint(1) // TODO: replace with authenticated user id + + // Get latest approval to validate workflow + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to get latest approval for expense %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") + } + + // Check if expense can be completed (must be at Realisasi step) + if latestApproval == nil { + s.Log.Errorf("No approval found for expense %d", id) + return nil, fiber.NewError(fiber.StatusBadRequest, "No approval found. Please create expense first.") + } + + if latestApproval.StepNumber != uint16(utils.ExpenseStepRealisasi) { + currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] + s.Log.Errorf("Cannot complete expense at step %s. Must be at Realisasi step", currentStepName) + return nil, fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Cannot complete expense at %s step. Must be at Realisasi step", currentStepName)) + } + + // Create approval for Selesai step (step 5) using transaction + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + approvalAction := entity.ApprovalActionApproved + + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowExpense, + id, + utils.ExpenseStepSelesai, + &approvalAction, + actorID, + notes); err != nil { + s.Log.Errorf("Failed to create Selesai approval for expense %d: %+v", id, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to complete expense") + } + + return nil + }) + + if err != nil { + return nil, err + } + + return s.getExpenseWithDetails(c, id) +} + +func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) { + if err := s.Validate.Struct(req); err != nil { + s.Log.Errorf("Validation failed for UpdateRealization: %+v", err) + return nil, err + } + + // Validate Expense exists using common service + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { + return s.Repository.IdExists(ctx, uint64(id)) + }}, + ); err != nil { + return nil, err + } + + // Use current date for realization date + realizationDate := time.Now() + + // Update realizations using transaction + if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + realizationRepoTx := repository.NewExpenseRealizationRepository(tx) + expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) + + // Process each realization item + for _, realizationItem := range req.Realizations { + // Validate ExpenseNonstock exists and belongs to this expense + expenseNonstockID := realizationItem.ExpenseNonstockID + + belongsToExpense, err := expenseNonstockRepoTx.GetByExpenseID(c.Context(), uint64(expenseID), uint64(expenseNonstockID)) + if err != nil { + s.Log.Errorf("Failed to validate ExpenseNonstock relation: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate ExpenseNonstock relation") + } + if !belongsToExpense { + s.Log.Errorf("ExpenseNonstock not found or does not belong to expense %d", expenseID) + return fiber.NewError(fiber.StatusNotFound, "ExpenseNonstock not found or does not belong to this expense") + } + + // Get existing realization + existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Realization not found for expense nonstock %d", expenseNonstockID) + return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock") + } + s.Log.Errorf("Failed to get existing realization: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization") + } + + // Update realization + updateData := map[string]interface{}{ + "realization_qty": realizationItem.Qty, + "realization_unit_price": realizationItem.UnitPrice, + "realization_total_price": realizationItem.TotalPrice, + "realization_date": realizationDate, + } + + if realizationItem.Notes != nil { + updateData["note"] = *realizationItem.Notes + } + + if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil { + s.Log.Errorf("Failed to update realization: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization") + } + } + + return nil + }); err != nil { + return nil, err + } + + // Return updated expense + return s.getExpenseWithDetails(c, expenseID) +} + +func (s *expenseService) generateReferenceNumber(ctx *gorm.DB) (string, error) { + + sequence, err := s.Repository.GetNextSequence(ctx.Statement.Context) + if err != nil { + return "", err + } + refNum := fmt.Sprintf("BOP-LTI-%05d", sequence) + + return refNum, nil +} + +func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) { + + expenseRepoTx := repository.NewExpenseRepository(ctx) + expense, err := expenseRepoTx.GetByID(context.Background(), uint(expenseID), nil) + if err != nil { + return "", err + } + if expense.ReferenceNumber == nil { + return "", errors.New("reference number is required") + } + + poNum := fmt.Sprintf("PO-%s", *expense.ReferenceNumber) + + return poNum, nil +} diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index 8155d76f..53a8d95f 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -1,13 +1,41 @@ package validation +import ( + "mime/multipart" +) + +// ApprovalRequest is used for expense approval endpoints +type ApprovalRequest struct { + Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` + Notes *string `json:"notes"` +} + type Create struct { - PoNumber string `json:"po_number" validate:"required,max=50"` - Category string `json:"category" validate:"required,max=50"` + PoNumber *string `form:"po_number" validate:"omitempty,max=50"` + TransactionDate string `form:"transaction_date" validate:"required,datetime=2006-01-02"` + SupplierID uint64 `form:"supplier_id" validate:"required,gt=0"` + Documents []*multipart.FileHeader `form:"documents" validate:"omitempty,dive"` + CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" validate:"required,min=1,dive"` +} + +type CostPerKandang struct { + KandangID uint64 `json:"kandang_id" form:"kandang_id" validate:"required,gt=0"` + CostItems []CostItem `json:"cost_items" form:"cost_items" validate:"required,min=1,dive"` +} + +type CostItem struct { + NonstockID uint64 `json:"nonstock_id" form:"nonstock_id" validate:"required,gt=0"` + Quantity float64 `json:"quantity" form:"quantity" validate:"required,gt=0"` + TotalCost float64 `json:"total_cost" form:"total_cost" validate:"required,gt=0"` + Notes string `json:"notes" form:"notes" validate:"required,max=500"` } type Update struct { - PoNumber *string `json:"po_number,omitempty" validate:"omitempty,max=50"` - Category *string `json:"category,omitempty" validate:"omitempty,max=50"` + PoNumber *string `json:"po_number,omitempty" validate:"omitempty,max=50"` + TransactionDate *string `json:"transaction_date,omitempty" validate:"omitempty,datetime=2006-01-02"` + SupplierID *uint64 `json:"supplier_id,omitempty" validate:"omitempty,gt=0"` + Documents *[]string `json:"documents,omitempty" validate:"omitempty,dive,max=255"` + CostPerKandang *[]CostPerKandang `json:"cost_per_kandang,omitempty" validate:"omitempty,min=1,dive"` } type Query struct { @@ -15,3 +43,23 @@ type Query struct { Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Search string `query:"search" validate:"omitempty,max=50"` } + +type CreateRealization struct { + Documents []*multipart.FileHeader `form:"documents" validate:"omitempty,dive"` + Realizations []RealizationItem `json:"realizations" form:"realizations" validate:"required,min=1,dive"` +} + +type RealizationItem struct { + ExpenseNonstockID uint64 `json:"expense_nonstock_id" form:"expense_nonstock_id" validate:"required,gt=0"` + Qty float64 `json:"qty" form:"qty" validate:"required,gt=0"` + UnitPrice float64 `json:"unit_price" form:"unit_price" validate:"required,gt=0"` + TotalPrice float64 `json:"total_price" form:"total_price" validate:"required,gt=0"` + Notes *string `json:"notes" form:"notes" validate:"omitempty,max=500"` +} + +type CompleteExpense struct { +} + +type UpdateRealization struct { + Realizations []RealizationItem `json:"realizations" validate:"required,min=1,dive"` +} diff --git a/internal/modules/master/nonstocks/repositories/nonstock.repository.go b/internal/modules/master/nonstocks/repositories/nonstock.repository.go index affcbc4b..01188980 100644 --- a/internal/modules/master/nonstocks/repositories/nonstock.repository.go +++ b/internal/modules/master/nonstocks/repositories/nonstock.repository.go @@ -18,6 +18,7 @@ type NonstockRepository interface { SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error DeleteFlags(ctx context.Context, tx *gorm.DB, nonstockID uint) error GetFlags(ctx context.Context, nonstockID uint) ([]entity.Flag, error) + IdExists(ctx context.Context, id uint) (bool, error) } type NonstockRepositoryImpl struct { @@ -34,6 +35,10 @@ func (r *NonstockRepositoryImpl) NameExists(ctx context.Context, name string, ex return repository.ExistsByName[entity.Nonstock](ctx, r.DB(), name, excludeID) } +func (r *NonstockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Nonstock](ctx, r.DB(), id) +} + func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error { db := tx if db == 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 a2ab8ebe..1fffe73b 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -13,6 +13,7 @@ import ( type ProjectFlockKandangRepository interface { GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) + GetActiveByKandangID(ctx context.Context, kandangID 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, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) @@ -220,6 +221,35 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont return record, nil } +func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) { + record := new(entity.ProjectFlockKandang) + if err := r.db.WithContext(ctx). + Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). + Joins(` + INNER JOIN ( + SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at + FROM approvals + WHERE approvable_type = 'PROJECT_FLOCKS' + ORDER BY approvable_id, action_at DESC + ) latest_approval ON latest_approval.approvable_id = project_flocks.id + `). + Where("project_flock_kandangs.kandang_id = ?", kandangID). + Where("LOWER(latest_approval.step_name) = LOWER(?)", "Aktif"). + Order("project_flock_kandangs.id DESC"). + Preload("ProjectFlock"). + Preload("ProjectFlock.Fcr"). + Preload("ProjectFlock.Area"). + Preload("ProjectFlock.Location"). + Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.Kandangs"). + Preload("ProjectFlock.KandangHistory"). + Preload("Kandang"). + First(record).Error; err != nil { + return nil, err + } + return record, nil +} + func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) { if len(kandangIDs) == 0 { return nil, nil diff --git a/internal/utils/constant.go b/internal/utils/constant.go index d1cb7415..98381df6 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -260,12 +260,16 @@ const ( ExpenseStepPengajuan approvalutils.ApprovalStep = 1 ExpenseStepManager approvalutils.ApprovalStep = 2 ExpenseStepFinance approvalutils.ApprovalStep = 3 + ExpenseStepRealisasi approvalutils.ApprovalStep = 4 + ExpenseStepSelesai approvalutils.ApprovalStep = 5 ) var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{ ExpenseStepPengajuan: "Pengajuan", - ExpenseStepManager: "Manager", - ExpenseStepFinance: "Finance", + ExpenseStepManager: "Approval Manager", + ExpenseStepFinance: "Approval Finance", + ExpenseStepRealisasi: "Realisasi", + ExpenseStepSelesai: "Selesai", } // ------------------------------------------------------------------- From 4c7e5b073114ffd3203f50d9c109a950f0505a74 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 20 Nov 2025 08:27:02 +0700 Subject: [PATCH 50/59] Feat[BE-261,265]: add category request body on create on --- .../controllers/expense.controller.go | 1 + internal/modules/expenses/dto/expense.dto.go | 38 +++- .../expenses/services/expense.service.go | 194 ++++++++++++++++-- .../validations/expense.validation.go | 1 + 4 files changed, 206 insertions(+), 28 deletions(-) diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go index 3a9d76bb..2d0bebac 100644 --- a/internal/modules/expenses/controllers/expense.controller.go +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -82,6 +82,7 @@ func (u *ExpenseController) CreateOne(c *fiber.Ctx) error { req := new(validation.Create) req.TransactionDate = c.FormValue("transaction_date") + req.Category = c.FormValue("category") supplierID, err := strconv.ParseUint(c.FormValue("supplier_id"), 10, 64) if err != nil { diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index 1f1fecef..ae048ca1 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -1,11 +1,13 @@ package dto import ( + "encoding/json" "fmt" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" @@ -14,12 +16,14 @@ import ( // === Base DTO === type ExpenseBaseDTO struct { - Id uint64 `json:"id"` - ReferenceNumber string `json:"reference_number"` - PoNumber *string `json:"po_number,omitempty"` - Category string `json:"category"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` + Id uint64 `json:"id"` + ReferenceNumber string `json:"reference_number"` + PoNumber *string `json:"po_number"` + Category string `json:"category"` + Documents []string `json:"documents,omitempty"` + ExpenseDate time.Time `json:"expense_date"` + GrandTotal float64 `json:"grand_total"` + Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` } // === List DTO (untuk GetAll) === @@ -110,13 +114,33 @@ func getStringValue(s *string) string { // === Mapper Functions === func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO { + var documents []string + var location *locationDTO.LocationBaseDTO + + // Parse document paths from JSON if available + if e.DocumentPath.Valid && e.DocumentPath.String != "" { + if err := json.Unmarshal([]byte(e.DocumentPath.String), &documents); err == nil { + // Successfully parsed documents + } + } + + // Get location from the first kandang if available + if len(e.Nonstocks) > 0 && e.Nonstocks[0].ProjectFlockKandang != nil { + if e.Nonstocks[0].ProjectFlockKandang.Kandang.Location.Id != 0 { + mapped := locationDTO.ToLocationBaseDTO(e.Nonstocks[0].ProjectFlockKandang.Kandang.Location) + location = &mapped + } + } + return ExpenseBaseDTO{ Id: e.Id, ReferenceNumber: getStringValue(e.ReferenceNumber), - PoNumber: e.PoNumber, + PoNumber: e.PoNumber, // Keep as pointer to allow null in JSON Category: e.Category, + Documents: documents, ExpenseDate: e.ExpenseDate, GrandTotal: e.GrandTotal, + Location: location, } } diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index e0d2b343..3ed792fb 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "errors" "fmt" "time" @@ -65,7 +66,7 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB { Preload("CreatedUser"). Preload("Supplier"). Preload("Nonstocks", func(db *gorm.DB) *gorm.DB { - return db.Preload("Nonstock").Preload("Realization").Preload("ProjectFlockKandang") + return db.Preload("Nonstock").Preload("Realization").Preload("ProjectFlockKandang.Kandang.Location") }) } @@ -150,15 +151,6 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen return nil, err } - // Get supplier for category - supplierEntity, err := s.SupplierRepo.GetByID(c.Context(), uint(req.SupplierID), nil) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found") - } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier") - } - for _, costPerKandang := range req.CostPerKandangs { for _, costItem := range costPerKandang.CostItems { nonstockId := uint(costItem.NonstockID) @@ -220,7 +212,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen expense = &entity.Expense{ ReferenceNumber: &referenceNumber, PoNumber: req.PoNumber, - Category: supplierEntity.Category, + Category: req.Category, SupplierId: &req.SupplierID, ExpenseDate: expenseDate, GrandTotal: grandTotal, @@ -276,12 +268,36 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen &approvalAction, createdByUint, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval") + } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval") - } // TODO: Handle documents (save file references) + // Handle documents - save file paths as JSON array + if len(req.Documents) > 0 { + documentPaths := make([]string, len(req.Documents)) + for i, doc := range req.Documents { + // Generate dummy path for each document + // In real implementation, this would save the file and return the actual path + documentPath := fmt.Sprintf("/documents/expenses/%d_%d_%s", expense.Id, i+1, doc.Filename) + documentPaths[i] = documentPath + } + + // Save document paths as JSON in expense record + documentPathsJSON, err := json.Marshal(documentPaths) + if err != nil { + s.Log.Errorf("Failed to marshal document paths: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to save document references") + } + + if err := expenseRepoTx.PatchOne(c.Context(), uint(expense.Id), map[string]interface{}{ + "document_path": string(documentPathsJSON), + }, nil); err != nil { + s.Log.Errorf("Failed to save document paths: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to save document references") + } + } - return nil - }) + return nil + }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { @@ -308,6 +324,23 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return nil, err } + // Get latest approval to validate workflow + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to get latest approval for expense %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") + } + + // Check if expense can be updated (must be before Realisasi step) + if latestApproval != nil { + if latestApproval.StepNumber >= uint16(utils.ExpenseStepRealisasi) { + currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] + s.Log.Errorf("Cannot update expense at %s step. Must be before Realisasi step", currentStepName) + return nil, fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Cannot update expense at %s step. Must be before Realisasi step", currentStepName)) + } + } + updateBody := make(map[string]any) if req.SupplierID != nil { @@ -324,16 +357,135 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody["supplier_id"] = *req.SupplierID } - if len(updateBody) == 0 { + if req.TransactionDate != nil { + // Parse transaction_date + expenseDate, err := utils.ParseDateString(*req.TransactionDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") + } + updateBody["expense_date"] = expenseDate + } + + if req.PoNumber != nil { + updateBody["po_number"] = *req.PoNumber + } + + if len(updateBody) == 0 && req.CostPerKandang == nil { return s.getExpenseWithDetails(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, "Expense not found") + // Update expense using transaction + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + expenseRepoTx := repository.NewExpenseRepository(tx) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) + + // Update expense + if len(updateBody) > 0 { + if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + s.Log.Errorf("Failed to update expense: %+v", err) + return err + } } - s.Log.Errorf("Failed to update expense: %+v", err) - return nil, err + + // Update cost per kandang if provided + if req.CostPerKandang != nil { + // First, delete existing expense nonstocks using GORM + if err := tx.Where("expense_id = ?", id).Delete(&entity.ExpenseNonstock{}).Error; err != nil { + s.Log.Errorf("Failed to delete existing expense nonstocks: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense items") + } + + // Calculate new grand total + var grandTotal float64 + for _, cpk := range *req.CostPerKandang { + for _, costItem := range cpk.CostItems { + grandTotal += costItem.TotalCost + } + } + + // Update expense grand total + if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{ + "grand_total": grandTotal, + }, nil); err != nil { + s.Log.Errorf("Failed to update expense grand total: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense grand total") + } + + // Create new expense nonstocks + for _, cpk := range *req.CostPerKandang { + // Get active project_flock_kandang for this kandang + projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) + projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang") + } + + for _, costItem := range cpk.CostItems { + // Validate nonstock exists + nonstockId := uint(costItem.NonstockID) + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Nonstock", ID: &nonstockId, Exists: s.NonstockRepo.IdExists}, + ); err != nil { + return err + } + + projectFlockKandangId := uint64(projectFlockKandang.Id) + expenseId := uint64(id) + expenseNonstock := &entity.ExpenseNonstock{ + ExpenseId: &expenseId, + ProjectFlockKandangId: &projectFlockKandangId, + NonstockId: &costItem.NonstockID, + Qty: costItem.Quantity, + TotalPrice: costItem.TotalCost, + Note: &costItem.Notes, + } + + if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { + s.Log.Errorf("Failed to create expense nonstock: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense cost item") + } + } + } + } + + // Reset approval step to Pengajuan + actorID := uint(1) // TODO: replace with authenticated user id + var approvalAction entity.ApprovalAction + + // Check if latest approval was Updated, then use Updated action, otherwise use Created + if latestApproval != nil && latestApproval.Action != nil && *latestApproval.Action == entity.ApprovalActionUpdated { + approvalAction = entity.ApprovalActionUpdated + } else { + approvalAction = entity.ApprovalActionCreated + } + + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowExpense, + id, + utils.ExpenseStepPengajuan, + &approvalAction, + actorID, + nil); err != nil { + s.Log.Errorf("Failed to reset approval to Pengajuan for expense %d: %+v", id, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step") + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense") } return s.getExpenseWithDetails(c, id) diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index 53a8d95f..56420e06 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -13,6 +13,7 @@ type ApprovalRequest struct { type Create struct { PoNumber *string `form:"po_number" validate:"omitempty,max=50"` TransactionDate string `form:"transaction_date" validate:"required,datetime=2006-01-02"` + Category string `form:"category" validate:"required,oneof=BOP NON-BOP"` SupplierID uint64 `form:"supplier_id" validate:"required,gt=0"` Documents []*multipart.FileHeader `form:"documents" validate:"omitempty,dive"` CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" validate:"required,min=1,dive"` From b502751b4e257fc5330f12fd53104b4e868859fd Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 20 Nov 2025 08:50:47 +0700 Subject: [PATCH 51/59] Fix[BE-261]: change realization json to not using prefix Realization --- internal/modules/expenses/dto/expense.dto.go | 50 ++++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index ae048ca1..70afd4d4 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -69,20 +69,20 @@ type ProjectFlockKandangNestedDTO struct { } type ExpenseRealizationDTO struct { - Id uint64 `json:"id"` - RealizationQty float64 `json:"realization_qty"` - RealizationUnitPrice float64 `json:"realization_unit_price"` - RealizationTotalPrice float64 `json:"realization_total_price"` - RealizationDate time.Time `json:"realization_date"` - Note *string `json:"note,omitempty"` + Id uint64 `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalPrice float64 `json:"total_price"` + Date time.Time `json:"date"` + Note *string `json:"note,omitempty"` } type RealizationOnlyDTO struct { Id uint64 `json:"id"` - RealizationQty float64 `json:"realization_qty"` - RealizationUnitPrice float64 `json:"realization_unit_price"` - RealizationTotalPrice float64 `json:"realization_total_price"` - RealizationDate time.Time `json:"realization_date"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalPrice float64 `json:"total_price"` + Date time.Time `json:"date"` Note *string `json:"note,omitempty"` Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"` ProjectFlockKandang *ProjectFlockKandangNestedDTO `json:"project_flock_kandang,omitempty"` @@ -224,14 +224,14 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { } realisasiDTO := RealizationOnlyDTO{ - Id: ns.Realization.Id, - RealizationQty: ns.Realization.RealizationQty, - RealizationUnitPrice: ns.Realization.RealizationUnitPrice, - RealizationTotalPrice: ns.Realization.RealizationTotalPrice, - RealizationDate: ns.Realization.RealizationDate, - Note: ns.Realization.Note, - Nonstock: nonstock, - ProjectFlockKandang: projectFlockKandang, + Id: ns.Realization.Id, + Qty: ns.Realization.RealizationQty, + UnitPrice: ns.Realization.RealizationUnitPrice, + TotalPrice: ns.Realization.RealizationTotalPrice, + Date: ns.Realization.RealizationDate, + Note: ns.Realization.Note, + Nonstock: nonstock, + ProjectFlockKandang: projectFlockKandang, } realisasi = append(realisasi, realisasiDTO) } @@ -246,7 +246,7 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { var totalRealisasi float64 for _, r := range realisasi { - totalRealisasi += r.RealizationTotalPrice + totalRealisasi += r.TotalPrice } // Group pengajuans and realisasi by kandang @@ -336,11 +336,11 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO { func ToExpenseRealizationDTO(r entity.ExpenseRealization) ExpenseRealizationDTO { return ExpenseRealizationDTO{ - Id: r.Id, - RealizationQty: r.RealizationQty, - RealizationUnitPrice: r.RealizationUnitPrice, - RealizationTotalPrice: r.RealizationTotalPrice, - RealizationDate: r.RealizationDate, - Note: r.Note, + Id: r.Id, + Qty: r.RealizationQty, + UnitPrice: r.RealizationUnitPrice, + TotalPrice: r.RealizationTotalPrice, + Date: r.RealizationDate, + Note: r.Note, } } From 228aedc215f2a4be768181edbeae815cf6d20d0a Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Thu, 20 Nov 2025 14:59:50 +0700 Subject: [PATCH 52/59] fix(BE-273): add object nonstock and supplier in response get one and fix name base to relation in dto --- go.mod | 10 +- go.sum | 19 - internal/database/seed/seeder.go | 8 +- internal/entities/nonstock.go | 8 +- internal/entities/nonstock_supplier.go | 7 +- internal/entities/product.go | 10 +- internal/entities/product_supplier.go | 7 +- internal/entities/supplier.go | 4 +- .../controllers/approval.controller.go | 2 +- .../modules/approvals/dto/approval.dto.go | 36 +- internal/modules/expenses/dto/expense.dto.go | 28 +- .../adjustments/dto/adjustment.dto.go | 62 +-- .../dto/product_warehouse.dto.go | 80 ++-- .../inventory/transfers/dto/transfer.dto.go | 34 +- .../dto/delivery-orders.dto.go | 120 ++--- internal/modules/master/areas/dto/area.dto.go | 26 +- internal/modules/master/banks/dto/bank.dto.go | 26 +- .../master/customers/dto/customer.dto.go | 32 +- internal/modules/master/fcrs/dto/fcr.dto.go | 26 +- .../modules/master/flocks/dto/flock.dto.go | 26 +- .../master/kandangs/dto/kandang.dto.go | 56 +-- .../master/locations/dto/location.dto.go | 40 +- .../master/nonstocks/dto/nonstock.dto.go | 55 +-- .../repositories/nonstock.repository.go | 8 +- .../nonstocks/services/nonstock.service.go | 3 +- .../dto/product-category.dto.go | 26 +- .../master/products/dto/product.dto.go | 80 ++-- .../repositories/product.repository.go | 16 +- .../products/services/product.service.go | 3 +- .../controllers/supplier.controller.go | 9 +- .../master/suppliers/dto/supplier.dto.go | 70 +-- .../suppliers/dto/supplier_nonstock.dto.go | 50 +++ .../suppliers/dto/supplier_product.dto.go | 54 +++ internal/modules/master/suppliers/route.go | 4 +- .../suppliers/services/supplier.service.go | 20 +- .../validations/supplier.validation.go | 7 +- internal/modules/master/uoms/dto/uom.dto.go | 20 +- .../master/warehouses/dto/warehouse.dto.go | 64 +-- .../production/chickins/dto/chickin.dto.go | 138 +++--- .../project_flock_kandang.controller.go | 2 +- .../dto/project_flock_kandang.dto.go | 112 ++--- .../services/project_flock_kandang.service.go | 16 +- .../controllers/projectflock.controller.go | 9 +- .../project_flocks/dto/projectflock.dto.go | 84 ++-- .../dto/projectflock_kandang.dto.go | 54 +-- .../services/projectflock.service.go | 16 +- .../recordings/dto/recording.dto.go | 66 +-- .../dto/transfer_laying.dto.go | 54 +-- .../modules/purchases/dto/purchase.dto.go | 94 ++-- internal/modules/users/dto/user.dto.go | 14 +- test/integration/master_data/area_test.go | 27 -- test/integration/master_data/bank_test.go | 127 ------ test/integration/master_data/customer_test.go | 189 -------- test/integration/master_data/fcr_test.go | 218 --------- test/integration/master_data/kandang_test.go | 96 ---- test/integration/master_data/location_test.go | 35 -- test/integration/master_data/master_data.go | 401 ----------------- test/integration/master_data/nonstock_test.go | 309 ------------- .../master_data/product_category_test.go | 150 ------- test/integration/master_data/product_test.go | 410 ----------------- .../master_data/project_flock_test.go | 417 ------------------ test/integration/master_data/supplier_test.go | 238 ---------- .../integration/master_data/warehouse_test.go | 96 ---- tools/templates/dto.tmpl | 12 +- 64 files changed, 964 insertions(+), 3576 deletions(-) create mode 100644 internal/modules/master/suppliers/dto/supplier_nonstock.dto.go create mode 100644 internal/modules/master/suppliers/dto/supplier_product.dto.go delete mode 100644 test/integration/master_data/area_test.go delete mode 100644 test/integration/master_data/bank_test.go delete mode 100644 test/integration/master_data/customer_test.go delete mode 100644 test/integration/master_data/fcr_test.go delete mode 100644 test/integration/master_data/kandang_test.go delete mode 100644 test/integration/master_data/location_test.go delete mode 100644 test/integration/master_data/master_data.go delete mode 100644 test/integration/master_data/nonstock_test.go delete mode 100644 test/integration/master_data/product_category_test.go delete mode 100644 test/integration/master_data/product_test.go delete mode 100644 test/integration/master_data/project_flock_test.go delete mode 100644 test/integration/master_data/supplier_test.go delete mode 100644 test/integration/master_data/warehouse_test.go diff --git a/go.mod b/go.mod index 078bcfe0..517bcdc1 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,10 @@ go 1.23 require ( github.com/MicahParks/keyfunc/v2 v2.1.0 github.com/bytedance/sonic v1.12.1 - github.com/glebarez/sqlite v1.11.0 github.com/go-playground/validator/v10 v10.27.0 github.com/gofiber/contrib/jwt v1.0.10 github.com/gofiber/fiber/v2 v2.52.5 github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/uuid v1.6.0 github.com/jackc/pgconn v1.14.1 github.com/redis/go-redis/v9 v9.14.0 github.com/sirupsen/logrus v1.9.3 @@ -27,13 +25,12 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect @@ -54,7 +51,6 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/philhofer/fwd v1.1.2 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -79,8 +75,4 @@ require ( golang.org/x/text v0.22.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.22.5 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/sqlite v1.23.1 // indirect ) diff --git a/go.sum b/go.sum index ea477c5d..c07e37e3 100644 --- a/go.sum +++ b/go.sum @@ -27,18 +27,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= -github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= -github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -56,8 +50,6 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -154,9 +146,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -317,12 +306,4 @@ gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= -modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= -modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= -modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 7c1f8a1e..d848711e 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -82,7 +82,7 @@ func Run(db *gorm.DB) error { return err } - if err := seedTransferStock(tx, adminID); err != nil { + if err := seedTransferStock(tx); err != nil { return err } fmt.Println("✅ Master data seeding completed") @@ -692,7 +692,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories var existing entity.ProductSupplier err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error if errors.Is(err, gorm.ErrRecordNotFound) { - link := entity.ProductSupplier{ProductID: product.Id, SupplierID: supplierID} + link := entity.ProductSupplier{ProductId: product.Id, SupplierId: supplierID} if err := tx.Create(&link).Error; err != nil { return err } @@ -765,7 +765,7 @@ func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers var existing entity.NonstockSupplier err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error if errors.Is(err, gorm.ErrRecordNotFound) { - link := entity.NonstockSupplier{NonstockID: nonstock.Id, SupplierID: supplierID} + link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID} if err := tx.Create(&link).Error; err != nil { return err } @@ -929,7 +929,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error { return nil } -func seedTransferStock(tx *gorm.DB, createdBy uint) error { +func seedTransferStock(tx *gorm.DB) error { transfer := entity.StockTransfer{ FromWarehouseId: 1, diff --git a/internal/entities/nonstock.go b/internal/entities/nonstock.go index 3f291cc5..0e78ca8b 100644 --- a/internal/entities/nonstock.go +++ b/internal/entities/nonstock.go @@ -15,8 +15,8 @@ type Nonstock struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Uom Uom `gorm:"foreignKey:UomId;references:Id"` - Suppliers []Supplier `gorm:"many2many:nonstock_suppliers;joinForeignKey:NonstockID;joinReferences:SupplierID"` - Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Uom Uom `gorm:"foreignKey:UomId;references:Id"` + NonstockSuppliers []NonstockSupplier `gorm:"foreignKey:NonstockId;references:Id"` + Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"` } diff --git a/internal/entities/nonstock_supplier.go b/internal/entities/nonstock_supplier.go index 2d56a680..2206390c 100644 --- a/internal/entities/nonstock_supplier.go +++ b/internal/entities/nonstock_supplier.go @@ -3,7 +3,10 @@ package entities import "time" type NonstockSupplier struct { - NonstockID uint `gorm:"primaryKey"` - SupplierID uint `gorm:"primaryKey"` + NonstockId uint `gorm:"not null"` + SupplierId uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` + + Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"` + Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"` } diff --git a/internal/entities/product.go b/internal/entities/product.go index 120a018c..52b04627 100644 --- a/internal/entities/product.go +++ b/internal/entities/product.go @@ -22,9 +22,9 @@ type Product struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Uom Uom `gorm:"foreignKey:UomId;references:Id"` - ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"` - Suppliers []Supplier `gorm:"many2many:product_suppliers;joinForeignKey:ProductID;joinReferences:SupplierID"` - Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Uom Uom `gorm:"foreignKey:UomId;references:Id"` + ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"` + ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"` + Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"` } diff --git a/internal/entities/product_supplier.go b/internal/entities/product_supplier.go index 9aa2deec..d64b1e85 100644 --- a/internal/entities/product_supplier.go +++ b/internal/entities/product_supplier.go @@ -3,7 +3,10 @@ package entities import "time" type ProductSupplier struct { - ProductID uint `gorm:"primaryKey"` - SupplierID uint `gorm:"primaryKey"` + ProductId uint `gorm:"not null"` + SupplierId uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` + + Product Product `gorm:"foreignKey:ProductId;references:Id"` + Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"` } diff --git a/internal/entities/supplier.go b/internal/entities/supplier.go index 141c4a58..7d801896 100644 --- a/internal/entities/supplier.go +++ b/internal/entities/supplier.go @@ -26,5 +26,7 @@ type Supplier struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + ProductSuppliers []ProductSupplier `gorm:"foreignKey:SupplierId;references:Id"` + NonstockSuppliers []NonstockSupplier `gorm:"foreignKey:SupplierId;references:Id"` } diff --git a/internal/modules/approvals/controllers/approval.controller.go b/internal/modules/approvals/controllers/approval.controller.go index fd0baa6e..94a66afd 100644 --- a/internal/modules/approvals/controllers/approval.controller.go +++ b/internal/modules/approvals/controllers/approval.controller.go @@ -85,7 +85,7 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error { flat := dto.ToApprovalDTOs(records) return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.ApprovalBaseDTO]{ + JSON(response.SuccessWithPaginate[dto.ApprovalRelationDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get All approvals successfully", diff --git a/internal/modules/approvals/dto/approval.dto.go b/internal/modules/approvals/dto/approval.dto.go index 52b99fc6..0ef9f8d5 100644 --- a/internal/modules/approvals/dto/approval.dto.go +++ b/internal/modules/approvals/dto/approval.dto.go @@ -10,24 +10,24 @@ import ( approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" ) -type ApprovalBaseDTO struct { - Id uint `json:"id"` - 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 ApprovalRelationDTO struct { + Id uint `json:"id"` + StepNumber uint16 `json:"step_number"` + StepName string `json:"step_name"` + Action *string `json:"action"` + Notes *string `json:"notes"` + ActionBy userDTO.UserRelationDTO `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"` + StepNumber uint16 `json:"step_number"` + StepName string `json:"step_name"` + Approvals []ApprovalRelationDTO `json:"approvals"` } -func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO { - dto := ApprovalBaseDTO{ +func ToApprovalDTO(e entity.Approval) ApprovalRelationDTO { + dto := ApprovalRelationDTO{ Id: e.Id, Notes: e.Notes, } @@ -54,10 +54,10 @@ func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO { } if e.ActionUser != nil && e.ActionUser.Id != 0 { - user := userDTO.ToUserBaseDTO(*e.ActionUser) + user := userDTO.ToUserRelationDTO(*e.ActionUser) dto.ActionBy = user } else if e.ActionBy != nil && *e.ActionBy != 0 { - dto.ActionBy = userDTO.UserBaseDTO{ + dto.ActionBy = userDTO.UserRelationDTO{ Id: *e.ActionBy, IdUser: int64(*e.ActionBy), } @@ -71,8 +71,8 @@ func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO { return dto } -func ToApprovalDTOs(items []entity.Approval) []ApprovalBaseDTO { - result := make([]ApprovalBaseDTO, len(items)) +func ToApprovalDTOs(items []entity.Approval) []ApprovalRelationDTO { + result := make([]ApprovalRelationDTO, len(items)) for i, item := range items { result[i] = ToApprovalDTO(item) } @@ -86,7 +86,7 @@ func ToApprovalGroupDTOs(items []entity.Approval) []ApprovalGroupDTO { type groupAccumulator struct { StepName string - Approvals []ApprovalBaseDTO + Approvals []ApprovalRelationDTO } groups := make(map[uint16]*groupAccumulator) diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index b5fca80f..21403ed9 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -9,7 +9,7 @@ import ( // === DTO Structs === -type ExpenseBaseDTO struct { +type ExpenseRelationDTO struct { Id uint64 `json:"id"` PoNumber string `json:"po_number"` ExpenseDate time.Time `json:"expense_date"` @@ -17,21 +17,21 @@ type ExpenseBaseDTO struct { } type ExpenseListDTO struct { - Id uint64 `json:"id"` - ReferenceNumber string `json:"reference_number"` - PoNumber string `json:"po_number"` - Category string `json:"category"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint64 `json:"id"` + ReferenceNumber string `json:"reference_number"` + PoNumber string `json:"po_number"` + Category string `json:"category"` + ExpenseDate time.Time `json:"expense_date"` + GrandTotal float64 `json:"grand_total"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // === Mapper Functions === -func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO { - return ExpenseBaseDTO{ +func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO { + return ExpenseRelationDTO{ Id: e.Id, PoNumber: e.PoNumber, ExpenseDate: e.ExpenseDate, @@ -40,9 +40,9 @@ func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO { } func ToExpenseListDTO(e entity.Expense) ExpenseListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(*e.CreatedUser) createdUser = &mapped } diff --git a/internal/modules/inventory/adjustments/dto/adjustment.dto.go b/internal/modules/inventory/adjustments/dto/adjustment.dto.go index d577e134..f91e6eda 100644 --- a/internal/modules/inventory/adjustments/dto/adjustment.dto.go +++ b/internal/modules/inventory/adjustments/dto/adjustment.dto.go @@ -10,28 +10,28 @@ import ( // === DTO Structs === -type ProductBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - SKU string `json:"sku"` - ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"` +type ProductRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + SKU string `json:"sku"` + ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` } -type WarehouseBaseDTO struct { +type WarehouseRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` } type ProductWarehouseDTO struct { - Id uint `json:"id"` - ProductId uint `json:"product_id"` - WarehouseId uint `json:"warehouse_id"` - Quantity float64 `json:"quantity"` - Product *ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` + Id uint `json:"id"` + ProductId uint `json:"product_id"` + WarehouseId uint `json:"warehouse_id"` + Quantity float64 `json:"quantity"` + Product *ProductRelationDTO `json:"product,omitempty"` + Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"` } -type AdjustmentBaseDTO struct { +type AdjustmentRelationDTO struct { Id uint `json:"id"` TransactionType string `json:"transaction_type"` Quantity float64 `json:"quantity"` @@ -43,9 +43,9 @@ type AdjustmentBaseDTO struct { } type AdjustmentListDTO struct { - AdjustmentBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` + AdjustmentRelationDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` } type AdjustmentDetailDTO struct { @@ -55,7 +55,7 @@ type AdjustmentDetailDTO struct { // === Mapper Functions === -func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO { +func ToProductRelationDTO(e *entity.Product) *ProductRelationDTO { if e == nil { return nil } @@ -64,13 +64,13 @@ func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO { sku = *e.Sku } - var category *productCategoryDTO.ProductCategoryBaseDTO + var category *productCategoryDTO.ProductCategoryRelationDTO if e.ProductCategory.Id != 0 { - mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory) + mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory) category = &mapped } - return &ProductBaseDTO{ + return &ProductRelationDTO{ Id: e.Id, Name: e.Name, SKU: sku, @@ -78,11 +78,11 @@ func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO { } } -func ToWarehouseBaseDTO(e *entity.Warehouse) *WarehouseBaseDTO { +func ToWarehouseRelationDTO(e *entity.Warehouse) *WarehouseRelationDTO { if e == nil { return nil } - return &WarehouseBaseDTO{ + return &WarehouseRelationDTO{ Id: e.Id, Name: e.Name, } @@ -97,13 +97,13 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO { ProductId: e.ProductId, WarehouseId: e.WarehouseId, Quantity: e.Quantity, - Product: ToProductBaseDTO(&e.Product), - Warehouse: ToWarehouseBaseDTO(&e.Warehouse), + Product: ToProductRelationDTO(&e.Product), + Warehouse: ToWarehouseRelationDTO(&e.Warehouse), } } -func ToAdjustmentBaseDTO(e *entity.StockLog) AdjustmentBaseDTO { - return AdjustmentBaseDTO{ +func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO { + return AdjustmentRelationDTO{ Id: e.Id, TransactionType: e.TransactionType, Quantity: e.Quantity, @@ -116,9 +116,9 @@ func ToAdjustmentBaseDTO(e *entity.StockLog) AdjustmentBaseDTO { } func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser != nil { - createdUser = &userDTO.UserBaseDTO{ + createdUser = &userDTO.UserRelationDTO{ Id: e.CreatedUser.Id, IdUser: e.CreatedUser.IdUser, Email: e.CreatedUser.Email, @@ -127,9 +127,9 @@ func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO { } return AdjustmentListDTO{ - AdjustmentBaseDTO: ToAdjustmentBaseDTO(e), - CreatedUser: createdUser, - CreatedAt: e.CreatedAt, + AdjustmentRelationDTO: ToAdjustmentRelationDTO(e), + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, } } 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 f88a6ca3..06889670 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -9,7 +9,7 @@ import ( // === DTO Structs === -type ProductWarehouseBaseDTO struct { +type ProductWarehouseRelationDTO struct { Id uint `json:"id"` ProductId uint `json:"product_id"` WarehouseId uint `json:"warehouse_id"` @@ -17,21 +17,21 @@ type ProductWarehouseBaseDTO struct { } type ProductWarehousNestedDTO struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` + Id uint `json:"id"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"` } type ProductWarehouseListDTO struct { - ProductWarehouseBaseDTO - Product *productDTO.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"` + ProductWarehouseRelationDTO + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"` + CreatedUser *UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } -type UserBaseDTO struct { +type UserRelationDTO struct { Id uint `json:"id"` Username string `json:"username"` } @@ -41,40 +41,40 @@ type ProductWarehouseDetailDTO struct { } // Nested DTOs for relations -type ProductBaseDTO struct { +type ProductRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` Sku string `json:"sku"` Flags []string `json:"flags"` } -type WarehouseBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Kandang *KandangBaseDTO `json:"kandang,omitempty"` - Location *LocationBaseDTO `json:"location,omitempty"` - Area *AreaBaseDTO `json:"area,omitempty"` +type WarehouseRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Kandang *KandangRelationDTO `json:"kandang,omitempty"` + Location *LocationRelationDTO `json:"location,omitempty"` + Area *AreaRelationDTO `json:"area,omitempty"` } -type KandangBaseDTO struct { +type KandangRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` } -type LocationBaseDTO struct { +type LocationRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` } -type AreaBaseDTO struct { +type AreaRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` } // === Mapper Functions === -func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDTO { - return ProductWarehouseBaseDTO{ +func ToProductWarehouseRelationDTO(e entity.ProductWarehouse) ProductWarehouseRelationDTO { + return ProductWarehouseRelationDTO{ Id: e.Id, ProductId: e.ProductId, // Field yang benar dari entity WarehouseId: e.WarehouseId, // Field yang benar dari entity @@ -83,12 +83,12 @@ func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDT } func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO { - product := productDTO.ToProductBaseDTO(e.Product) + product := productDTO.ToProductRelationDTO(e.Product) return ProductWarehousNestedDTO{ Id: e.Id, Product: &product, - Warehouse: &WarehouseBaseDTO{ + Warehouse: &WarehouseRelationDTO{ Id: e.Warehouse.Id, Name: e.Warehouse.Name, }, @@ -97,40 +97,40 @@ func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNeste func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO { dto := ProductWarehouseListDTO{ - ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, + ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, } // Map Product relation jika ada if e.Product.Id != 0 { - product := productDTO.ToProductBaseDTO(e.Product) + product := productDTO.ToProductRelationDTO(e.Product) dto.Product = &product } // Map Warehouse relation jika ada if e.Warehouse.Id != 0 { - warehouse := WarehouseBaseDTO{ + warehouse := WarehouseRelationDTO{ 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{ + warehouse.Kandang = &KandangRelationDTO{ 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{ + warehouse.Location = &LocationRelationDTO{ Id: e.Warehouse.Location.Id, Name: e.Warehouse.Location.Name, } } if e.Warehouse.Area.Id != 0 { - warehouse.Area = &AreaBaseDTO{ + warehouse.Area = &AreaRelationDTO{ Id: e.Warehouse.Area.Id, Name: e.Warehouse.Area.Name, } @@ -141,7 +141,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT // Map CreatedUser relation jika ada if e.CreatedUser.Id != 0 { - user := UserBaseDTO{ + user := UserRelationDTO{ Id: e.CreatedUser.Id, Username: e.CreatedUser.Name, } @@ -165,22 +165,22 @@ func ToProductWarehouseDetailDTO(e entity.ProductWarehouse) ProductWarehouseDeta } } -func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO { - return KandangBaseDTO{ +func ToKandangRelationDTO(e entity.Kandang) KandangRelationDTO { + return KandangRelationDTO{ Id: e.Id, Name: e.Name, } } -func ToLocationBaseDTO(e entity.Location) LocationBaseDTO { - return LocationBaseDTO{ +func ToLocationRelationDTO(e entity.Location) LocationRelationDTO { + return LocationRelationDTO{ Id: e.Id, Name: e.Name, } } -func ToAreaBaseDTO(e entity.Area) AreaBaseDTO { - return AreaBaseDTO{ +func ToAreaRelationDTO(e entity.Area) AreaRelationDTO { + return AreaRelationDTO{ Id: e.Id, Name: e.Name, } diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index cb85af94..fe97ce0f 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -9,7 +9,7 @@ import ( // === DTO Structs === -type TransferBaseDTO struct { +type TransferRelationDTO struct { Id uint64 `json:"id"` TransferReason string `json:"transfer_reason"` TransferDate string `json:"transfer_date"` @@ -51,12 +51,12 @@ type WarehouseDetailDTO struct { } type TransferListDTO struct { - TransferBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Details []TransferDetailItemDTO `json:"details"` - Deliveries []TransferDeliveryDTO `json:"deliveries"` + TransferRelationDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Details []TransferDetailItemDTO `json:"details"` + Deliveries []TransferDeliveryDTO `json:"deliveries"` } type TransferDetailDTO struct { @@ -93,7 +93,7 @@ type TransferDeliveryItemDTO struct { // === Mapper Functions === -func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO { +func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO { var sourceWarehouse *WarehouseDetailDTO if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 { @@ -103,7 +103,7 @@ func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO { if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 { destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse) } - return TransferBaseDTO{ + return TransferRelationDTO{ Id: e.Id, TransferReason: e.Reason, TransferDate: e.CreatedAt.Format("2006-01-02"), @@ -145,9 +145,9 @@ func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO { } func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser != nil { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(*e.CreatedUser) createdUser = &mapped } // Map details @@ -190,12 +190,12 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { }) } return TransferListDTO{ - TransferBaseDTO: ToTransferBaseDTO(e), - CreatedUser: createdUser, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - Details: details, - Deliveries: deliveries, + TransferRelationDTO: ToTransferRelationDTO(e), + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + Details: details, + Deliveries: deliveries, } } diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go index 397bf0ef..b3ac5a05 100644 --- a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -12,7 +12,7 @@ import ( userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -type MarketingBaseDTO struct { +type MarketingRelationDTO struct { Id uint `json:"id"` SoNumber string `json:"so_number"` SoDate time.Time `json:"so_date"` @@ -20,28 +20,28 @@ type MarketingBaseDTO struct { } type MarketingListDTO struct { - MarketingBaseDTO - Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` - SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` - SoDocs string `json:"so_docs,omitempty"` - SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` + MarketingRelationDTO + Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` + SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"` + SoDocs string `json:"so_docs,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` } type MarketingDetailDTO struct { - MarketingBaseDTO - Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` - SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` - SoDocs string `json:"so_docs,omitempty"` - SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` - DeliveryOrder []DeliveryGroupDTO `json:"delivery_order,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` + MarketingRelationDTO + Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` + SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"` + SoDocs string `json:"so_docs,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` + DeliveryOrder []DeliveryGroupDTO `json:"delivery_order,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` } type MarketingDeliveryProductDTO struct { Id uint `json:"id"` @@ -67,10 +67,10 @@ type DeliveryItemDTO struct { } type DeliveryGroupDTO struct { - DoNumber string `json:"do_number"` - DeliveryDate *time.Time `json:"delivery_date"` - Warehouse *productwarehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - Deliveries []DeliveryItemDTO `json:"deliveries"` + DoNumber string `json:"do_number"` + DeliveryDate *time.Time `json:"delivery_date"` + Warehouse *productwarehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + Deliveries []DeliveryItemDTO `json:"deliveries"` } type MarketingProductDTO struct { @@ -86,8 +86,8 @@ type MarketingProductDTO struct { VehicleNumber string `json:"vehicle_number,omitempty"` } -func ToMarketingBaseDTO(marketing *entity.Marketing) MarketingBaseDTO { - return MarketingBaseDTO{ +func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO { + return MarketingRelationDTO{ Id: marketing.Id, SoNumber: marketing.SoNumber, SoDate: marketing.SoDate, @@ -131,25 +131,25 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD } func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if marketing.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser) + mapped := userDTO.ToUserRelationDTO(marketing.CreatedUser) createdUser = &mapped } - var customer *customerDTO.CustomerBaseDTO + var customer *customerDTO.CustomerRelationDTO if marketing.Customer.Id != 0 { - mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer) + mapped := customerDTO.ToCustomerRelationDTO(marketing.Customer) customer = &mapped } - var salesPerson *userDTO.UserBaseDTO + var salesPerson *userDTO.UserRelationDTO if marketing.SalesPerson.Id != 0 { - mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson) + mapped := userDTO.ToUserRelationDTO(marketing.SalesPerson) salesPerson = &mapped } - var latestApproval *approvalDTO.ApprovalBaseDTO + var latestApproval *approvalDTO.ApprovalRelationDTO if marketing.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) latestApproval = &mapped @@ -164,34 +164,34 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M } return MarketingListDTO{ - MarketingBaseDTO: ToMarketingBaseDTO(marketing), - Customer: customer, - SalesPerson: salesPerson, - SoDocs: marketing.SoDocs, - SalesOrder: salesOrderProducts, - CreatedUser: createdUser, - CreatedAt: marketing.CreatedAt, - UpdatedAt: marketing.UpdatedAt, - LatestApproval: latestApproval, + MarketingRelationDTO: ToMarketingRelationDTO(marketing), + Customer: customer, + SalesPerson: salesPerson, + SoDocs: marketing.SoDocs, + SalesOrder: salesOrderProducts, + CreatedUser: createdUser, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + LatestApproval: latestApproval, } } func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingDetailDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if marketing.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser) + mapped := userDTO.ToUserRelationDTO(marketing.CreatedUser) createdUser = &mapped } - var customer *customerDTO.CustomerBaseDTO + var customer *customerDTO.CustomerRelationDTO if marketing.Customer.Id != 0 { - mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer) + mapped := customerDTO.ToCustomerRelationDTO(marketing.Customer) customer = &mapped } - var salesPerson *userDTO.UserBaseDTO + var salesPerson *userDTO.UserRelationDTO if marketing.SalesPerson.Id != 0 { - mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson) + mapped := userDTO.ToUserRelationDTO(marketing.SalesPerson) salesPerson = &mapped } @@ -214,23 +214,23 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs, marketing.SoNumber) - var latestApproval *approvalDTO.ApprovalBaseDTO + var latestApproval *approvalDTO.ApprovalRelationDTO if marketing.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) latestApproval = &mapped } return MarketingDetailDTO{ - MarketingBaseDTO: ToMarketingBaseDTO(marketing), - SoDocs: marketing.SoDocs, - Customer: customer, - SalesPerson: salesPerson, - SalesOrder: salesOrderProducts, - DeliveryOrder: deliveryGroups, - CreatedUser: createdUser, - CreatedAt: marketing.CreatedAt, - UpdatedAt: marketing.UpdatedAt, - LatestApproval: latestApproval, + MarketingRelationDTO: ToMarketingRelationDTO(marketing), + SoDocs: marketing.SoDocs, + Customer: customer, + SalesPerson: salesPerson, + SalesOrder: salesOrderProducts, + DeliveryOrder: deliveryGroups, + CreatedUser: createdUser, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + LatestApproval: latestApproval, } } @@ -285,7 +285,7 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri if !exists { group = &DeliveryGroupDTO{ DeliveryDate: product.DeliveryDate, - Warehouse: &productwarehouseDTO.WarehouseBaseDTO{ + Warehouse: &productwarehouseDTO.WarehouseRelationDTO{ Id: warehouseId, Name: warehouseName, }, diff --git a/internal/modules/master/areas/dto/area.dto.go b/internal/modules/master/areas/dto/area.dto.go index eceff7a9..a3e08607 100644 --- a/internal/modules/master/areas/dto/area.dto.go +++ b/internal/modules/master/areas/dto/area.dto.go @@ -9,16 +9,16 @@ import ( // === DTO Structs === -type AreaBaseDTO struct { +type AreaRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` } type AreaListDTO struct { - AreaBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + AreaRelationDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type AreaDetailDTO struct { @@ -27,25 +27,25 @@ type AreaDetailDTO struct { // === Mapper Functions === -func ToAreaBaseDTO(e entity.Area) AreaBaseDTO { - return AreaBaseDTO{ +func ToAreaRelationDTO(e entity.Area) AreaRelationDTO { + return AreaRelationDTO{ Id: e.Id, Name: e.Name, } } func ToAreaListDTO(e entity.Area) AreaListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } return AreaListDTO{ - AreaBaseDTO: ToAreaBaseDTO(e), - CreatedUser: createdUser, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, + AreaRelationDTO: ToAreaRelationDTO(e), + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, } } diff --git a/internal/modules/master/banks/dto/bank.dto.go b/internal/modules/master/banks/dto/bank.dto.go index 415c5f6b..d440b197 100644 --- a/internal/modules/master/banks/dto/bank.dto.go +++ b/internal/modules/master/banks/dto/bank.dto.go @@ -9,7 +9,7 @@ import ( // === DTO Structs === -type BankBaseDTO struct { +type BankRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` Alias string `json:"alias"` @@ -18,10 +18,10 @@ type BankBaseDTO struct { } type BankListDTO struct { - BankBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + BankRelationDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type BankDetailDTO struct { @@ -30,8 +30,8 @@ type BankDetailDTO struct { // === Mapper Functions === -func ToBankBaseDTO(e entity.Bank) BankBaseDTO { - return BankBaseDTO{ +func ToBankRelationDTO(e entity.Bank) BankRelationDTO { + return BankRelationDTO{ Id: e.Id, Name: e.Name, Alias: e.Alias, @@ -41,17 +41,17 @@ func ToBankBaseDTO(e entity.Bank) BankBaseDTO { } func ToBankListDTO(e entity.Bank) BankListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } return BankListDTO{ - BankBaseDTO: ToBankBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + BankRelationDTO: ToBankRelationDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } diff --git a/internal/modules/master/customers/dto/customer.dto.go b/internal/modules/master/customers/dto/customer.dto.go index e1fb8d0d..333c1297 100644 --- a/internal/modules/master/customers/dto/customer.dto.go +++ b/internal/modules/master/customers/dto/customer.dto.go @@ -9,7 +9,7 @@ import ( // === DTO Structs === -type CustomerBaseDTO struct { +type CustomerRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` PicId uint `json:"pic_id"` @@ -20,14 +20,14 @@ type CustomerBaseDTO struct { AccountNumber string `json:"account_number"` Balance float64 `json:"balance"` - Pic *userDTO.UserBaseDTO `json:"pic,omitempty"` + Pic *userDTO.UserRelationDTO `json:"pic,omitempty"` } type CustomerListDTO struct { - CustomerBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CustomerRelationDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CustomerDetailDTO struct { @@ -36,14 +36,14 @@ type CustomerDetailDTO struct { // === Mapper Functions === -func ToCustomerBaseDTO(e entity.Customer) CustomerBaseDTO { - var pic *userDTO.UserBaseDTO +func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO { + var pic *userDTO.UserRelationDTO if e.Pic.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.Pic) + mapped := userDTO.ToUserRelationDTO(e.Pic) pic = &mapped } - return CustomerBaseDTO{ + return CustomerRelationDTO{ Id: e.Id, Name: e.Name, PicId: e.PicId, @@ -57,17 +57,17 @@ func ToCustomerBaseDTO(e entity.Customer) CustomerBaseDTO { } func ToCustomerListDTO(e entity.Customer) CustomerListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } return CustomerListDTO{ - CustomerBaseDTO: ToCustomerBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + CustomerRelationDTO: ToCustomerRelationDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } diff --git a/internal/modules/master/fcrs/dto/fcr.dto.go b/internal/modules/master/fcrs/dto/fcr.dto.go index c88bb1a5..7b2a4e68 100644 --- a/internal/modules/master/fcrs/dto/fcr.dto.go +++ b/internal/modules/master/fcrs/dto/fcr.dto.go @@ -9,7 +9,7 @@ import ( // === DTO Structs === -type FcrBaseDTO struct { +type FcrRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` } @@ -22,10 +22,10 @@ type FcrStandardDTO struct { } type FcrListDTO struct { - FcrBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + FcrRelationDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type FcrDetailDTO struct { @@ -35,25 +35,25 @@ type FcrDetailDTO struct { // === Mapper Functions === -func ToFcrBaseDTO(e entity.Fcr) FcrBaseDTO { - return FcrBaseDTO{ +func ToFcrRelationDTO(e entity.Fcr) FcrRelationDTO { + return FcrRelationDTO{ Id: e.Id, Name: e.Name, } } func ToFcrListDTO(e entity.Fcr) FcrListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } return FcrListDTO{ - FcrBaseDTO: ToFcrBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + FcrRelationDTO: ToFcrRelationDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } diff --git a/internal/modules/master/flocks/dto/flock.dto.go b/internal/modules/master/flocks/dto/flock.dto.go index 8038ddb0..d5a44ee2 100644 --- a/internal/modules/master/flocks/dto/flock.dto.go +++ b/internal/modules/master/flocks/dto/flock.dto.go @@ -9,16 +9,16 @@ import ( // === DTO Structs === -type FlockBaseDTO struct { +type FlockRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` } type FlockListDTO struct { - FlockBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + FlockRelationDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type FlockDetailDTO struct { @@ -27,25 +27,25 @@ type FlockDetailDTO struct { // === Mapper Functions === -func ToFlockBaseDTO(e entity.Flock) FlockBaseDTO { - return FlockBaseDTO{ +func ToFlockRelationDTO(e entity.Flock) FlockRelationDTO { + return FlockRelationDTO{ Id: e.Id, Name: e.Name, } } func ToFlockListDTO(e entity.Flock) FlockListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } return FlockListDTO{ - FlockBaseDTO: ToFlockBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + FlockRelationDTO: ToFlockRelationDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } diff --git a/internal/modules/master/kandangs/dto/kandang.dto.go b/internal/modules/master/kandangs/dto/kandang.dto.go index 5e775c68..9f99d5fa 100644 --- a/internal/modules/master/kandangs/dto/kandang.dto.go +++ b/internal/modules/master/kandangs/dto/kandang.dto.go @@ -10,25 +10,25 @@ import ( // === DTO Structs === -type KandangBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Capacity float64 `json:"capacity"` - Location locationDTO.LocationBaseDTO `json:"location,omitempty"` - Pic userDTO.UserBaseDTO `json:"pic,omitempty"` +type KandangRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Capacity float64 `json:"capacity"` + Location locationDTO.LocationRelationDTO `json:"location,omitempty"` + Pic userDTO.UserRelationDTO `json:"pic,omitempty"` } type KandangListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Capacity float64 `json:"capacity"` - Location locationDTO.LocationBaseDTO `json:"location"` - Pic userDTO.UserBaseDTO `json:"pic"` - CreatedUser userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Capacity float64 `json:"capacity"` + Location locationDTO.LocationRelationDTO `json:"location"` + Pic userDTO.UserRelationDTO `json:"pic"` + CreatedUser userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type KandangDetailDTO struct { @@ -37,20 +37,20 @@ type KandangDetailDTO struct { // === Mapper Functions === -func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO { - var location locationDTO.LocationBaseDTO +func ToKandangRelationDTO(e entity.Kandang) KandangRelationDTO { + var location locationDTO.LocationRelationDTO if e.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(e.Location) + mapped := locationDTO.ToLocationRelationDTO(e.Location) location = mapped } - var pic userDTO.UserBaseDTO + var pic userDTO.UserRelationDTO if e.Pic.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.Pic) + mapped := userDTO.ToUserRelationDTO(e.Pic) pic = mapped } - return KandangBaseDTO{ + return KandangRelationDTO{ Id: e.Id, Name: e.Name, Status: e.Status, @@ -61,21 +61,21 @@ func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO { } func ToKandangListDTO(e entity.Kandang) KandangListDTO { - var location locationDTO.LocationBaseDTO + var location locationDTO.LocationRelationDTO if e.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(e.Location) + mapped := locationDTO.ToLocationRelationDTO(e.Location) location = mapped } - var pic userDTO.UserBaseDTO + var pic userDTO.UserRelationDTO if e.Pic.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.Pic) + mapped := userDTO.ToUserRelationDTO(e.Pic) pic = mapped } - var createdUser userDTO.UserBaseDTO + var createdUser userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = mapped } diff --git a/internal/modules/master/locations/dto/location.dto.go b/internal/modules/master/locations/dto/location.dto.go index d5d5b26e..ce3d5821 100644 --- a/internal/modules/master/locations/dto/location.dto.go +++ b/internal/modules/master/locations/dto/location.dto.go @@ -10,21 +10,21 @@ import ( // === DTO Structs === -type LocationBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Address string `json:"address"` - Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` +type LocationRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Address string `json:"address"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` } type LocationListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Address string `json:"address"` - Area *areaDTO.AreaBaseDTO `json:"area"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + Address string `json:"address"` + Area *areaDTO.AreaRelationDTO `json:"area"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type LocationDetailDTO struct { @@ -33,14 +33,14 @@ type LocationDetailDTO struct { // === Mapper Functions === -func ToLocationBaseDTO(e entity.Location) LocationBaseDTO { - var area *areaDTO.AreaBaseDTO +func ToLocationRelationDTO(e entity.Location) LocationRelationDTO { + var area *areaDTO.AreaRelationDTO if e.Area.Id != 0 { - mapped := areaDTO.ToAreaBaseDTO(e.Area) + mapped := areaDTO.ToAreaRelationDTO(e.Area) area = &mapped } - return LocationBaseDTO{ + return LocationRelationDTO{ Id: e.Id, Name: e.Name, Address: e.Address, @@ -49,15 +49,15 @@ func ToLocationBaseDTO(e entity.Location) LocationBaseDTO { } func ToLocationListDTO(e entity.Location) LocationListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } - var area *areaDTO.AreaBaseDTO + var area *areaDTO.AreaRelationDTO if e.Area.Id != 0 { - mapped := areaDTO.ToAreaBaseDTO(e.Area) + mapped := areaDTO.ToAreaRelationDTO(e.Area) area = &mapped } diff --git a/internal/modules/master/nonstocks/dto/nonstock.dto.go b/internal/modules/master/nonstocks/dto/nonstock.dto.go index fb30b6cf..dd187230 100644 --- a/internal/modules/master/nonstocks/dto/nonstock.dto.go +++ b/internal/modules/master/nonstocks/dto/nonstock.dto.go @@ -4,41 +4,39 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs === -type NonstockBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"` - Flags []string `json:"flags"` +type NonstockRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` + Flags []string `json:"flags"` } type NonstockListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Uom *uomDTO.UomBaseDTO `json:"uom"` - Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + Flags []string `json:"flags"` + Uom *uomDTO.UomRelationDTO `json:"uom"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type NonstockDetailDTO struct { NonstockListDTO - Flags []string `json:"flags"` } // === Mapper Functions === -func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO { - var uomRef *uomDTO.UomBaseDTO +func ToNonstockRelationDTO(e entity.Nonstock) NonstockRelationDTO { + var uomRef *uomDTO.UomRelationDTO if e.Uom.Id != 0 { - mapped := uomDTO.ToUomBaseDTO(e.Uom) + mapped := uomDTO.ToUomRelationDTO(e.Uom) uomRef = &mapped } @@ -47,7 +45,7 @@ func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO { flags[i] = f.Name } - return NonstockBaseDTO{ + return NonstockRelationDTO{ Id: e.Id, Name: e.Name, Uom: uomRef, @@ -56,23 +54,18 @@ func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO { } func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } - var uomRef *uomDTO.UomBaseDTO + var uomRef *uomDTO.UomRelationDTO if e.Uom.Id != 0 { - mapped := uomDTO.ToUomBaseDTO(e.Uom) + mapped := uomDTO.ToUomRelationDTO(e.Uom) uomRef = &mapped } - suppliers := make([]supplierDTO.SupplierBaseDTO, len(e.Suppliers)) - for i, s := range e.Suppliers { - suppliers[i] = supplierDTO.ToSupplierBaseDTO(s) - } - flags := make([]string, len(e.Flags)) for i, f := range e.Flags { flags[i] = f.Name @@ -81,11 +74,11 @@ func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO { return NonstockListDTO{ Id: e.Id, Name: e.Name, + Flags: flags, Uom: uomRef, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, - Suppliers: suppliers, } } @@ -98,13 +91,7 @@ func ToNonstockListDTOs(e []entity.Nonstock) []NonstockListDTO { } func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO { - flags := make([]string, len(e.Flags)) - for i, f := range e.Flags { - flags[i] = f.Name - } - return NonstockDetailDTO{ NonstockListDTO: ToNonstockListDTO(e), - Flags: flags, } } diff --git a/internal/modules/master/nonstocks/repositories/nonstock.repository.go b/internal/modules/master/nonstocks/repositories/nonstock.repository.go index affcbc4b..25a36e92 100644 --- a/internal/modules/master/nonstocks/repositories/nonstock.repository.go +++ b/internal/modules/master/nonstocks/repositories/nonstock.repository.go @@ -57,7 +57,7 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm existingMap := make(map[uint]struct{}, len(existing)) for _, rel := range existing { - existingMap[rel.SupplierID] = struct{}{} + existingMap[rel.SupplierId] = struct{}{} } incomingMap := make(map[uint]struct{}, len(supplierIDs)) @@ -66,16 +66,16 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm if _, exists := existingMap[id]; exists { continue } - record := entity.NonstockSupplier{NonstockID: nonstockID, SupplierID: id} + record := entity.NonstockSupplier{NonstockId: nonstockID, SupplierId: id} if err := db.WithContext(ctx).Create(&record).Error; err != nil { return err } } for _, rel := range existing { - if _, keep := incomingMap[rel.SupplierID]; !keep { + if _, keep := incomingMap[rel.SupplierId]; !keep { if err := db.WithContext(ctx). - Where("nonstock_id = ? AND supplier_id = ?", nonstockID, rel.SupplierID). + Where("nonstock_id = ? AND supplier_id = ?", nonstockID, rel.SupplierId). Delete(&entity.NonstockSupplier{}). Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/modules/master/nonstocks/services/nonstock.service.go b/internal/modules/master/nonstocks/services/nonstock.service.go index 3833432e..c0001a52 100644 --- a/internal/modules/master/nonstocks/services/nonstock.service.go +++ b/internal/modules/master/nonstocks/services/nonstock.service.go @@ -44,7 +44,8 @@ func (s nonstockService) withRelations(db *gorm.DB) *gorm.DB { Preload("CreatedUser"). Preload("Uom"). Preload("Flags"). - Preload("Suppliers", func(db *gorm.DB) *gorm.DB { + Preload("NonstockSuppliers"). + Preload("NonstockSuppliers.Supplier", func(db *gorm.DB) *gorm.DB { return db.Order("suppliers.name ASC") }) } diff --git a/internal/modules/master/product-categories/dto/product-category.dto.go b/internal/modules/master/product-categories/dto/product-category.dto.go index 6750c7c7..fc7f3c6f 100644 --- a/internal/modules/master/product-categories/dto/product-category.dto.go +++ b/internal/modules/master/product-categories/dto/product-category.dto.go @@ -9,17 +9,17 @@ import ( // === DTO Structs === -type ProductCategoryBaseDTO struct { +type ProductCategoryRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` Code string `json:"code"` } type ProductCategoryListDTO struct { - ProductCategoryBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ProductCategoryRelationDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type ProductCategoryDetailDTO struct { @@ -28,8 +28,8 @@ type ProductCategoryDetailDTO struct { // === Mapper Functions === -func ToProductCategoryBaseDTO(e entity.ProductCategory) ProductCategoryBaseDTO { - return ProductCategoryBaseDTO{ +func ToProductCategoryRelationDTO(e entity.ProductCategory) ProductCategoryRelationDTO { + return ProductCategoryRelationDTO{ Id: e.Id, Name: e.Name, Code: e.Code, @@ -37,17 +37,17 @@ func ToProductCategoryBaseDTO(e entity.ProductCategory) ProductCategoryBaseDTO { } func ToProductCategoryListDTO(e entity.ProductCategory) ProductCategoryListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } return ProductCategoryListDTO{ - ProductCategoryBaseDTO: ToProductCategoryBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + ProductCategoryRelationDTO: ToProductCategoryRelationDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } diff --git a/internal/modules/master/products/dto/product.dto.go b/internal/modules/master/products/dto/product.dto.go index 60696fc7..f676f994 100644 --- a/internal/modules/master/products/dto/product.dto.go +++ b/internal/modules/master/products/dto/product.dto.go @@ -5,55 +5,57 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" - supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs === -type ProductBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"` - Flags []string `json:"flags"` +type ProductRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + ProductPrice float64 `gorm:"type:numeric(15,3);not null"` + SellingPrice *float64 `gorm:"type:numeric(15,3)"` + Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` + Flags []string `json:"flags"` } type ProductListDTO struct { - ProductBaseDTO - Brand string `json:"brand"` - Sku *string `json:"sku,omitempty"` - ProductPrice float64 `json:"product_price"` - SellingPrice *float64 `json:"selling_price,omitempty"` - Tax *float64 `json:"tax,omitempty"` - ExpiryPeriod *int `json:"expiry_period,omitempty"` - ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"` - Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + Brand string `json:"brand"` + Sku *string `json:"sku,omitempty"` + ProductPrice float64 `json:"product_price"` + SellingPrice *float64 `json:"selling_price,omitempty"` + Tax *float64 `json:"tax,omitempty"` + ExpiryPeriod *int `json:"expiry_period,omitempty"` + Flags []string `json:"flags"` + Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` + ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type ProductDetailDTO struct { ProductListDTO - Flags []string `json:"flags"` } // === Mapper Functions === -func ToProductBaseDTO(e entity.Product) ProductBaseDTO { +func ToProductRelationDTO(e entity.Product) ProductRelationDTO { flags := make([]string, len(e.Flags)) for i, f := range e.Flags { flags[i] = f.Name } - var uomRef *uomDTO.UomBaseDTO + var uomRef *uomDTO.UomRelationDTO if e.Uom.Id != 0 { - mapped := uomDTO.ToUomBaseDTO(e.Uom) + mapped := uomDTO.ToUomRelationDTO(e.Uom) uomRef = &mapped } - return ProductBaseDTO{ + return ProductRelationDTO{ Id: e.Id, Name: e.Name, Flags: flags, @@ -62,36 +64,44 @@ func ToProductBaseDTO(e entity.Product) ProductBaseDTO { } func ToProductListDTO(e entity.Product) ProductListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } - var categoryRef *productCategoryDTO.ProductCategoryBaseDTO + var categoryRef *productCategoryDTO.ProductCategoryRelationDTO if e.ProductCategory.Id != 0 { - mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory) + mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory) categoryRef = &mapped } - suppliers := make([]supplierDTO.SupplierBaseDTO, len(e.Suppliers)) - for i, s := range e.Suppliers { - suppliers[i] = supplierDTO.ToSupplierBaseDTO(s) + flags := make([]string, len(e.Flags)) + for i, f := range e.Flags { + flags[i] = f.Name + } + + var uomRef *uomDTO.UomRelationDTO + if e.Uom.Id != 0 { + mapped := uomDTO.ToUomRelationDTO(e.Uom) + uomRef = &mapped } return ProductListDTO{ + Id: e.Id, + Name: e.Name, + Flags: flags, + Uom: uomRef, Brand: e.Brand, Sku: e.Sku, ProductPrice: e.ProductPrice, SellingPrice: e.SellingPrice, Tax: e.Tax, ExpiryPeriod: e.ExpiryPeriod, - ProductBaseDTO: ToProductBaseDTO(e), CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, ProductCategory: categoryRef, - Suppliers: suppliers, } } @@ -104,13 +114,7 @@ func ToProductListDTOs(e []entity.Product) []ProductListDTO { } func ToProductDetailDTO(e entity.Product) ProductDetailDTO { - flags := make([]string, len(e.Flags)) - for i, f := range e.Flags { - flags[i] = f.Name - } - return ProductDetailDTO{ ProductListDTO: ToProductListDTO(e), - Flags: flags, } } diff --git a/internal/modules/master/products/repositories/product.repository.go b/internal/modules/master/products/repositories/product.repository.go index d1cc5cb2..244259d5 100644 --- a/internal/modules/master/products/repositories/product.repository.go +++ b/internal/modules/master/products/repositories/product.repository.go @@ -102,13 +102,13 @@ func (r *ProductRepositoryImpl) IsLinkedToSupplier(ctx context.Context, productI return count > 0, nil } -func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error { +func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIds []uint) error { db := tx if db == nil { db = r.DB() } - if supplierIDs == nil { + if supplierIds == nil { return db.WithContext(ctx). Where("product_id = ?", productID). Delete(&entity.ProductSupplier{}). @@ -125,25 +125,25 @@ func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm. existingMap := make(map[uint]struct{}, len(existing)) for _, rel := range existing { - existingMap[rel.SupplierID] = struct{}{} + existingMap[rel.SupplierId] = struct{}{} } - incomingMap := make(map[uint]struct{}, len(supplierIDs)) - for _, id := range supplierIDs { + incomingMap := make(map[uint]struct{}, len(supplierIds)) + for _, id := range supplierIds { incomingMap[id] = struct{}{} if _, exists := existingMap[id]; exists { continue } - record := entity.ProductSupplier{ProductID: productID, SupplierID: id} + record := entity.ProductSupplier{ProductId: productID, SupplierId: id} if err := db.WithContext(ctx).Create(&record).Error; err != nil { return err } } for _, rel := range existing { - if _, keep := incomingMap[rel.SupplierID]; !keep { + if _, keep := incomingMap[rel.SupplierId]; !keep { if err := db.WithContext(ctx). - Where("product_id = ? AND supplier_id = ?", productID, rel.SupplierID). + Where("product_id = ? AND supplier_id = ?", productID, rel.SupplierId). Delete(&entity.ProductSupplier{}). Error; err != nil { return err diff --git a/internal/modules/master/products/services/product.service.go b/internal/modules/master/products/services/product.service.go index fb1fe00f..35e24927 100644 --- a/internal/modules/master/products/services/product.service.go +++ b/internal/modules/master/products/services/product.service.go @@ -55,7 +55,8 @@ func (s productService) withRelations(db *gorm.DB) *gorm.DB { Preload("Uom"). Preload("ProductCategory"). Preload("Flags"). - Preload("Suppliers", func(db *gorm.DB) *gorm.DB { + Preload("ProductSuppliers"). + Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB { return db.Order("suppliers.name ASC") }) } diff --git a/internal/modules/master/suppliers/controllers/supplier.controller.go b/internal/modules/master/suppliers/controllers/supplier.controller.go index 5d70e43e..c427316d 100644 --- a/internal/modules/master/suppliers/controllers/supplier.controller.go +++ b/internal/modules/master/suppliers/controllers/supplier.controller.go @@ -24,9 +24,10 @@ func NewSupplierController(supplierService service.SupplierService) *SupplierCon func (u *SupplierController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + Category: c.Query("category", ""), } if query.Page < 1 || query.Limit < 1 { @@ -71,7 +72,7 @@ func (u *SupplierController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get supplier successfully", - Data: dto.ToSupplierListDTO(*result), + Data: dto.ToSupplierDetailDTO(*result), }) } diff --git a/internal/modules/master/suppliers/dto/supplier.dto.go b/internal/modules/master/suppliers/dto/supplier.dto.go index 7e0df680..c8302b0b 100644 --- a/internal/modules/master/suppliers/dto/supplier.dto.go +++ b/internal/modules/master/suppliers/dto/supplier.dto.go @@ -9,7 +9,7 @@ import ( // === DTO Structs === -type SupplierBaseDTO struct { +type SupplierRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` Alias string `json:"alias"` @@ -17,30 +17,32 @@ type SupplierBaseDTO struct { } type SupplierListDTO struct { - SupplierBaseDTO - Pic string `json:"pic"` - Type string `json:"type"` - Hatchery *string `json:"hatchery,omitempty"` - Phone string `json:"phone"` - Email string `json:"email"` - Address string `json:"address"` - Npwp *string `json:"npwp,omitempty"` - AccountNumber *string `json:"account_number,omitempty"` - Balance float64 `json:"balance"` - DueDate int `json:"due_date"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + SupplierRelationDTO + Pic string `json:"pic"` + Type string `json:"type"` + Hatchery *string `json:"hatchery,omitempty"` + Phone string `json:"phone"` + Email string `json:"email"` + Address string `json:"address"` + Npwp *string `json:"npwp,omitempty"` + AccountNumber *string `json:"account_number,omitempty"` + Balance float64 `json:"balance"` + DueDate int `json:"due_date"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type SupplierDetailDTO struct { SupplierListDTO + Products []SupplierProductDTO `json:"products"` + Nonstocks []SupplierNonstockDTO `json:"nonstocks"` } // === Mapper Functions === -func ToSupplierBaseDTO(e entity.Supplier) SupplierBaseDTO { - return SupplierBaseDTO{ +func ToSupplierRelationDTO(e entity.Supplier) SupplierRelationDTO { + return SupplierRelationDTO{ Id: e.Id, Name: e.Name, Alias: e.Alias, @@ -49,27 +51,27 @@ func ToSupplierBaseDTO(e entity.Supplier) SupplierBaseDTO { } func ToSupplierListDTO(e entity.Supplier) SupplierListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } return SupplierListDTO{ - Pic: e.Pic, - Type: e.Type, - Hatchery: e.Hatchery, - Phone: e.Phone, - Email: e.Email, - Address: e.Address, - Npwp: e.Npwp, - AccountNumber: e.AccountNumber, - Balance: e.Balance, - DueDate: e.DueDate, - SupplierBaseDTO: ToSupplierBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + Pic: e.Pic, + Type: e.Type, + Hatchery: e.Hatchery, + Phone: e.Phone, + Email: e.Email, + Address: e.Address, + Npwp: e.Npwp, + AccountNumber: e.AccountNumber, + Balance: e.Balance, + DueDate: e.DueDate, + SupplierRelationDTO: ToSupplierRelationDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } @@ -84,5 +86,7 @@ func ToSupplierListDTOs(e []entity.Supplier) []SupplierListDTO { func ToSupplierDetailDTO(e entity.Supplier) SupplierDetailDTO { return SupplierDetailDTO{ SupplierListDTO: ToSupplierListDTO(e), + Products: toSupplierProductDTOs(e.ProductSuppliers), + Nonstocks: toSupplierNonstockDTOs(e.NonstockSuppliers), } } diff --git a/internal/modules/master/suppliers/dto/supplier_nonstock.dto.go b/internal/modules/master/suppliers/dto/supplier_nonstock.dto.go new file mode 100644 index 00000000..828063eb --- /dev/null +++ b/internal/modules/master/suppliers/dto/supplier_nonstock.dto.go @@ -0,0 +1,50 @@ +package dto + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" +) + +// === DTO Structs === + +type SupplierNonstockDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` + Flags []string `json:"flags"` +} + +// === Mapper Functions === + +func toSupplierNonstockDTOs(relations []entity.NonstockSupplier) []SupplierNonstockDTO { + if len(relations) == 0 { + return nil + } + + result := make([]SupplierNonstockDTO, 0, len(relations)) + for _, relation := range relations { + Nonstock := relation.Nonstock + if Nonstock.Id == 0 { + continue + } + + flags := make([]string, len(Nonstock.Flags)) + for i, f := range Nonstock.Flags { + flags[i] = f.Name + } + + var uomRef *uomDTO.UomRelationDTO + if Nonstock.Uom.Id != 0 { + mapped := uomDTO.ToUomRelationDTO(Nonstock.Uom) + uomRef = &mapped + } + + result = append(result, SupplierNonstockDTO{ + Id: Nonstock.Id, + Name: Nonstock.Name, + Uom: uomRef, + Flags: flags, + }) + } + return result +} diff --git a/internal/modules/master/suppliers/dto/supplier_product.dto.go b/internal/modules/master/suppliers/dto/supplier_product.dto.go new file mode 100644 index 00000000..47a6ae0e --- /dev/null +++ b/internal/modules/master/suppliers/dto/supplier_product.dto.go @@ -0,0 +1,54 @@ +package dto + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" +) + +// === DTO Structs === + +type SupplierProductDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + ProductPrice float64 `gorm:"type:numeric(15,3);not null"` + SellingPrice *float64 `gorm:"type:numeric(15,3)"` + Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` + Flags []string `json:"flags"` +} + +// === Mapper Functions === + +func toSupplierProductDTOs(relations []entity.ProductSupplier) []SupplierProductDTO { + if len(relations) == 0 { + return nil + } + + result := make([]SupplierProductDTO, 0, len(relations)) + for _, relation := range relations { + product := relation.Product + if product.Id == 0 { + continue + } + + flags := make([]string, len(product.Flags)) + for i, f := range product.Flags { + flags[i] = f.Name + } + + var uomRef *uomDTO.UomRelationDTO + if product.Uom.Id != 0 { + mapped := uomDTO.ToUomRelationDTO(product.Uom) + uomRef = &mapped + } + + result = append(result, SupplierProductDTO{ + Id: product.Id, + Name: product.Name, + ProductPrice: product.ProductPrice, + SellingPrice: product.SellingPrice, + Uom: uomRef, + Flags: flags, + }) + } + return result +} diff --git a/internal/modules/master/suppliers/route.go b/internal/modules/master/suppliers/route.go index 17271d4a..3a57f645 100644 --- a/internal/modules/master/suppliers/route.go +++ b/internal/modules/master/suppliers/route.go @@ -1,7 +1,7 @@ package suppliers import ( - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/controllers" supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,7 +13,7 @@ func SupplierRoutes(v1 fiber.Router, u user.UserService, s supplier.SupplierServ ctrl := controller.NewSupplierController(s) route := v1.Group("/suppliers") - route.Use(m.Auth(u)) + // route.Use(m.Auth(u)) route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) diff --git a/internal/modules/master/suppliers/services/supplier.service.go b/internal/modules/master/suppliers/services/supplier.service.go index 99e15b29..30ff4b9b 100644 --- a/internal/modules/master/suppliers/services/supplier.service.go +++ b/internal/modules/master/suppliers/services/supplier.service.go @@ -39,7 +39,12 @@ func NewSupplierService(repo repository.SupplierRepository, validate *validator. } func (s supplierService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") + return db. + Preload("CreatedUser"). + Preload("ProductSuppliers.Product.Uom"). + Preload("ProductSuppliers.Product.Flags"). + Preload("NonstockSuppliers.Nonstock.Uom"). + Preload("NonstockSuppliers.Nonstock.Flags") } func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Supplier, int64, error) { @@ -47,6 +52,14 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return nil, 0, err } + if params.Category != "" { + category := strings.ToUpper(params.Category) + if category != "BOP" && category != "SAPRONAK" { + return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier category") + } + params.Category = category + } + offset := (params.Page - 1) * params.Limit suppliers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { @@ -54,6 +67,11 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit if params.Search != "" { return db.Where("name LIKE ?", "%"+params.Search+"%") } + + if params.Category != "" { + db = db.Where("category LIKE ?", "%"+params.Category+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") }) diff --git a/internal/modules/master/suppliers/validations/supplier.validation.go b/internal/modules/master/suppliers/validations/supplier.validation.go index 7eeff716..fa1d135d 100644 --- a/internal/modules/master/suppliers/validations/supplier.validation.go +++ b/internal/modules/master/suppliers/validations/supplier.validation.go @@ -31,7 +31,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"` - Search string `query:"search" validate:"omitempty,max=50"` + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + Search string `query:"search" validate:"omitempty,max=50"` + Category string `query:"category" validate:"omitempty,max=50"` } diff --git a/internal/modules/master/uoms/dto/uom.dto.go b/internal/modules/master/uoms/dto/uom.dto.go index 2e614de0..cc32a702 100644 --- a/internal/modules/master/uoms/dto/uom.dto.go +++ b/internal/modules/master/uoms/dto/uom.dto.go @@ -9,17 +9,17 @@ import ( // === DTO Structs === -type UomBaseDTO struct { +type UomRelationDTO struct { Id uint `json:"id"` Name string `json:"name"` } type UomListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type UomDetailDTO struct { @@ -28,17 +28,17 @@ type UomDetailDTO struct { // === Mapper Functions === -func ToUomBaseDTO(e entity.Uom) UomBaseDTO { - return UomBaseDTO{ +func ToUomRelationDTO(e entity.Uom) UomRelationDTO { + return UomRelationDTO{ Id: e.Id, Name: e.Name, } } func ToUomListDTO(e entity.Uom) UomListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } diff --git a/internal/modules/master/warehouses/dto/warehouse.dto.go b/internal/modules/master/warehouses/dto/warehouse.dto.go index f3c3715b..e80c4f9f 100644 --- a/internal/modules/master/warehouses/dto/warehouse.dto.go +++ b/internal/modules/master/warehouses/dto/warehouse.dto.go @@ -12,25 +12,25 @@ import ( // === DTO Structs === -type WarehouseBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` - Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` - Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` +type WarehouseRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` + Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` + Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` } type WarehouseListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Area *areaDTO.AreaBaseDTO `json:"area"` - Location *locationDTO.LocationBaseDTO `json:"location"` - Kandang *kandangDTO.KandangBaseDTO `json:"kandang"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Area *areaDTO.AreaRelationDTO `json:"area"` + Location *locationDTO.LocationRelationDTO `json:"location"` + Kandang *kandangDTO.KandangRelationDTO `json:"kandang"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type WarehouseDetailDTO struct { @@ -39,26 +39,26 @@ type WarehouseDetailDTO struct { // === Mapper Functions === -func ToWarehouseBaseDTO(e entity.Warehouse) WarehouseBaseDTO { - var area *areaDTO.AreaBaseDTO +func ToWarehouseRelationDTO(e entity.Warehouse) WarehouseRelationDTO { + var area *areaDTO.AreaRelationDTO if e.Area.Id != 0 { - mapped := areaDTO.ToAreaBaseDTO(e.Area) + mapped := areaDTO.ToAreaRelationDTO(e.Area) area = &mapped } - var location *locationDTO.LocationBaseDTO + var location *locationDTO.LocationRelationDTO if e.Location != nil && e.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(*e.Location) + mapped := locationDTO.ToLocationRelationDTO(*e.Location) location = &mapped } - var kandang *kandangDTO.KandangBaseDTO + var kandang *kandangDTO.KandangRelationDTO if e.Kandang != nil && e.Kandang.Id != 0 { - mapped := kandangDTO.ToKandangBaseDTO(*e.Kandang) + mapped := kandangDTO.ToKandangRelationDTO(*e.Kandang) kandang = &mapped } - return WarehouseBaseDTO{ + return WarehouseRelationDTO{ Id: e.Id, Name: e.Name, Type: e.Type, @@ -69,27 +69,27 @@ func ToWarehouseBaseDTO(e entity.Warehouse) WarehouseBaseDTO { } func ToWarehouseListDTO(e entity.Warehouse) WarehouseListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } - var area *areaDTO.AreaBaseDTO + var area *areaDTO.AreaRelationDTO if e.Area.Id != 0 { - mapped := areaDTO.ToAreaBaseDTO(e.Area) + mapped := areaDTO.ToAreaRelationDTO(e.Area) area = &mapped } - var location *locationDTO.LocationBaseDTO + var location *locationDTO.LocationRelationDTO if e.Location != nil && e.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(*e.Location) + mapped := locationDTO.ToLocationRelationDTO(*e.Location) location = &mapped } - var kandang *kandangDTO.KandangBaseDTO + var kandang *kandangDTO.KandangRelationDTO if e.Kandang != nil && e.Kandang.Id != 0 { - mapped := kandangDTO.ToKandangBaseDTO(*e.Kandang) + mapped := kandangDTO.ToKandangRelationDTO(*e.Kandang) kandang = &mapped } diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 4a0e1bbb..d53b9491 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -4,26 +4,26 @@ 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" + areaRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" + fcrRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" + flockRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" + kandangRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" + locationRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" - userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + userRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs (ordered) === type ProductWarehouseDTO struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + Id uint `json:"id"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` } -type ChickinBaseDTO struct { +type ChickinRelationDTO struct { Id uint `json:"id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ChickInDate time.Time `json:"chick_in_date"` @@ -35,19 +35,19 @@ type ChickinBaseDTO struct { } 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"` + Id uint `json:"id"` + Period int `json:"period"` + Category string `json:"category"` + Flock *flockRelationDTO.FlockRelationDTO `json:"flock"` + Area *areaRelationDTO.AreaRelationDTO `json:"area"` + Fcr *fcrRelationDTO.FcrRelationDTO `json:"fcr"` + Location *locationRelationDTO.LocationRelationDTO `json:"location"` } type ProjectFlockKandangDTO struct { - Id uint `json:"id"` - ProjectFlock *ProjectFlockDTO `json:"project_flock"` - Kandang *kandangBaseDTO.KandangBaseDTO `json:"kandang"` + Id uint `json:"id"` + ProjectFlock *ProjectFlockDTO `json:"project_flock"` + Kandang *kandangRelationDTO.KandangRelationDTO `json:"kandang"` } // gunakan base DTO dari package users @@ -64,71 +64,71 @@ type ChickinSimpleDTO struct { } type ChickinListDTO struct { - ChickinBaseDTO - CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ChickinRelationDTO + CreatedUser *userRelationDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type ChickinDetailDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ChickInDate time.Time `json:"chick_in_date"` - ProductWarehouseId uint `json:"product_warehouse_id"` - UsageQty float64 `json:"usage_qty"` - PendingUsageQty float64 `json:"pending_usage_qty"` - Notes string `json:"notes"` - CreatedBy uint `json:"created_by"` - CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` + CreatedBy uint `json:"created_by"` + CreatedUser *userRelationDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // === Mapper Functions (ordered) === -func ToFlockDTO(e entity.Flock) flockBaseDTO.FlockBaseDTO { - return flockBaseDTO.ToFlockBaseDTO(e) +func ToFlockDTO(e entity.Flock) flockRelationDTO.FlockRelationDTO { + return flockRelationDTO.ToFlockRelationDTO(e) } -func ToKandangDTO(e entity.Kandang) kandangBaseDTO.KandangBaseDTO { - return kandangBaseDTO.ToKandangBaseDTO(e) +func ToKandangDTO(e entity.Kandang) kandangRelationDTO.KandangRelationDTO { + return kandangRelationDTO.ToKandangRelationDTO(e) } -func ToAreaDTO(e entity.Area) areaBaseDTO.AreaBaseDTO { - return areaBaseDTO.ToAreaBaseDTO(e) +func ToAreaDTO(e entity.Area) areaRelationDTO.AreaRelationDTO { + return areaRelationDTO.ToAreaRelationDTO(e) } -func ToFcrDTO(e entity.Fcr) fcrBaseDTO.FcrBaseDTO { - return fcrBaseDTO.ToFcrBaseDTO(e) +func ToFcrDTO(e entity.Fcr) fcrRelationDTO.FcrRelationDTO { + return fcrRelationDTO.ToFcrRelationDTO(e) } -func ToLocationDTO(e entity.Location) locationBaseDTO.LocationBaseDTO { - return locationBaseDTO.ToLocationBaseDTO(e) +func ToLocationDTO(e entity.Location) locationRelationDTO.LocationRelationDTO { + return locationRelationDTO.ToLocationRelationDTO(e) } -func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO { - return userBaseDTO.ToUserBaseDTO(e) +func ToUserRelationDTO(e entity.User) userRelationDTO.UserRelationDTO { + return userRelationDTO.ToUserRelationDTO(e) } func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO { e := pfk.ProjectFlock - var flock *flockBaseDTO.FlockBaseDTO + var flock *flockRelationDTO.FlockRelationDTO if base := pfutils.DeriveBaseName(e.FlockName); base != "" { - summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base} + summary := flockRelationDTO.FlockRelationDTO{Id: 0, Name: base} flock = &summary } - var area *areaBaseDTO.AreaBaseDTO + var area *areaRelationDTO.AreaRelationDTO if e.Area.Id != 0 { - mapped := areaBaseDTO.ToAreaBaseDTO(e.Area) + mapped := areaRelationDTO.ToAreaRelationDTO(e.Area) area = &mapped } - var fcr *fcrBaseDTO.FcrBaseDTO + var fcr *fcrRelationDTO.FcrRelationDTO if e.Fcr.Id != 0 { - mapped := fcrBaseDTO.ToFcrBaseDTO(e.Fcr) + mapped := fcrRelationDTO.ToFcrRelationDTO(e.Fcr) fcr = &mapped } - var location *locationBaseDTO.LocationBaseDTO + var location *locationRelationDTO.LocationRelationDTO if e.Location.Id != 0 { - mapped := locationBaseDTO.ToLocationBaseDTO(e.Location) + mapped := locationRelationDTO.ToLocationRelationDTO(e.Location) location = &mapped } return ProjectFlockDTO{ @@ -148,9 +148,9 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD mapped := ToProjectFlockDTO(e) pf = &mapped } - var kandang *kandangBaseDTO.KandangBaseDTO + var kandang *kandangRelationDTO.KandangRelationDTO if e.Kandang.Id != 0 { - mapped := kandangBaseDTO.ToKandangBaseDTO(e.Kandang) + mapped := kandangRelationDTO.ToKandangRelationDTO(e.Kandang) kandang = &mapped } return ProjectFlockKandangDTO{ @@ -160,7 +160,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD } } -func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { +func ToChickinRelationDTO(e entity.ProjectChickin) ChickinRelationDTO { var projectFlockKandangId uint // Check if ProjectFlockKandang relation is loaded if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 { @@ -175,7 +175,7 @@ func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { productWarehouse = toProductWarehouseDTO(e.ProductWarehouse) } - return ChickinBaseDTO{ + return ChickinRelationDTO{ Id: e.Id, ProjectFlockKandangId: projectFlockKandangId, ChickInDate: e.ChickInDate, @@ -201,16 +201,16 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { } func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { - var createdUser *userBaseDTO.UserBaseDTO + var createdUser *userRelationDTO.UserRelationDTO if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser) + mapped := userRelationDTO.ToUserRelationDTO(*e.CreatedUser) createdUser = &mapped } return ChickinListDTO{ - ChickinBaseDTO: ToChickinBaseDTO(e), - CreatedUser: createdUser, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, + ChickinRelationDTO: ToChickinRelationDTO(e), + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, } } @@ -231,9 +231,9 @@ func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO { } func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO { - var createdUser *userBaseDTO.UserBaseDTO + var createdUser *userRelationDTO.UserRelationDTO if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser) + mapped := userRelationDTO.ToUserRelationDTO(*e.CreatedUser) createdUser = &mapped } @@ -268,8 +268,8 @@ func ToProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO { return nil } - product := productDTO.ToProductBaseDTO(pw.Product) - warehouse := warehouseDTO.ToWarehouseBaseDTO(pw.Warehouse) + product := productDTO.ToProductRelationDTO(pw.Product) + warehouse := warehouseDTO.ToWarehouseRelationDTO(pw.Warehouse) return &ProductWarehouseDTO{ Id: pw.Id, diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index d65cedfd..f3ef7271 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -48,7 +48,7 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { data := make([]dto.ProjectFlockKandangListDTO, 0) for _, result := range results { - var flock *flockDTO.FlockBaseDTO + var flock *flockDTO.FlockRelationDTO if flockMap != nil { flock = flockMap[result.ProjectFlock.Id] } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 1a076232..1cc0c1f5 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -18,24 +18,24 @@ import ( // === DTO Structs (ordered) === -type ProjectFlockKandangBaseDTO struct { +type ProjectFlockKandangRelationDTO struct { Id uint `json:"id"` KandangId uint `json:"kandang_id"` ProjectFlockId uint `json:"project_flock_id"` } type ProjectFlockDTO struct { - Id uint `json:"id"` - Name string `json:"flock_name,omitempty"` - Period int `json:"period"` - 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"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"flock_name,omitempty"` + Period int `json:"period"` + Flock *flockDTO.FlockRelationDTO `json:"flock,omitempty"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` + Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type KandangDTO struct { @@ -45,9 +45,9 @@ type KandangDTO struct { } type ProductWarehouseDTO struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + Id uint `json:"id"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` } type AvailableQtyDTO struct { @@ -56,24 +56,24 @@ type AvailableQtyDTO struct { } type ProjectFlockKandangListDTO struct { - ProjectFlockKandangBaseDTO - ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` - Kandang *KandangDTO `json:"kandang,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` + ProjectFlockKandangRelationDTO + ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` + Kandang *KandangDTO `json:"kandang,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` } type ProjectFlockKandangDetailDTO struct { ProjectFlockKandangListDTO - Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"` - AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` + Chickins []chickinDTO.ChickinRelationDTO `json:"chickins,omitempty"` + AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` } // === Mapper Functions (ordered) === -func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO { - return ProjectFlockKandangBaseDTO{ +func ToProjectFlockKandangRelationDTO(e entity.ProjectFlockKandang) ProjectFlockKandangRelationDTO { + return ProjectFlockKandangRelationDTO{ Id: e.Id, KandangId: e.KandangId, ProjectFlockId: e.ProjectFlockId, @@ -104,20 +104,20 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang return ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e, availableQtyMap, productWarehouses, nil) } -func ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse, flock *flockDTO.FlockBaseDTO) ProjectFlockKandangDetailDTO { +func ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse, flock *flockDTO.FlockRelationDTO) ProjectFlockKandangDetailDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { - mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock, flock) + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) projectFlockSummary = &mapped } listDTO := ProjectFlockKandangListDTO{ - ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), - ProjectFlock: toProjectFlockDTO(projectFlockSummary), - Kandang: toKandangDTO(e.Kandang), - CreatedAt: e.CreatedAt, - CreatedUser: toCreatedUserDTO(e.ProjectFlock), - Approval: toApprovalDTO(e), + ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e), + ProjectFlock: toProjectFlockDTO(projectFlockSummary), + Kandang: toKandangDTO(e.Kandang), + CreatedAt: e.CreatedAt, + CreatedUser: toCreatedUserDTO(e.ProjectFlock), + Approval: toApprovalDTO(e), } return ProjectFlockKandangDetailDTO{ @@ -139,18 +139,18 @@ func toKandangDTO(kandang entity.Kandang) *KandangDTO { } } -func toFlockDTO(flock *entity.Flock) *flockDTO.FlockBaseDTO { - if flock == nil || flock.Id == 0 { - return nil - } +// func toFlockDTO(flock *entity.Flock) *flockDTO.FlockRelationDTO { +// if flock == nil || flock.Id == 0 { +// return nil +// } - return &flockDTO.FlockBaseDTO{ - Id: flock.Id, - Name: flock.Name, - } -} +// return &flockDTO.FlockRelationDTO{ +// Id: flock.Id, +// Name: flock.Name, +// } +// } -func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { +func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalRelationDTO { if e.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) return &mapped @@ -162,7 +162,7 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand return ToProjectFlockKandangListDTOWithFlock(e, nil) } -func ToProjectFlockKandangListDTOWithFlock(e entity.ProjectFlockKandang, flock *flockDTO.FlockBaseDTO) ProjectFlockKandangListDTO { +func ToProjectFlockKandangListDTOWithFlock(e entity.ProjectFlockKandang, flock *flockDTO.FlockRelationDTO) ProjectFlockKandangListDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { mapped := projectFlockDTO.ToProjectFlockListDTOWithFlock(e.ProjectFlock, flock) @@ -170,12 +170,12 @@ func ToProjectFlockKandangListDTOWithFlock(e entity.ProjectFlockKandang, flock * } return ProjectFlockKandangListDTO{ - ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), - ProjectFlock: toProjectFlockDTO(projectFlockSummary), - Kandang: toKandangDTO(e.Kandang), - CreatedAt: e.CreatedAt, - CreatedUser: toCreatedUserDTO(e.ProjectFlock), - Approval: toApprovalDTO(e), + ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e), + ProjectFlock: toProjectFlockDTO(projectFlockSummary), + Kandang: toKandangDTO(e.Kandang), + CreatedAt: e.CreatedAt, + CreatedUser: toCreatedUserDTO(e.ProjectFlock), + Approval: toApprovalDTO(e), } } @@ -187,12 +187,12 @@ func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFloc return result } -func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO { +func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserRelationDTO { if pf.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(pf.CreatedUser) + mapped := userDTO.ToUserRelationDTO(pf.CreatedUser) return &mapped } else if pf.CreatedBy != 0 { - return &userDTO.UserBaseDTO{ + return &userDTO.UserRelationDTO{ Id: pf.CreatedBy, IdUser: int64(pf.CreatedBy), } @@ -200,14 +200,14 @@ func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO { return nil } -func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { +func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinRelationDTO { if len(chickins) == 0 { return nil } - result := make([]chickinDTO.ChickinBaseDTO, len(chickins)) + result := make([]chickinDTO.ChickinRelationDTO, len(chickins)) for i, ch := range chickins { - result[i] = chickinDTO.ToChickinBaseDTO(ch) + result[i] = chickinDTO.ToChickinRelationDTO(ch) } return result } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index ec61a7e4..6e28a245 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -21,8 +21,8 @@ import ( ) type ProjectFlockKandangService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockBaseDTO, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockBaseDTO, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockRelationDTO, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockRelationDTO, error) } // Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity @@ -51,7 +51,7 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository } } -func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockBaseDTO, error) { +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockRelationDTO, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, nil, err } @@ -85,7 +85,7 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } } - flockMap := make(map[uint]*flockDTO.FlockBaseDTO) + flockMap := make(map[uint]*flockDTO.FlockRelationDTO) for i := range projectFlockKandangs { if projectFlockKandangs[i].ProjectFlock.Id != 0 { baseName := pfutils.DeriveBaseName(projectFlockKandangs[i].ProjectFlock.FlockName) @@ -95,7 +95,7 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) } else if flock != nil { - flockMap[projectFlockKandangs[i].ProjectFlock.Id] = &flockDTO.FlockBaseDTO{ + flockMap[projectFlockKandangs[i].ProjectFlock.Id] = &flockDTO.FlockRelationDTO{ Id: flock.Id, Name: flock.Name, } @@ -107,7 +107,7 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer return projectFlockKandangs, total, flockMap, nil } -func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockBaseDTO, error) { +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockRelationDTO, error) { projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") @@ -147,7 +147,7 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje productWarehouses = []entity.ProductWarehouse{} } } - var flockResult *flockDTO.FlockBaseDTO + var flockResult *flockDTO.FlockRelationDTO if projectFlockKandang.ProjectFlock.Id != 0 { baseName := pfutils.DeriveBaseName(projectFlockKandang.ProjectFlock.FlockName) if baseName != "" { @@ -155,7 +155,7 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) } else if flock != nil { - flockResult = &flockDTO.FlockBaseDTO{ + flockResult = &flockDTO.FlockRelationDTO{ Id: flock.Id, Name: flock.Name, } diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index 13b46cfd..5d51b985 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -7,7 +7,6 @@ import ( "strconv" "strings" - flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" @@ -85,7 +84,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { query.KandangIds = ids } - result, totalResults, flockMap, err := u.ProjectflockService.GetAll(c, query) + result, totalResults, _, err := u.ProjectflockService.GetAll(c, query) if err != nil { return err } @@ -124,7 +123,7 @@ func (u *ProjectflockController) GetOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, flock, err := u.ProjectflockService.GetOne(c, uint(id)) + result, _, err := u.ProjectflockService.GetOne(c, uint(id)) if err != nil { return err } @@ -162,7 +161,7 @@ func (u *ProjectflockController) CreateOne(c *fiber.Ctx) error { Code: fiber.StatusCreated, Status: "success", Message: "Create projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result, nil), + Data: dto.ToProjectFlockListDTO(*result), }) } @@ -189,7 +188,7 @@ func (u *ProjectflockController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result, nil), + Data: dto.ToProjectFlockListDTO(*result), }) } diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index 24b673d6..f2bc95df 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -17,28 +17,28 @@ import ( approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" ) -type ProjectFlockBaseDTO struct { +type ProjectFlockRelationDTO struct { Id uint `json:"id"` Period int `json:"period"` FlockName string `json:"flock_name"` } type ProjectFlockListDTO struct { - ProjectFlockBaseDTO - 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 []KandangWithProjectFlockIdDTO `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"` + ProjectFlockRelationDTO + Flock *flockDTO.FlockRelationDTO `json:"flock,omitempty"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` + Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` + Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval approvalDTO.ApprovalRelationDTO `json:"approval"` } type KandangWithProjectFlockIdDTO struct { - kandangDTO.KandangBaseDTO + kandangDTO.KandangRelationDTO ProjectFlockKandangId uint `json:"project_flock_kandang_id"` } @@ -47,8 +47,8 @@ type ProjectFlockDetailDTO struct { } type FlockPeriodDTO struct { - Flock flockDTO.FlockBaseDTO `json:"flock"` - NextPeriod int `json:"next_period"` + Flock flockDTO.FlockRelationDTO `json:"flock"` + NextPeriod int `json:"next_period"` } type KandangPeriodSummaryDTO struct { @@ -58,9 +58,9 @@ type KandangPeriodSummaryDTO struct { } func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectFlockListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } @@ -77,34 +77,34 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF } } kandangSummaries[i] = KandangWithProjectFlockIdDTO{ - KandangBaseDTO: kandangDTO.ToKandangBaseDTO(kandang), + KandangRelationDTO: kandangDTO.ToKandangRelationDTO(kandang), ProjectFlockKandangId: pfkId, } } } - var areaSummary *areaDTO.AreaBaseDTO + var areaSummary *areaDTO.AreaRelationDTO if e.Area.Id != 0 { - mapped := areaDTO.ToAreaBaseDTO(e.Area) + mapped := areaDTO.ToAreaRelationDTO(e.Area) areaSummary = &mapped } - var fcrSummary *fcrDTO.FcrBaseDTO + var fcrSummary *fcrDTO.FcrRelationDTO if e.Fcr.Id != 0 { - mapped := fcrDTO.ToFcrBaseDTO(e.Fcr) + mapped := fcrDTO.ToFcrRelationDTO(e.Fcr) fcrSummary = &mapped } - var locationSummary *locationDTO.LocationBaseDTO + var locationSummary *locationDTO.LocationRelationDTO if e.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(e.Location) + mapped := locationDTO.ToLocationRelationDTO(e.Location) locationSummary = &mapped } - var flockSummary *flockDTO.FlockBaseDTO - if flock != nil && flock.Id != 0 { - flockSummary = flock - } + // var flockSummary *flockDTO.FlockRelationDTO + // if flock != nil && flock.Id != 0 { + // flockSummary = flock + // } latestApproval := defaultProjectFlockLatestApproval(e) if e.LatestApproval != nil { @@ -113,7 +113,7 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF } return ProjectFlockListDTO{ - ProjectFlockBaseDTO: createProjectFlockBaseDTO(e, period), + ProjectFlockRelationDTO: createProjectFlockRelationDTO(e, period), // Flock: flockSummary, Area: areaSummary, Kandangs: kandangSummaries, @@ -127,8 +127,8 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF } } -func ToProjectFlockListDTOWithFlock(e entity.ProjectFlock, flock *flockDTO.FlockBaseDTO) ProjectFlockListDTO { - return ToProjectFlockListDTO(e, flock) +func ToProjectFlockListDTOWithFlock(e entity.ProjectFlock, flock *flockDTO.FlockRelationDTO) ProjectFlockListDTO { + return ToProjectFlockListDTO(e) } func ToProjectFlockListDTOs(items []entity.ProjectFlock) []ProjectFlockListDTO { @@ -160,10 +160,10 @@ func ToProjectFlockListDTOsWithPeriods(items []entity.ProjectFlock, periods map[ func ToProjectFlockListDTOsWithFlocks(items []entity.ProjectFlock, flocks map[uint]*entity.Flock) []ProjectFlockListDTO { result := make([]ProjectFlockListDTO, len(items)) for i, item := range items { - var flock *flockDTO.FlockBaseDTO + var flock *flockDTO.FlockRelationDTO if flocks != nil { if f := flocks[item.Id]; f != nil { - flock = &flockDTO.FlockBaseDTO{ + flock = &flockDTO.FlockRelationDTO{ Id: f.Id, Name: f.Name, } @@ -174,14 +174,14 @@ func ToProjectFlockListDTOsWithFlocks(items []entity.ProjectFlock, flocks map[ui return result } -func ToProjectFlockDetailDTO(e entity.ProjectFlock, flock *flockDTO.FlockBaseDTO) ProjectFlockDetailDTO { +func ToProjectFlockDetailDTO(e entity.ProjectFlock, flock *flockDTO.FlockRelationDTO) ProjectFlockDetailDTO { return ProjectFlockDetailDTO{ ProjectFlockListDTO: ToProjectFlockListDTOWithPeriod(e, 0), } } -func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.ApprovalBaseDTO { - result := approvalDTO.ApprovalBaseDTO{} +func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.ApprovalRelationDTO { + result := approvalDTO.ApprovalRelationDTO{} step := utils.ProjectFlockStepPengajuan if step > 0 { @@ -194,9 +194,9 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv } if e.CreatedUser.Id != 0 { - result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) + result.ActionBy = userDTO.ToUserRelationDTO(e.CreatedUser) } else if e.CreatedBy != 0 { - result.ActionBy = userDTO.UserBaseDTO{ + result.ActionBy = userDTO.UserRelationDTO{ Id: e.CreatedBy, IdUser: int64(e.CreatedBy), } @@ -205,16 +205,16 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv return result } -func createProjectFlockBaseDTO(e entity.ProjectFlock, period int) ProjectFlockBaseDTO { - return ProjectFlockBaseDTO{ +func createProjectFlockRelationDTO(e entity.ProjectFlock, period int) ProjectFlockRelationDTO { + return ProjectFlockRelationDTO{ Id: e.Id, Period: period, FlockName: e.FlockName, } } -func ToFlockSummaryDTO(e entity.Flock) flockDTO.FlockBaseDTO { - return flockDTO.FlockBaseDTO{ +func ToFlockSummaryDTO(e entity.Flock) flockDTO.FlockRelationDTO { + return flockDTO.FlockRelationDTO{ Id: e.Id, Name: e.Name, } diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 2e25bf09..416c7821 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -12,35 +12,35 @@ import ( ) type KandangWithPivotDTO struct { - kandangDTO.KandangBaseDTO + kandangDTO.KandangRelationDTO AvailableQuantity float64 `json:"available_quantity"` } type ProjectFlockWithPivotDTO struct { - ProjectFlockBaseDTO - 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 []KandangWithPivotDTO `json:"kandangs,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + ProjectFlockRelationDTO + Flock *flockDTO.FlockRelationDTO `json:"flock,omitempty"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` + Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` + Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` } type ProjectFlockKandangDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ProjectFlockId uint `json:"project_flock_id"` - KandangId uint `json:"kandang_id"` - Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` - ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` - AvailableQuantity float64 `json:"available_quantity"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ProjectFlockId uint `json:"project_flock_id"` + KandangId uint `json:"kandang_id"` + Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` + ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` + AvailableQuantity float64 `json:"available_quantity"` } func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { - var kandang *kandangDTO.KandangBaseDTO + var kandang *kandangDTO.KandangRelationDTO if e.Kandang.Id != 0 { - mapped := kandangDTO.ToKandangBaseDTO(e.Kandang) + mapped := kandangDTO.ToKandangRelationDTO(e.Kandang) kandang = &mapped } @@ -48,7 +48,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD if e.ProjectFlock.Id != 0 { pfLocal := ProjectFlockWithPivotDTO{ - ProjectFlockBaseDTO: ProjectFlockBaseDTO{ + ProjectFlockRelationDTO: ProjectFlockRelationDTO{ Id: e.ProjectFlock.Id, Period: e.Period, FlockName: e.ProjectFlock.FlockName, @@ -57,31 +57,31 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD } if base := pfutils.DeriveBaseName(e.ProjectFlock.FlockName); base != "" { - summary := flockDTO.FlockBaseDTO{Id: 0, Name: base} + summary := flockDTO.FlockRelationDTO{Id: 0, Name: base} pfLocal.Flock = &summary } if e.ProjectFlock.Area.Id != 0 { - mapped := areaDTO.ToAreaBaseDTO(e.ProjectFlock.Area) + mapped := areaDTO.ToAreaRelationDTO(e.ProjectFlock.Area) pfLocal.Area = &mapped } if e.ProjectFlock.Fcr.Id != 0 { - mapped := fcrDTO.ToFcrBaseDTO(e.ProjectFlock.Fcr) + mapped := fcrDTO.ToFcrRelationDTO(e.ProjectFlock.Fcr) pfLocal.Fcr = &mapped } if e.ProjectFlock.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(e.ProjectFlock.Location) + mapped := locationDTO.ToLocationRelationDTO(e.ProjectFlock.Location) pfLocal.Location = &mapped } if e.ProjectFlock.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.ProjectFlock.CreatedUser) pfLocal.CreatedUser = &mapped } for _, k := range e.ProjectFlock.Kandangs { - kb := kandangDTO.ToKandangBaseDTO(k) + kb := kandangDTO.ToKandangRelationDTO(k) pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{ - KandangBaseDTO: kb, - AvailableQuantity: 0, + KandangRelationDTO: kb, + AvailableQuantity: 0, }) } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index d7dc5ad8..f6fa2638 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -29,8 +29,8 @@ import ( ) type ProjectflockService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockBaseDTO, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockBaseDTO, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) @@ -96,7 +96,7 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { Preload("KandangHistory.Kandang") } -func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockBaseDTO, error) { +func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, nil, err } @@ -137,7 +137,7 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } } - flockMap := make(map[uint]*flockDTO.FlockBaseDTO) + flockMap := make(map[uint]*flockDTO.FlockRelationDTO) for i := range projectflocks { if projectflocks[i].FlockName != "" { baseName := pfutils.DeriveBaseName(projectflocks[i].FlockName) @@ -146,7 +146,7 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) } else if flock != nil { - flockMap[projectflocks[i].Id] = &flockDTO.FlockBaseDTO{ + flockMap[projectflocks[i].Id] = &flockDTO.FlockRelationDTO{ Id: flock.Id, Name: flock.Name, } @@ -187,7 +187,7 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr return projectflock, nil } -func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockBaseDTO, error) { +func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) { projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") @@ -214,7 +214,7 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock } // Fetch Flock master data for this ProjectFlock - var flockResult *flockDTO.FlockBaseDTO + var flockResult *flockDTO.FlockRelationDTO if projectflock.FlockName != "" { baseName := pfutils.DeriveBaseName(projectflock.FlockName) if baseName != "" { @@ -222,7 +222,7 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) } else if flock != nil { - flockResult = &flockDTO.FlockBaseDTO{ + flockResult = &flockDTO.FlockRelationDTO{ Id: flock.Id, Name: flock.Name, } diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index 21fccd41..f7cc4ee2 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -15,30 +15,30 @@ import ( // === DTO Structs === -type RecordingBaseDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - RecordDatetime time.Time `json:"record_datetime"` - Day int `json:"day"` - ProjectFlockCategory string `json:"project_flock_category"` - TotalDepletionQty float64 `json:"total_depletion_qty"` - CumDepletionRate float64 `json:"cum_depletion_rate"` - DailyGain float64 `json:"daily_gain"` - AvgDailyGain float64 `json:"avg_daily_gain"` - CumIntake int `json:"cum_intake"` - FcrValue float64 `json:"fcr_value"` - TotalChickQty float64 `json:"total_chick_qty"` - Approval approvalDTO.ApprovalBaseDTO `json:"approval"` - EggGradingStatus *string `json:"egg_grading_status"` - EggGradingPendingQty *int `json:"egg_grading_pending_qty"` - EggGradingCompletedQty *int `json:"egg_grading_completed_qty"` +type RecordingRelationDTO struct { + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + RecordDatetime time.Time `json:"record_datetime"` + Day int `json:"day"` + ProjectFlockCategory string `json:"project_flock_category"` + TotalDepletionQty float64 `json:"total_depletion_qty"` + CumDepletionRate float64 `json:"cum_depletion_rate"` + DailyGain float64 `json:"daily_gain"` + AvgDailyGain float64 `json:"avg_daily_gain"` + CumIntake int `json:"cum_intake"` + FcrValue float64 `json:"fcr_value"` + TotalChickQty float64 `json:"total_chick_qty"` + Approval approvalDTO.ApprovalRelationDTO `json:"approval"` + EggGradingStatus *string `json:"egg_grading_status"` + EggGradingPendingQty *int `json:"egg_grading_pending_qty"` + EggGradingCompletedQty *int `json:"egg_grading_completed_qty"` } type RecordingListDTO struct { - RecordingBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + RecordingRelationDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type RecordingDetailDTO struct { @@ -91,7 +91,7 @@ type RecordingEggGradingDTO struct { // === Mapper Functions === -func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { +func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { var ( projectFlockCategory string day int @@ -142,7 +142,7 @@ func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { gradingStatus, gradingPending, gradingCompleted := computeEggGradingStatus(e) - return RecordingBaseDTO{ + return RecordingRelationDTO{ Id: e.Id, ProjectFlockKandangId: e.ProjectFlockKandangId, RecordDatetime: e.RecordDatetime, @@ -163,17 +163,17 @@ func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { } func ToRecordingListDTO(e entity.Recording) RecordingListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(*e.CreatedUser) createdUser = &mapped } return RecordingListDTO{ - RecordingBaseDTO: ToRecordingBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + RecordingRelationDTO: ToRecordingRelationDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } @@ -344,8 +344,8 @@ func filterGoodEggs(eggs []entity.RecordingEgg) []entity.RecordingEgg { return result } -func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalBaseDTO { - result := approvalDTO.ApprovalBaseDTO{} +func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO { + result := approvalDTO.ApprovalRelationDTO{} step := utils.RecordingStepPengajuan result.StepNumber = uint16(step) @@ -356,9 +356,9 @@ func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalBase } if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - result.ActionBy = userDTO.ToUserBaseDTO(*e.CreatedUser) + result.ActionBy = userDTO.ToUserRelationDTO(*e.CreatedUser) } else if e.CreatedBy != 0 { - result.ActionBy = userDTO.UserBaseDTO{ + result.ActionBy = userDTO.UserRelationDTO{ Id: e.CreatedBy, IdUser: int64(e.CreatedBy), } diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index ef537e19..aeb12e5e 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -10,7 +10,7 @@ import ( // === DTO Structs === -type TransferLayingBaseDTO struct { +type TransferLayingRelationDTO struct { Id uint `json:"id"` TransferNumber string `json:"transfer_number"` TransferDate time.Time `json:"transfer_date"` @@ -64,22 +64,22 @@ type LayingTransferTargetDTO struct { } type TransferLayingListDTO struct { - TransferLayingBaseDTO - FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"` - ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"` - PendingUsageQty *float64 `json:"pending_usage_qty"` - UsageQty *float64 `json:"usage_qty"` - CreatedBy uint `json:"created_by"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` + TransferLayingRelationDTO + FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"` + ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"` + PendingUsageQty *float64 `json:"pending_usage_qty"` + UsageQty *float64 `json:"usage_qty"` + CreatedBy uint `json:"created_by"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` } type TransferLayingDetailDTO struct { TransferLayingListDTO - Sources []LayingTransferSourceDTO `json:"sources,omitempty"` - Targets []LayingTransferTargetDTO `json:"targets,omitempty"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` + Sources []LayingTransferSourceDTO `json:"sources,omitempty"` + Targets []LayingTransferTargetDTO `json:"targets,omitempty"` + Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` } // === Available Quantity DTOs === @@ -203,8 +203,8 @@ func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingT return result } -func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO { - return TransferLayingBaseDTO{ +func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO { + return TransferLayingRelationDTO{ Id: e.Id, TransferNumber: e.TransferNumber, TransferDate: e.TransferDate, @@ -213,26 +213,26 @@ func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO { } func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(*e.CreatedUser) createdUser = &mapped } return TransferLayingListDTO{ - TransferLayingBaseDTO: ToTransferLayingBaseDTO(e), - FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock), - ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock), - PendingUsageQty: e.PendingUsageQty, - UsageQty: e.UsageQty, - CreatedBy: e.CreatedBy, - CreatedUser: createdUser, - CreatedAt: e.CreatedAt, + TransferLayingRelationDTO: ToTransferLayingRelationDTO(e), + FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock), + ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock), + PendingUsageQty: e.PendingUsageQty, + UsageQty: e.UsageQty, + CreatedBy: e.CreatedBy, + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, } } func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO { - var latestApproval *approvalDTO.ApprovalBaseDTO + var latestApproval *approvalDTO.ApprovalRelationDTO if e.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) @@ -252,7 +252,7 @@ func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Appro } func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO { - var mappedApproval *approvalDTO.ApprovalBaseDTO + var mappedApproval *approvalDTO.ApprovalRelationDTO // Prefer LatestApproval from entity if e.LatestApproval != nil && e.LatestApproval.Id != 0 { diff --git a/internal/modules/purchases/dto/purchase.dto.go b/internal/modules/purchases/dto/purchase.dto.go index 5a2926cf..bbd59fdd 100644 --- a/internal/modules/purchases/dto/purchase.dto.go +++ b/internal/modules/purchases/dto/purchase.dto.go @@ -13,52 +13,52 @@ import ( ) type PurchaseListItemDTO struct { - Id uint64 `json:"id"` - PrNumber string `json:"pr_number"` - PoNumber *string `json:"po_number"` - Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"` - CreditTerm *int `json:"credit_term"` - DueDate *time.Time `json:"due_date"` - PoDate *time.Time `json:"po_date"` - GrandTotal float64 `json:"grand_total"` - Notes *string `json:"notes"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval"` + Id uint64 `json:"id"` + PrNumber string `json:"pr_number"` + PoNumber *string `json:"po_number"` + Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` + CreditTerm *int `json:"credit_term"` + DueDate *time.Time `json:"due_date"` + PoDate *time.Time `json:"po_date"` + GrandTotal float64 `json:"grand_total"` + Notes *string `json:"notes"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval *approvalDTO.ApprovalRelationDTO `json:"approval"` } type PurchaseDetailDTO struct { - Id uint64 `json:"id"` - PrNumber string `json:"pr_number"` - PoNumber *string `json:"po_number"` - Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"` - CreditTerm *int `json:"credit_term"` - DueDate *time.Time `json:"due_date"` - PoDate *time.Time `json:"po_date"` - GrandTotal float64 `json:"grand_total"` - Notes *string `json:"notes"` - Items []PurchaseItemDTO `json:"items"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval"` + Id uint64 `json:"id"` + PrNumber string `json:"pr_number"` + PoNumber *string `json:"po_number"` + Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` + CreditTerm *int `json:"credit_term"` + DueDate *time.Time `json:"due_date"` + PoDate *time.Time `json:"po_date"` + GrandTotal float64 `json:"grand_total"` + Notes *string `json:"notes"` + Items []PurchaseItemDTO `json:"items"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval *approvalDTO.ApprovalRelationDTO `json:"approval"` } type PurchaseItemDTO struct { - Id uint64 `json:"id"` - ProductID uint64 `json:"product_id"` - Product *productDTO.ProductBaseDTO `json:"product"` - WarehouseID uint64 `json:"warehouse_id"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse"` - ProductWarehouseID *uint64 `json:"product_warehouse_id"` - SubQty float64 `json:"sub_qty"` - TotalQty float64 `json:"total_qty"` - TotalUsed float64 `json:"total_used"` - Price float64 `json:"price"` - TotalPrice float64 `json:"total_price"` - ReceivedDate *time.Time `json:"received_date"` - TravelNumber *string `json:"travel_number"` - TravelDocumentPath *string `json:"travel_document_path"` - VehicleNumber *string `json:"vehicle_number"` + Id uint64 `json:"id"` + ProductID uint64 `json:"product_id"` + Product *productDTO.ProductRelationDTO `json:"product"` + WarehouseID uint64 `json:"warehouse_id"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse"` + ProductWarehouseID *uint64 `json:"product_warehouse_id"` + SubQty float64 `json:"sub_qty"` + TotalQty float64 `json:"total_qty"` + TotalUsed float64 `json:"total_used"` + Price float64 `json:"price"` + TotalPrice float64 `json:"total_price"` + ReceivedDate *time.Time `json:"received_date"` + TravelNumber *string `json:"travel_number"` + TravelDocumentPath *string `json:"travel_document_path"` + VehicleNumber *string `json:"vehicle_number"` } func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO { @@ -78,17 +78,17 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO { VehicleNumber: item.VehicleNumber, } if item.Product != nil && item.Product.Id != 0 { - summary := productDTO.ToProductBaseDTO(*item.Product) + summary := productDTO.ToProductRelationDTO(*item.Product) dto.Product = &summary } if item.Warehouse != nil && item.Warehouse.Id != 0 { - summary := warehouseDTO.ToWarehouseBaseDTO(*item.Warehouse) + summary := warehouseDTO.ToWarehouseRelationDTO(*item.Warehouse) if item.Warehouse.Area.Id != 0 { - areaSummary := areaDTO.ToAreaBaseDTO(item.Warehouse.Area) + areaSummary := areaDTO.ToAreaRelationDTO(item.Warehouse.Area) summary.Area = &areaSummary } if item.Warehouse.Location != nil && item.Warehouse.Location.Id != 0 { - locationSummary := locationDTO.ToLocationBaseDTO(*item.Warehouse.Location) + locationSummary := locationDTO.ToLocationRelationDTO(*item.Warehouse.Location) summary.Location = &locationSummary } dto.Warehouse = &summary @@ -145,11 +145,11 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListItemDTO { return dto } -func mapSupplier(s entity.Supplier) *supplierDTO.SupplierBaseDTO { +func mapSupplier(s entity.Supplier) *supplierDTO.SupplierRelationDTO { if s.Id == 0 { return nil } - summary := supplierDTO.ToSupplierBaseDTO(s) + summary := supplierDTO.ToSupplierRelationDTO(s) return &summary } @@ -164,7 +164,7 @@ func ToPurchaseListDTOs(items []entity.Purchase) []PurchaseListItemDTO { return result } -func toPurchaseApprovalDTO(p entity.Purchase) *approvalDTO.ApprovalBaseDTO { +func toPurchaseApprovalDTO(p entity.Purchase) *approvalDTO.ApprovalRelationDTO { if p.LatestApproval == nil || p.LatestApproval.Id == 0 { return nil } diff --git a/internal/modules/users/dto/user.dto.go b/internal/modules/users/dto/user.dto.go index 670a2810..b89a6724 100644 --- a/internal/modules/users/dto/user.dto.go +++ b/internal/modules/users/dto/user.dto.go @@ -8,7 +8,7 @@ import ( // === DTO Structs === -type UserBaseDTO struct { +type UserRelationDTO struct { Id uint `json:"id"` IdUser int64 `json:"id_user"` Email string `json:"email"` @@ -16,7 +16,7 @@ type UserBaseDTO struct { } type UserListDTO struct { - UserBaseDTO + UserRelationDTO CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } @@ -27,8 +27,8 @@ type UserDetailDTO struct { // === Mapper Functions === -func ToUserBaseDTO(m entity.User) UserBaseDTO { - return UserBaseDTO{ +func ToUserRelationDTO(m entity.User) UserRelationDTO { + return UserRelationDTO{ Id: m.Id, IdUser: m.IdUser, Email: m.Email, @@ -38,9 +38,9 @@ func ToUserBaseDTO(m entity.User) UserBaseDTO { func ToUserListDTO(m entity.User) UserListDTO { return UserListDTO{ - UserBaseDTO: ToUserBaseDTO(m), - CreatedAt: m.CreatedAt, - UpdatedAt: m.UpdatedAt, + UserRelationDTO: ToUserRelationDTO(m), + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, } } diff --git a/test/integration/master_data/area_test.go b/test/integration/master_data/area_test.go deleted file mode 100644 index ad8a17ad..00000000 --- a/test/integration/master_data/area_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package test - -import ( - "net/http" - "testing" - - "github.com/gofiber/fiber/v2" -) - -func TestAreaIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - - t.Run("create area trims name", func(t *testing.T) { - areaID := createArea(t, app, " Area Trim ") - if got := fetchAreaName(t, db, areaID); got != "Area Trim" { - t.Fatalf("expected trimmed name, got %q", got) - } - }) - - t.Run("duplicate area returns conflict", func(t *testing.T) { - createArea(t, app, "Duplicate Area") - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/areas", map[string]any{"name": "Duplicate Area"}) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 conflict, got %d: %s", resp.StatusCode, string(body)) - } - }) -} diff --git a/test/integration/master_data/bank_test.go b/test/integration/master_data/bank_test.go deleted file mode 100644 index 49a8c2a7..00000000 --- a/test/integration/master_data/bank_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package test - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "testing" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "gitlab.com/mbugroup/lti-api.git/internal/entities" -) - -func TestBankIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - - const bankAlias = "BNI" - const bankName = "Bank Negara Indonesia" - const bankOwner = "John Doe" - const bankAccountNumber = "1234567890" - var bankID uint - - t.Run("creating bank succeeds", func(t *testing.T) { - bankID = createBank(t, app, bankName, bankAlias, bankAccountNumber, bankOwner) - bank := fetchBank(t, db, bankID) - if bank.Alias != bankAlias { - t.Fatalf("expected alias %q, got %q", bankAlias, bank.Alias) - } - if bank.AccountNumber != bankAccountNumber { - t.Fatalf("expected account number %q, got %q", bankAccountNumber, bank.AccountNumber) - } - }) - - t.Run("creating bank with duplicate name fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/banks", map[string]any{ - "name": bankName, - "alias": "NEWALIAS", - "owner": "Owner Name", - "account_number": "1122334455", - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when creating duplicate bank name, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("getting existing bank succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/banks/%d", bankID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when fetching bank, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Id uint `json:"id"` - Name string `json:"name"` - Alias string `json:"alias"` - AccountNumber string `json:"account_number"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse response: %v", err) - } - if payload.Data.Id != bankID { - t.Fatalf("expected id %d, got %d", bankID, payload.Data.Id) - } - if payload.Data.Alias != bankAlias { - t.Fatalf("expected alias %q, got %q", bankAlias, payload.Data.Alias) - } - if payload.Data.AccountNumber != bankAccountNumber { - t.Fatalf("expected account number %q, got %q", bankAccountNumber, payload.Data.AccountNumber) - } - }) - - const updatedName = "BNI Updated" - - t.Run("updating bank name succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/banks/%d", bankID), map[string]any{ - "name": updatedName, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when updating bank, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Name string `json:"name"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse response: %v", err) - } - if payload.Data.Name != updatedName { - t.Fatalf("expected updated name %q, got %q", updatedName, payload.Data.Name) - } - bank := fetchBank(t, db, bankID) - if bank.Name != updatedName { - t.Fatalf("expected persisted name %q, got %q", updatedName, bank.Name) - } - }) - - t.Run("updating non existent bank returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, "/api/master-data/banks/9999", map[string]any{ - "name": "Does Not Matter", - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when updating missing bank, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("deleting bank succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/banks/%d", bankID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when deleting bank, got %d: %s", resp.StatusCode, string(body)) - } - var bank entities.Bank - if err := db.First(&bank, bankID).Error; !errors.Is(err, gorm.ErrRecordNotFound) { - t.Fatalf("expected bank to be deleted, got error %v", err) - } - }) - - t.Run("deleting non existent bank returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/banks/%d", bankID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when deleting missing bank, got %d: %s", resp.StatusCode, string(body)) - } - }) -} diff --git a/test/integration/master_data/customer_test.go b/test/integration/master_data/customer_test.go deleted file mode 100644 index f3b3dbed..00000000 --- a/test/integration/master_data/customer_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package test - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "testing" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/utils" -) - -func TestCustomerIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - - t.Run("creating customer without existing pic fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{ - "name": "Invalid Customer", - "pic_id": 9999, - "type": utils.CustomerSupplierTypeBisnis, - "address": "Somewhere", - "phone": "0800000000", - "email": "Invalid@example.com", - "account_number": "ACC-INVALID", - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when pic is missing, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("creating customer with Invalid type fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{ - "name": "Invalid Type", - "pic_id": 1, - "type": "UNKNOWN", - "address": "Somewhere", - "phone": "081234567891", - "email": "Invalid-type@example.com", - "account_number": "ACC-INVALID-TYPE", - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when type is Invalid, got %d: %s", resp.StatusCode, string(body)) - } - }) - - const customerName = "Customer Alpha" - var customerID uint - - t.Run("creating customer succeeds", func(t *testing.T) { - customerID = createCustomer(t, app, customerName, 1) - customer := fetchCustomer(t, db, customerID) - if customer.Name != customerName { - t.Fatalf("expected name %q, got %q", customerName, customer.Name) - } - if customer.PicId != 1 { - t.Fatalf("expected pic id 1, got %d", customer.PicId) - } - if customer.Type != string(utils.CustomerSupplierTypeBisnis) { - t.Fatalf("expected type %q, got %q", utils.CustomerSupplierTypeBisnis, customer.Type) - } - }) - - t.Run("creating customer with duplicate name fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{ - "name": customerName, - "pic_id": 1, - "type": utils.CustomerSupplierTypeBisnis, - "address": "Duplicate address", - "phone": "0811111111", - "email": "duplicate@example.com", - "account_number": "ACC-DUP", - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when creating duplicate customer, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("getting existing customer succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when fetching customer, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Id uint `json:"id"` - Name string `json:"name"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse customer response: %v", err) - } - if payload.Data.Id != customerID { - t.Fatalf("expected id %d, got %d", customerID, payload.Data.Id) - } - if payload.Data.Name != customerName { - t.Fatalf("expected name %q, got %q", customerName, payload.Data.Name) - } - }) - - const updatedName = "Customer Gamma" - - t.Run("updating customer name succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{ - "name": updatedName, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when updating customer, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Name string `json:"name"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse update response: %v", err) - } - if payload.Data.Name != updatedName { - t.Fatalf("expected updated name %q, got %q", updatedName, payload.Data.Name) - } - customer := fetchCustomer(t, db, customerID) - if customer.Name != updatedName { - t.Fatalf("expected persisted name %q, got %q", updatedName, customer.Name) - } - }) - - t.Run("updating customer type succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{ - "type": utils.CustomerSupplierTypeIndividual, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when updating customer type, got %d: %s", resp.StatusCode, string(body)) - } - customer := fetchCustomer(t, db, customerID) - if customer.Type != string(utils.CustomerSupplierTypeIndividual) { - t.Fatalf("expected persisted type %q, got %q", utils.CustomerSupplierTypeIndividual, customer.Type) - } - }) - - t.Run("updating customer with Invalid type fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/customers/%d", customerID), map[string]any{ - "type": "random-type", - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when updating with Invalid type, got %d: %s", resp.StatusCode, string(body)) - } - customer := fetchCustomer(t, db, customerID) - if customer.Type != string(utils.CustomerSupplierTypeIndividual) { - t.Fatalf("expected type to remain %q after Invalid update, got %q", utils.CustomerSupplierTypeIndividual, customer.Type) - } - }) - - t.Run("updating non existent customer returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, "/api/master-data/customers/9999", map[string]any{ - "name": "Does Not Matter", - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when updating missing customer, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("deleting customer succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when deleting customer, got %d: %s", resp.StatusCode, string(body)) - } - var customer entities.Customer - if err := db.First(&customer, customerID).Error; !errors.Is(err, gorm.ErrRecordNotFound) { - t.Fatalf("expected customer to be deleted, got error %v", err) - } - }) - - t.Run("deleting non existent customer returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when deleting missing customer, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("getting deleted customer returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/customers/%d", customerID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when fetching deleted customer, got %d: %s", resp.StatusCode, string(body)) - } - }) -} diff --git a/test/integration/master_data/fcr_test.go b/test/integration/master_data/fcr_test.go deleted file mode 100644 index a07666d8..00000000 --- a/test/integration/master_data/fcr_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package test - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "testing" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "gitlab.com/mbugroup/lti-api.git/internal/entities" -) - -func TestFcrIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - - t.Run("creating fcr without standards fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/fcrs", map[string]any{ - "name": "FCR Alpha", - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when standards missing, got %d: %s", resp.StatusCode, string(body)) - } - }) - - initialStandards := []map[string]any{ - { - "weight": 7.2, - "fcr_number": 400.0, - "mortality": 12.0, - }, - { - "weight": 7.4, - "fcr_number": 410.0, - "mortality": 11.5, - }, - } - - var fcrID uint - - t.Run("creating fcr succeeds", func(t *testing.T) { - fcrID = createFcr(t, app, "FCR Layer", initialStandards) - fcr := fetchFcr(t, db, fcrID) - if fcr.Name != "FCR Layer" { - t.Fatalf("expected name FCR Layer, got %q", fcr.Name) - } - if len(fcr.Standards) != len(initialStandards) { - t.Fatalf("expected %d standards, got %d", len(initialStandards), len(fcr.Standards)) - } - if fcr.CreatedBy != 1 { - t.Fatalf("expected created_by 1, got %d", fcr.CreatedBy) - } - }) - - t.Run("creating duplicate fcr name fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/fcrs", map[string]any{ - "name": "FCR Layer", - "fcr_standards": initialStandards, - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when creating duplicate, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("getting fcr detail succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when fetching fcr, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Id uint `json:"id"` - Name string `json:"name"` - FcrStandards []map[string]any `json:"fcr_standards"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse fcr detail: %v", err) - } - if payload.Data.Id != fcrID { - t.Fatalf("expected id %d, got %d", fcrID, payload.Data.Id) - } - if len(payload.Data.FcrStandards) != len(initialStandards) { - t.Fatalf("expected %d standards in response, got %d", len(initialStandards), len(payload.Data.FcrStandards)) - } - }) - - updatedStandards := []map[string]any{ - { - "weight": 7.2, - "fcr_number": 395.0, - "mortality": 10.0, - }, - { - "weight": 7.5, - "fcr_number": 420.0, - "mortality": 13.0, - }, - } - - t.Run("updating fcr name and standards succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), map[string]any{ - "name": "FCR Layer Updated", - "fcr_standards": updatedStandards, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when updating fcr, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Name string `json:"name"` - FcrStandards []map[string]any `json:"fcr_standards"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse update response: %v", err) - } - if payload.Data.Name != "FCR Layer Updated" { - t.Fatalf("expected updated name, got %q", payload.Data.Name) - } - if len(payload.Data.FcrStandards) != len(updatedStandards) { - t.Fatalf("expected %d standards after update, got %d", len(updatedStandards), len(payload.Data.FcrStandards)) - } - fcr := fetchFcr(t, db, fcrID) - if fcr.Name != "FCR Layer Updated" { - t.Fatalf("expected persisted name FCR Layer Updated, got %q", fcr.Name) - } - if len(fcr.Standards) != len(updatedStandards) { - t.Fatalf("expected %d persisted standards, got %d", len(updatedStandards), len(fcr.Standards)) - } - if fcr.Standards[0].FcrNumber != 395.0 { - t.Fatalf("expected first standard fcr_number 395, got %f", fcr.Standards[0].FcrNumber) - } - }) - - var otherFcrID uint - t.Run("creating another fcr for duplicate update", func(t *testing.T) { - otherFcrID = createFcr(t, app, "FCR Grower", []map[string]any{ - { - "weight": 8.0, - "fcr_number": 430.0, - "mortality": 9.0, - }, - }) - if otherFcrID == 0 { - t.Fatal("expected other fcr id to be non zero") - } - }) - - t.Run("updating fcr with duplicate name fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), map[string]any{ - "name": "FCR Grower", - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when renaming to existing fcr, got %d: %s", resp.StatusCode, string(body)) - } - fcr := fetchFcr(t, db, fcrID) - if fcr.Name != "FCR Layer Updated" { - t.Fatalf("expected name unchanged after failed update, got %q", fcr.Name) - } - }) - - t.Run("updating fcr with invalid standards fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), map[string]any{ - "fcr_standards": []map[string]any{ - { - "weight": -1, - "fcr_number": 300, - "mortality": 5, - }, - }, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when updating with invalid standard, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("deleting fcr succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when deleting fcr, got %d: %s", resp.StatusCode, string(body)) - } - var fcr entities.Fcr - if err := db.First(&fcr, fcrID).Error; !errors.Is(err, gorm.ErrRecordNotFound) { - t.Fatalf("expected fcr to be deleted, got error %v", err) - } - var standardsCount int64 - if err := db.Model(&entities.FcrStandard{}).Where("fcr_id = ?", fcrID).Count(&standardsCount).Error; err != nil { - t.Fatalf("failed counting standards: %v", err) - } - if standardsCount != 0 { - t.Fatalf("expected standards removed, found %d", standardsCount) - } - }) - - t.Run("deleting fcr again returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when deleting missing fcr, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("getting deleted fcr returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/fcrs/%d", fcrID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when fetching deleted fcr, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("cleanup other fcr", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/fcrs/%d", otherFcrID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when deleting other fcr, got %d: %s", resp.StatusCode, string(body)) - } - }) -} diff --git a/test/integration/master_data/kandang_test.go b/test/integration/master_data/kandang_test.go deleted file mode 100644 index b7b82b21..00000000 --- a/test/integration/master_data/kandang_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package test - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/gofiber/fiber/v2" - - "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/utils" -) - -func TestKandangIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - areaID := createArea(t, app, "Area Kandang") - locationID := createLocation(t, app, "Location For Kandang", "Address", areaID) - - t.Run("create kandang success", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{ - "name": "Kandang OK", - "location_id": locationID, - "pic_id": 1, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body)) - } - - var createResp struct { - Data struct { - Status string `json:"status"` - } `json:"data"` - } - if err := json.Unmarshal(body, &createResp); err != nil { - t.Fatalf("failed to parse create response: %v", err) - } - if createResp.Data.Status == "" { - t.Fatalf("expected default status to be returned, got empty") - } - }) - - t.Run("create kandang with unknown location fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{ - "name": "Kandang Fail", - "status": "ACTIVE", - "location_id": 999, - "pic_id": 1, - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("cannot assign project floc with existing active kandang", func(t *testing.T) { - fcrID := createFcr(t, app, "FCR For Floc", []map[string]any{ - {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, - }) - flocID := createFlock(t, app, "Floc Test") - - projectFloc := entities.ProjectFlock{ - FlockName: fmt.Sprintf("Project Flock %d", flocID), - AreaId: areaID, - Category: string(utils.ProjectFlockCategoryGrowing), - FcrId: fcrID, - LocationId: locationID, - Period: 1, - CreatedBy: 1, - } - if err := db.Create(&projectFloc).Error; err != nil { - t.Fatalf("failed to seed project floc: %v", err) - } - - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{ - "name": "Kandang Active 1", - "status": "ACTIVE", - "location_id": locationID, - "pic_id": 1, - "project_flock_id": projectFloc.Id, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating first kandang, got %d: %s", resp.StatusCode, string(body)) - } - - resp, body = doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{ - "name": "Kandang Active 2", - "status": "ACTIVE", - "location_id": locationID, - "pic_id": 1, - "project_flock_id": projectFloc.Id, - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when creating second active kandang, got %d: %s", resp.StatusCode, string(body)) - } - }) -} diff --git a/test/integration/master_data/location_test.go b/test/integration/master_data/location_test.go deleted file mode 100644 index 0c78ac2a..00000000 --- a/test/integration/master_data/location_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package test - -import ( - "net/http" - "testing" - - "github.com/gofiber/fiber/v2" -) - -func TestLocationIntegration(t *testing.T) { - app, _ := setupIntegrationApp(t) - - t.Run("creating location without existing area fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/locations", map[string]any{ - "name": "Loc A", - "address": "Address", - "area_id": 999, // non-existent - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("creating location succeeds", func(t *testing.T) { - areaID := createArea(t, app, "Area For Location") - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/locations", map[string]any{ - "name": "Location OK", - "address": "Addr", - "area_id": areaID, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body)) - } - }) -} diff --git a/test/integration/master_data/master_data.go b/test/integration/master_data/master_data.go deleted file mode 100644 index d43ddf15..00000000 --- a/test/integration/master_data/master_data.go +++ /dev/null @@ -1,401 +0,0 @@ -package test - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/glebarez/sqlite" - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - "gorm.io/gorm/logger" - - "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/route" - "gitlab.com/mbugroup/lti-api.git/internal/utils" -) - -func setupIntegrationApp(t *testing.T) (*fiber.App, *gorm.DB) { - t.Helper() - dir := t.TempDir() - dsn := filepath.Join(dir, "integration.db") - - db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) - if err != nil { - t.Fatalf("failed to open sqlite database: %v", err) - } - - if err := db.Exec("PRAGMA foreign_keys = ON").Error; err != nil { - t.Fatalf("failed to enable foreign keys: %v", err) - } - - if err := db.AutoMigrate( - &entities.User{}, - &entities.Area{}, - &entities.Location{}, - &entities.Flock{}, - &entities.ProjectFlock{}, - &entities.ProjectFlockKandang{}, - &entities.Kandang{}, - &entities.Warehouse{}, - &entities.Uom{}, - &entities.Customer{}, - &entities.Supplier{}, - &entities.Flag{}, - &entities.ProductCategory{}, - &entities.Nonstock{}, - &entities.NonstockSupplier{}, - &entities.Product{}, - &entities.ProductSupplier{}, - &entities.Fcr{}, - &entities.FcrStandard{}, - &entities.Bank{}, - ); err != nil { - t.Fatalf("auto migrate failed: %v", err) - } - - seedUser := entities.User{ - Id: 1, - IdUser: 1001, - Email: "tester@example.com", - Name: "Tester", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - if err := db.Create(&seedUser).Error; err != nil { - t.Fatalf("failed to seed user: %v", err) - } - - app := fiber.New(fiber.Config{ErrorHandler: utils.ErrorHandler}) - route.Routes(app, db) - return app, db -} - -func doJSONRequest(t *testing.T, app *fiber.App, method, path string, payload any) (*http.Response, []byte) { - t.Helper() - - var body io.Reader - if payload != nil { - buf := &bytes.Buffer{} - if err := json.NewEncoder(buf).Encode(payload); err != nil { - t.Fatalf("failed to encode payload: %v", err) - } - body = buf - } - - req := httptest.NewRequest(method, path, body) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - resp, err := app.Test(req, -1) - if err != nil { - t.Fatalf("request failed: %v", err) - } - - defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("failed to read response body: %v", err) - } - return resp, data -} - -func parseID(t *testing.T, body []byte) uint { - t.Helper() - var resp struct { - Data struct { - Id uint `json:"id"` - } `json:"data"` - } - if err := json.Unmarshal(body, &resp); err != nil { - t.Fatalf("failed to parse response: %v", err) - } - return resp.Data.Id -} - -func createArea(t *testing.T, app *fiber.App, name string) uint { - t.Helper() - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/areas", map[string]any{"name": name}) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating area, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func createLocation(t *testing.T, app *fiber.App, name, address string, areaID uint) uint { - t.Helper() - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/locations", map[string]any{ - "name": name, - "address": address, - "area_id": areaID, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating location, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func createUom(t *testing.T, app *fiber.App, name string) uint { - t.Helper() - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/uoms", map[string]any{"name": name}) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating uom, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func createKandang(t *testing.T, app *fiber.App, name string, locationID, picID uint) uint { - t.Helper() - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{ - "name": name, - "status": "ACTIVE", - "location_id": locationID, - "pic_id": picID, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating kandang, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func createCustomer(t *testing.T, app *fiber.App, name string, picID uint) uint { - t.Helper() - identifier := strings.ToLower(strings.ReplaceAll(name, " ", "_")) - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/customers", map[string]any{ - "name": name, - "pic_id": picID, - "type": utils.CustomerSupplierTypeBisnis, - "address": "Customer address", - "phone": "081234567890", - "email": fmt.Sprintf("%s@example.com", identifier), - "account_number": fmt.Sprintf("ACC-%s", identifier), - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating customer, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func fetchCustomer(t *testing.T, db *gorm.DB, id uint) entities.Customer { - t.Helper() - var customer entities.Customer - if err := db.Preload("Pic").Preload("CreatedUser").First(&customer, id).Error; err != nil { - t.Fatalf("failed to fetch customer: %v", err) - } - return customer -} - -func fetchKandang(t *testing.T, db *gorm.DB, id uint) entities.Kandang { - t.Helper() - var kandang entities.Kandang - if err := db.Preload("ProjectFlock").First(&kandang, id).Error; err != nil { - t.Fatalf("failed to fetch kandang: %v", err) - } - return kandang -} - -func createSupplier(t *testing.T, app *fiber.App, name, alias, category string) uint { - t.Helper() - identifier := strings.ToLower(strings.ReplaceAll(name, " ", "_")) - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/suppliers", map[string]any{ - "name": name, - "alias": alias, - "pic": "John Doe", - "type": utils.CustomerSupplierTypeBisnis, - "category": category, - "hatchery": "Hatchery A", - "phone": "081234567890", - "email": fmt.Sprintf("%s@supplier.com", identifier), - "address": "Supplier address", - "npwp": "NPWP-123", - "account_number": "ACC-SUPPLIER", - "due_date": 30, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating supplier, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func createProductCategory(t *testing.T, app *fiber.App, name, code string) uint { - t.Helper() - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/product-categories", map[string]any{ - "name": name, - "code": code, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating product category, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func fetchProductCategory(t *testing.T, db *gorm.DB, id uint) entities.ProductCategory { - t.Helper() - var pc entities.ProductCategory - if err := db.Preload("CreatedUser").First(&pc, id).Error; err != nil { - t.Fatalf("failed to fetch product category: %v", err) - } - return pc -} - -func createProduct(t *testing.T, app *fiber.App, name, brand string, sku *string, uomID, categoryID uint, productPrice float64, supplierIDs []uint, flags []string) uint { - t.Helper() - payload := map[string]any{ - "name": name, - "brand": brand, - "uom_id": uomID, - "product_category_id": categoryID, - "product_price": productPrice, - "supplier_ids": supplierIDs, - } - if sku != nil { - payload["sku"] = *sku - } - if len(flags) > 0 { - payload["flags"] = flags - } - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", payload) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating product, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func fetchProduct(t *testing.T, db *gorm.DB, id uint) entities.Product { - t.Helper() - var product entities.Product - if err := db.Preload("CreatedUser"). - Preload("Uom"). - Preload("ProductCategory"). - Preload("Suppliers", func(tx *gorm.DB) *gorm.DB { return tx.Order("suppliers.name ASC") }). - Preload("Flags", func(tx *gorm.DB) *gorm.DB { return tx.Order("flags.name ASC") }). - First(&product, id).Error; err != nil { - t.Fatalf("failed to fetch product: %v", err) - } - return product -} - -func fetchSupplier(t *testing.T, db *gorm.DB, id uint) entities.Supplier { - t.Helper() - var supplier entities.Supplier - if err := db.Preload("CreatedUser").First(&supplier, id).Error; err != nil { - t.Fatalf("failed to fetch supplier: %v", err) - } - return supplier -} - -func createFcr(t *testing.T, app *fiber.App, name string, standards []map[string]any) uint { - t.Helper() - payload := map[string]any{ - "name": name, - "fcr_standards": standards, - } - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/fcrs", payload) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating fcr, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func createFlock(t *testing.T, app *fiber.App, name string) uint { - t.Helper() - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/flocks", map[string]any{ - "name": name, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating flock, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func fetchFcr(t *testing.T, db *gorm.DB, id uint) entities.Fcr { - t.Helper() - var fcr entities.Fcr - if err := db.Preload("CreatedUser"). - Preload("Standards", func(tx *gorm.DB) *gorm.DB { - return tx.Order("weight ASC") - }). - First(&fcr, id).Error; err != nil { - t.Fatalf("failed to fetch fcr: %v", err) - } - return fcr -} - -func createNonstock(t *testing.T, app *fiber.App, name string, uomID uint, supplierIDs []uint, flags []string) uint { - t.Helper() - payload := map[string]any{ - "name": name, - "uom_id": uomID, - "supplier_ids": supplierIDs, - } - if len(flags) > 0 { - payload["flags"] = flags - } - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", payload) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating nonstock, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func fetchNonstock(t *testing.T, db *gorm.DB, id uint) entities.Nonstock { - t.Helper() - var nonstock entities.Nonstock - if err := db.Preload("CreatedUser"). - Preload("Uom"). - Preload("Suppliers", func(tx *gorm.DB) *gorm.DB { return tx.Order("suppliers.name ASC") }). - Preload("Flags", func(tx *gorm.DB) *gorm.DB { return tx.Order("flags.name ASC") }). - First(&nonstock, id).Error; err != nil { - t.Fatalf("failed to fetch nonstock: %v", err) - } - return nonstock -} - -func fetchAreaName(t *testing.T, db *gorm.DB, id uint) string { - t.Helper() - var area entities.Area - if err := db.First(&area, id).Error; err != nil { - t.Fatalf("failed to fetch area: %v", err) - } - return area.Name -} - -func fetchWarehouse(t *testing.T, db *gorm.DB, id uint) entities.Warehouse { - t.Helper() - var wh entities.Warehouse - if err := db.First(&wh, id).Error; err != nil { - t.Fatalf("failed to fetch warehouse: %v", err) - } - return wh -} - -func createBank(t *testing.T, app *fiber.App, name, alias, accountNumber string, owner any) uint { - t.Helper() - payload := map[string]any{ - "name": name, - "alias": alias, - "account_number": accountNumber, - "owner": owner, - } - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/banks", payload) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when creating bank, got %d: %s", resp.StatusCode, string(body)) - } - return parseID(t, body) -} - -func fetchBank(t *testing.T, db *gorm.DB, id uint) entities.Bank { - t.Helper() - var bank entities.Bank - if err := db.Preload("CreatedUser").First(&bank, id).Error; err != nil { - t.Fatalf("failed to fetch bank: %v", err) - } - return bank -} diff --git a/test/integration/master_data/nonstock_test.go b/test/integration/master_data/nonstock_test.go deleted file mode 100644 index cadea99d..00000000 --- a/test/integration/master_data/nonstock_test.go +++ /dev/null @@ -1,309 +0,0 @@ -package test - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - "testing" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/utils" -) - -func TestNonstockIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - - uomID := createUom(t, app, "Unit Piece") - altUomID := createUom(t, app, "Unit Box") - - supplierID1 := createSupplier(t, app, "Nonstock Supplier One", "ns1", string(utils.SupplierCategoryBOP)) - supplierID2 := createSupplier(t, app, "Nonstock Supplier Two", "ns2", string(utils.SupplierCategoryBOP)) - supplierID3 := createSupplier(t, app, "Nonstock Supplier Three", "ns3", string(utils.SupplierCategoryBOP)) - sapronakSupplierID := createSupplier(t, app, "SAPRONAK Supplier", "sap1", string(utils.SupplierCategorySapronak)) - - nonstockFlags := []string{string(utils.FlagEkspedisi)} - - t.Run("create nonstock without suppliers succeeds with empty relations", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{ - "name": "Supplierless Nonstock", - "uom_id": uomID, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when suppliers omitted, got %d: %s", resp.StatusCode, string(body)) - } - id := parseID(t, body) - ns := fetchNonstock(t, db, id) - if len(ns.Suppliers) != 0 { - t.Fatalf("expected no suppliers persisted, found %d", len(ns.Suppliers)) - } - if len(ns.Flags) != 0 { - t.Fatalf("expected no flags persisted, found %d", len(ns.Flags)) - } - resp, _ = doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", id), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected cleanup delete to succeed, got %d", resp.StatusCode) - } - }) - - t.Run("create nonstock with unknown supplier fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{ - "name": "Unknown Supplier Nonstock", - "uom_id": uomID, - "supplier_ids": []uint{99999}, - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when supplier missing, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("create nonstock with sapronak supplier fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{ - "name": "Invalid Category Nonstock", - "uom_id": uomID, - "supplier_ids": []uint{sapronakSupplierID}, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when supplier category is not BOP, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("create nonstock with invalid flags fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{ - "name": "Invalid Flag Nonstock", - "uom_id": uomID, - "supplier_ids": []uint{supplierID1}, - "flags": []string{"UNKNOWN"}, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when flags invalid, got %d: %s", resp.StatusCode, string(body)) - } - }) - - var nonstockID uint - - t.Run("create nonstock succeeds", func(t *testing.T) { - nonstockID = createNonstock(t, app, "Layer Feed", uomID, []uint{supplierID1, supplierID2, supplierID1}, nonstockFlags) - if nonstockID == 0 { - t.Fatal("expected nonstock id to be non zero") - } - ns := fetchNonstock(t, db, nonstockID) - if ns.Name != "Layer Feed" { - t.Fatalf("expected name Layer Feed, got %q", ns.Name) - } - if ns.UomId != uomID { - t.Fatalf("expected uom_id %d, got %d", uomID, ns.UomId) - } - if ns.CreatedBy != 1 { - t.Fatalf("expected created_by 1, got %d", ns.CreatedBy) - } - if len(ns.Suppliers) != 2 { - t.Fatalf("expected 2 unique suppliers, got %d", len(ns.Suppliers)) - } - if len(ns.Flags) != len(nonstockFlags) { - t.Fatalf("expected %d flags, got %d", len(nonstockFlags), len(ns.Flags)) - } - expectedFlags := make(map[string]struct{}, len(nonstockFlags)) - for _, flag := range nonstockFlags { - expectedFlags[strings.ToUpper(flag)] = struct{}{} - } - for _, flag := range ns.Flags { - upper := strings.ToUpper(flag.Name) - if _, ok := expectedFlags[upper]; !ok { - t.Fatalf("unexpected flag stored: %s", upper) - } - delete(expectedFlags, upper) - } - if len(expectedFlags) != 0 { - t.Fatalf("missing flags after create: %v", expectedFlags) - } - }) - - t.Run("get nonstock detail includes suppliers", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when fetching nonstock, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Id uint `json:"id"` - Name string `json:"name"` - UomID uint `json:"uom_id"` - Suppliers []map[string]any `json:"suppliers"` - Flags []string `json:"flags"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse nonstock detail: %v", err) - } - if payload.Data.Id != nonstockID { - t.Fatalf("expected id %d, got %d", nonstockID, payload.Data.Id) - } - if payload.Data.UomID != uomID { - t.Fatalf("expected response uom_id %d, got %d", uomID, payload.Data.UomID) - } - if len(payload.Data.Suppliers) != 2 { - t.Fatalf("expected 2 suppliers in response, got %d", len(payload.Data.Suppliers)) - } - if len(payload.Data.Flags) != len(nonstockFlags) { - t.Fatalf("expected %d flags in response, got %d", len(nonstockFlags), len(payload.Data.Flags)) - } - expected := make(map[string]struct{}, len(nonstockFlags)) - for _, flag := range nonstockFlags { - expected[strings.ToUpper(flag)] = struct{}{} - } - for _, flag := range payload.Data.Flags { - flag = strings.ToUpper(flag) - if _, ok := expected[flag]; !ok { - t.Fatalf("unexpected flag %s returned", flag) - } - delete(expected, flag) - } - if len(expected) != 0 { - t.Fatalf("missing flags in response: %v", expected) - } - }) - - t.Run("update nonstock with invalid uom fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ - "uom_id": 99999, - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when updating with invalid uom, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("update nonstock with invalid supplier fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ - "supplier_ids": []uint{supplierID1, 99999}, - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when updating with invalid supplier, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("update nonstock with sapronak supplier fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ - "supplier_ids": []uint{sapronakSupplierID}, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when updating with non-BOP supplier, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("update nonstock with invalid flags fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ - "flags": []string{"BAD"}, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when updating flags invalid, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("update nonstock name uom and suppliers succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ - "name": "Layer Feed Premium", - "uom_id": altUomID, - "supplier_ids": []uint{supplierID3}, - "flags": nonstockFlags, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when updating nonstock, got %d: %s", resp.StatusCode, string(body)) - } - ns := fetchNonstock(t, db, nonstockID) - if ns.Name != "Layer Feed Premium" { - t.Fatalf("expected name Layer Feed Premium, got %q", ns.Name) - } - if ns.UomId != altUomID { - t.Fatalf("expected uom_id %d, got %d", altUomID, ns.UomId) - } - if len(ns.Suppliers) != 1 || ns.Suppliers[0].Id != supplierID3 { - t.Fatalf("expected suppliers to contain only %d", supplierID3) - } - if len(ns.Flags) != len(nonstockFlags) { - t.Fatalf("expected flags retained, got %d", len(ns.Flags)) - } - }) - - t.Run("clear suppliers succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ - "supplier_ids": []uint{}, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when clearing suppliers, got %d: %s", resp.StatusCode, string(body)) - } - ns := fetchNonstock(t, db, nonstockID) - if len(ns.Suppliers) != 0 { - t.Fatalf("expected suppliers to be cleared, got %d entries", len(ns.Suppliers)) - } - if len(ns.Flags) != len(nonstockFlags) { - t.Fatalf("expected flags unaffected, got %d", len(ns.Flags)) - } - }) - - t.Run("clear nonstock flags succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ - "flags": []string{}, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when clearing nonstock flags, got %d: %s", resp.StatusCode, string(body)) - } - ns := fetchNonstock(t, db, nonstockID) - if len(ns.Flags) != 0 { - t.Fatalf("expected flags cleared, got %d", len(ns.Flags)) - } - }) - - t.Run("delete nonstock succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when deleting nonstock, got %d: %s", resp.StatusCode, string(body)) - } - var ns entities.Nonstock - if err := db.First(&ns, nonstockID).Error; !errors.Is(err, gorm.ErrRecordNotFound) { - t.Fatalf("expected nonstock to be deleted, got error %v", err) - } - var links int64 - if err := db.Model(&entities.NonstockSupplier{}).Where("nonstock_id = ?", nonstockID).Count(&links).Error; err != nil { - t.Fatalf("failed counting nonstock suppliers: %v", err) - } - if links != 0 { - t.Fatalf("expected link table cleared, found %d rows", links) - } - var flagCount int64 - if err := db.Model(&entities.Flag{}). - Where("flagable_id = ? AND flagable_type = ?", nonstockID, entities.FlagableTypeNonstock). - Count(&flagCount).Error; err != nil { - t.Fatalf("failed counting nonstock flags: %v", err) - } - if flagCount != 0 { - t.Fatalf("expected flags removed, found %d", flagCount) - } - }) - - t.Run("deleting nonstock twice returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when deleting missing nonstock, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("fetching deleted nonstock returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when fetching deleted nonstock, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("cleanup additional supplier references", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID3), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when deleting supplier, got %d: %s", resp.StatusCode, string(body)) - } - doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", sapronakSupplierID), nil) - }) -} diff --git a/test/integration/master_data/product_category_test.go b/test/integration/master_data/product_category_test.go deleted file mode 100644 index 14012301..00000000 --- a/test/integration/master_data/product_category_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package test - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "testing" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "gitlab.com/mbugroup/lti-api.git/internal/entities" -) - -func TestProductCategoryIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - - t.Run("create product category missing code fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/product-categories", map[string]any{ - "name": "Layer Feed", - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when code missing, got %d: %s", resp.StatusCode, string(body)) - } - }) - - var categoryID uint - - t.Run("create product category succeeds", func(t *testing.T) { - categoryID = createProductCategory(t, app, "Layer Feed", "LFD") - pc := fetchProductCategory(t, db, categoryID) - if pc.Name != "Layer Feed" { - t.Fatalf("expected name Layer Feed, got %q", pc.Name) - } - if pc.Code != "LFD" { - t.Fatalf("expected code LFD, got %q", pc.Code) - } - if pc.CreatedBy != 1 { - t.Fatalf("expected created_by 1, got %d", pc.CreatedBy) - } - }) - - t.Run("creating duplicate name fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/product-categories", map[string]any{ - "name": "Layer Feed", - "code": "LF2", - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when creating duplicate name, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("creating duplicate code fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/product-categories", map[string]any{ - "name": "Layer Feed Premium", - "code": "LFD", - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when creating duplicate code, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("get product category detail", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when fetching product category, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Id uint `json:"id"` - Name string `json:"name"` - Code string `json:"code"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse response: %v", err) - } - if payload.Data.Id != categoryID { - t.Fatalf("expected id %d, got %d", categoryID, payload.Data.Id) - } - if payload.Data.Code != "LFD" { - t.Fatalf("expected code LFD, got %q", payload.Data.Code) - } - }) - - t.Run("update product category with duplicate name fails", func(t *testing.T) { - otherID := createProductCategory(t, app, "Layer Feed Alt", "LFA") - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/product-categories/%d", otherID), map[string]any{ - "name": "Layer Feed", - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when updating with duplicate name, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("update product category succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), map[string]any{ - "name": "Layer Feed Updated", - "code": "LFU", - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when updating product category, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Name string `json:"name"` - Code string `json:"code"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse update response: %v", err) - } - if payload.Data.Name != "Layer Feed Updated" { - t.Fatalf("expected updated name, got %q", payload.Data.Name) - } - if payload.Data.Code != "LFU" { - t.Fatalf("expected code LFU, got %q", payload.Data.Code) - } - pc := fetchProductCategory(t, db, categoryID) - if pc.Code != "LFU" { - t.Fatalf("expected persisted code LFU, got %q", pc.Code) - } - }) - - t.Run("delete product category", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when deleting product category, got %d: %s", resp.StatusCode, string(body)) - } - var pc entities.ProductCategory - if err := db.First(&pc, categoryID).Error; !errors.Is(err, gorm.ErrRecordNotFound) { - t.Fatalf("expected product category deleted, got error %v", err) - } - }) - - t.Run("delete non existing product category returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when deleting missing product category, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("get deleted product category returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/product-categories/%d", categoryID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when fetching deleted product category, got %d: %s", resp.StatusCode, string(body)) - } - }) -} diff --git a/test/integration/master_data/product_test.go b/test/integration/master_data/product_test.go deleted file mode 100644 index 4ffb1463..00000000 --- a/test/integration/master_data/product_test.go +++ /dev/null @@ -1,410 +0,0 @@ -package test - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - "testing" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/utils" -) - -func TestProductIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - - uomID := createUom(t, app, "Kilogram") - categoryID := createProductCategory(t, app, "Feed", "FED") - - sapSupplier1 := createSupplier(t, app, "Feed Supplier One", "fs1", string(utils.SupplierCategorySapronak)) - sapSupplier2 := createSupplier(t, app, "Feed Supplier Two", "fs2", string(utils.SupplierCategorySapronak)) - bopSupplier := createSupplier(t, app, "BOP Supplier", "bop1", string(utils.SupplierCategoryBOP)) - - productFlags := []string{string(utils.FlagDOC), string(utils.FlagPakan)} - updatedProductFlags := []string{string(utils.FlagFinisher), string(utils.FlagObat)} - - t.Run("create product without suppliers succeeds with empty relations", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ - "name": "Supplierless Product", - "brand": "Brand A", - "uom_id": uomID, - "product_category_id": categoryID, - "product_price": 12000, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201 when suppliers omitted, got %d: %s", resp.StatusCode, string(body)) - } - id := parseID(t, body) - product := fetchProduct(t, db, id) - if len(product.Suppliers) != 0 { - t.Fatalf("expected no suppliers persisted, found %d", len(product.Suppliers)) - } - if len(product.Flags) != 0 { - t.Fatalf("expected no flags persisted, found %d", len(product.Flags)) - } - resp, _ = doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/products/%d", id), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected cleanup delete to succeed, got %d", resp.StatusCode) - } - }) - - t.Run("create product with invalid uom fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ - "name": "Layer Feed Invalid UOM", - "brand": "Brand A", - "uom_id": 9999, - "product_category_id": categoryID, - "product_price": 12000, - "supplier_ids": []uint{sapSupplier1}, - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when uom missing, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("create product with invalid category fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ - "name": "Layer Feed Invalid Category", - "brand": "Brand A", - "uom_id": uomID, - "product_category_id": 9999, - "product_price": 12000, - "supplier_ids": []uint{sapSupplier1}, - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when category missing, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("create product with invalid supplier fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ - "name": "Layer Feed Missing Supplier", - "brand": "Brand A", - "uom_id": uomID, - "product_category_id": categoryID, - "product_price": 12000, - "supplier_ids": []uint{99999}, - }) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when supplier missing, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("create product with BOP supplier fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ - "name": "Layer Feed Wrong Supplier", - "brand": "Brand A", - "uom_id": uomID, - "product_category_id": categoryID, - "product_price": 12000, - "supplier_ids": []uint{bopSupplier}, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when supplier category invalid, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("create product with invalid flags fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ - "name": "Layer Feed Invalid Flags", - "brand": "Brand A", - "uom_id": uomID, - "product_category_id": categoryID, - "product_price": 12000, - "supplier_ids": []uint{sapSupplier1}, - "flags": []string{"INVALID"}, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when flags invalid, got %d: %s", resp.StatusCode, string(body)) - } - }) - - var productID uint - - t.Run("create product succeeds", func(t *testing.T) { - sku := "lfd-001" - productID = createProduct(t, app, "Layer Feed", "Brand A", &sku, uomID, categoryID, 15000, []uint{sapSupplier1, sapSupplier2}, productFlags) - product := fetchProduct(t, db, productID) - if product.Name != "Layer Feed" { - t.Fatalf("expected name Layer Feed, got %q", product.Name) - } - if product.Brand != "Brand A" { - t.Fatalf("expected brand Brand A, got %q", product.Brand) - } - if product.Sku == nil || *product.Sku != strings.ToUpper(sku) { - t.Fatalf("expected sku %q, got %+v", strings.ToUpper(sku), product.Sku) - } - if product.UomId != uomID { - t.Fatalf("expected uom_id %d, got %d", uomID, product.UomId) - } - if product.ProductCategoryId != categoryID { - t.Fatalf("expected product_category_id %d, got %d", categoryID, product.ProductCategoryId) - } - if product.CreatedBy != 1 { - t.Fatalf("expected created_by 1, got %d", product.CreatedBy) - } - if len(product.Suppliers) != 2 { - t.Fatalf("expected 2 suppliers, got %d", len(product.Suppliers)) - } - if len(product.Flags) != len(productFlags) { - t.Fatalf("expected %d flags, got %d", len(productFlags), len(product.Flags)) - } - expectedFlags := make(map[string]struct{}, len(productFlags)) - for _, flag := range productFlags { - expectedFlags[strings.ToUpper(flag)] = struct{}{} - } - for _, flag := range product.Flags { - if _, ok := expectedFlags[strings.ToUpper(flag.Name)]; !ok { - t.Fatalf("unexpected flag %s present", flag.Name) - } - delete(expectedFlags, strings.ToUpper(flag.Name)) - } - if len(expectedFlags) != 0 { - t.Fatalf("missing expected flags: %v", expectedFlags) - } - }) - - t.Run("creating duplicate name fails", func(t *testing.T) { - sku := "lfd-002" - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ - "name": "Layer Feed", - "brand": "Brand B", - "sku": sku, - "uom_id": uomID, - "product_category_id": categoryID, - "product_price": 16000, - "supplier_ids": []uint{sapSupplier1}, - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when creating duplicate name, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("creating duplicate sku fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ - "name": "Layer Feed Premium", - "brand": "Brand B", - "sku": "LFD-001", - "uom_id": uomID, - "product_category_id": categoryID, - "product_price": 17000, - "supplier_ids": []uint{sapSupplier1}, - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when creating duplicate sku, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("get product detail returns nested data", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/products/%d", productID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when fetching product, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Id uint `json:"id"` - Name string `json:"name"` - Brand string `json:"brand"` - Uom *struct { - Id uint `json:"id"` - } `json:"uom"` - Suppliers []map[string]any `json:"suppliers"` - Flags []string `json:"flags"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse product detail: %v", err) - } - if payload.Data.Id != productID { - t.Fatalf("expected id %d, got %d", productID, payload.Data.Id) - } - if payload.Data.Uom == nil || payload.Data.Uom.Id != uomID { - t.Fatalf("expected uom id %d, got %+v", uomID, payload.Data.Uom) - } - if len(payload.Data.Suppliers) != 2 { - t.Fatalf("expected 2 suppliers in response, got %d", len(payload.Data.Suppliers)) - } - if len(payload.Data.Flags) != len(productFlags) { - t.Fatalf("expected %d flags in response, got %d", len(productFlags), len(payload.Data.Flags)) - } - expected := make(map[string]struct{}, len(productFlags)) - for _, flag := range productFlags { - expected[strings.ToUpper(flag)] = struct{}{} - } - for _, flag := range payload.Data.Flags { - flag = strings.ToUpper(flag) - if _, ok := expected[flag]; !ok { - t.Fatalf("unexpected flag %s returned", flag) - } - delete(expected, flag) - } - if len(expected) != 0 { - t.Fatalf("missing expected flags in response: %v", expected) - } - }) - - t.Run("update product with invalid supplier category fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ - "supplier_ids": []uint{bopSupplier}, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when updating with non SAPRONAK supplier, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("update product with invalid flags fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ - "flags": []string{"UNKNOWN"}, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when updating with invalid flags, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("update product succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ - "name": "Layer Feed Updated", - "brand": "Brand C", - "sku": "lfd-100", - "product_price": 18000, - "selling_price": 19000, - "tax": 1000, - "expiry_period": 30, - "supplier_ids": []uint{sapSupplier1}, - "flags": updatedProductFlags, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when updating product, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Name string `json:"name"` - Brand string `json:"brand"` - Sku *string `json:"sku"` - ProductPrice float64 `json:"product_price"` - SupplierIds []map[string]any `json:"suppliers"` - Flags []string `json:"flags"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse update response: %v", err) - } - if payload.Data.Name != "Layer Feed Updated" { - t.Fatalf("expected updated name, got %q", payload.Data.Name) - } - if len(payload.Data.Flags) != len(updatedProductFlags) { - t.Fatalf("expected %d flags in response, got %d", len(updatedProductFlags), len(payload.Data.Flags)) - } - respFlags := make(map[string]struct{}, len(payload.Data.Flags)) - for _, flag := range payload.Data.Flags { - respFlags[strings.ToUpper(flag)] = struct{}{} - } - for _, flag := range updatedProductFlags { - if _, ok := respFlags[strings.ToUpper(flag)]; !ok { - t.Fatalf("missing flag %s in response", flag) - } - } - product := fetchProduct(t, db, productID) - if product.Brand != "Brand C" { - t.Fatalf("expected persisted brand Brand C, got %q", product.Brand) - } - if product.Sku == nil || *product.Sku != "LFD-100" { - t.Fatalf("expected persisted sku LFD-100, got %+v", product.Sku) - } - if len(product.Suppliers) != 1 || product.Suppliers[0].Id != sapSupplier1 { - t.Fatalf("expected supplier to be %d", sapSupplier1) - } - if len(product.Flags) != len(updatedProductFlags) { - t.Fatalf("expected %d flags after update, got %d", len(updatedProductFlags), len(product.Flags)) - } - expectedFlags := make(map[string]struct{}, len(updatedProductFlags)) - for _, flag := range updatedProductFlags { - expectedFlags[strings.ToUpper(flag)] = struct{}{} - } - for _, flag := range product.Flags { - upper := strings.ToUpper(flag.Name) - if _, ok := expectedFlags[upper]; !ok { - t.Fatalf("unexpected flag after update: %s", upper) - } - delete(expectedFlags, upper) - } - if len(expectedFlags) != 0 { - t.Fatalf("missing flags after update: %v", expectedFlags) - } - }) - - t.Run("clear suppliers succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ - "supplier_ids": []uint{}, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when clearing suppliers, got %d: %s", resp.StatusCode, string(body)) - } - product := fetchProduct(t, db, productID) - if len(product.Suppliers) != 0 { - t.Fatalf("expected no suppliers after clearing, got %d", len(product.Suppliers)) - } - if len(product.Flags) != len(updatedProductFlags) { - t.Fatalf("expected flags untouched after clearing suppliers, got %d", len(product.Flags)) - } - }) - - t.Run("clear flags succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ - "flags": []string{}, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when clearing flags, got %d: %s", resp.StatusCode, string(body)) - } - product := fetchProduct(t, db, productID) - if len(product.Flags) != 0 { - t.Fatalf("expected all flags cleared, got %d", len(product.Flags)) - } - }) - - t.Run("delete product succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/products/%d", productID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when deleting product, got %d: %s", resp.StatusCode, string(body)) - } - var product entities.Product - if err := db.First(&product, productID).Error; !errors.Is(err, gorm.ErrRecordNotFound) { - t.Fatalf("expected product deleted, got error %v", err) - } - var links int64 - if err := db.Model(&entities.ProductSupplier{}).Where("product_id = ?", productID).Count(&links).Error; err != nil { - t.Fatalf("failed counting product suppliers: %v", err) - } - if links != 0 { - t.Fatalf("expected pivot cleared, found %d rows", links) - } - var flagCount int64 - if err := db.Model(&entities.Flag{}). - Where("flagable_id = ? AND flagable_type = ?", productID, entities.FlagableTypeProduct). - Count(&flagCount).Error; err != nil { - t.Fatalf("failed counting product flags: %v", err) - } - if flagCount != 0 { - t.Fatalf("expected flags removed, found %d", flagCount) - } - }) - - t.Run("delete missing product returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/products/%d", productID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when deleting missing product, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("get deleted product returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/products/%d", productID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when fetching deleted product, got %d: %s", resp.StatusCode, string(body)) - } - }) -} diff --git a/test/integration/master_data/project_flock_test.go b/test/integration/master_data/project_flock_test.go deleted file mode 100644 index a7f8f3f8..00000000 --- a/test/integration/master_data/project_flock_test.go +++ /dev/null @@ -1,417 +0,0 @@ -package test - -// import ( -// "encoding/json" -// "fmt" -// "net/http" -// "net/url" -// "testing" - -// "github.com/gofiber/fiber/v2" - -// "gitlab.com/mbugroup/lti-api.git/internal/entities" -// "gitlab.com/mbugroup/lti-api.git/internal/utils" -// ) - -// func TestProjectFlockSummary(t *testing.T) { -// app, db := setupIntegrationApp(t) - -// areaID := createArea(t, app, "Area Project") -// locationID := createLocation(t, app, "Location Project", "Address", areaID) -// flockID := createFlock(t, app, "Flock Summary") -// fcrID := createFcr(t, app, "FCR Summary", []map[string]any{ -// {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, -// }) -// kandangID := createKandang(t, app, "Kandang Summary", locationID, 1) - -// createPayload := map[string]any{ -// "flock_id": flockID, -// "area_id": areaID, -// "category": "growing", -// "fcr_id": fcrID, -// "location_id": locationID, -// "kandang_ids": []uint{kandangID}, -// } -// resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload) -// if resp.StatusCode != fiber.StatusCreated { -// t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body)) -// } - -// var createResp struct { -// Data struct { -// Id uint `json:"id"` -// Period int `json:"period"` -// Category string `json:"category"` -// Flock struct { -// Id uint `json:"id"` -// Name string `json:"name"` -// } `json:"flock"` -// Area struct { -// Id uint `json:"id"` -// Name string `json:"name"` -// } `json:"area"` -// Fcr struct { -// Id uint `json:"id"` -// Name string `json:"name"` -// } `json:"fcr"` -// Location struct { -// Id uint `json:"id"` -// Name string `json:"name"` -// Address string `json:"address"` -// } `json:"location"` -// Kandangs []struct { -// Id uint `json:"id"` -// Name string `json:"name"` -// Status string `json:"status"` -// } `json:"kandangs"` -// CreatedUser struct { -// Id uint `json:"id"` -// IdUser uint `json:"id_user"` -// Email string `json:"email"` -// Name string `json:"name"` -// } `json:"created_user"` -// } `json:"data"` -// } -// if err := json.Unmarshal(body, &createResp); err != nil { -// t.Fatalf("failed to parse create response: %v", err) -// } -// if createResp.Data.Flock.Id != flockID || createResp.Data.Flock.Name == "" { -// t.Fatalf("expected flock detail to be present, got %+v", createResp.Data.Flock) -// } -// if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" { -// t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area) -// } -// if createResp.Data.Category != string(utils.ProjectFlockCategoryGrowing) { -// t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryGrowing, createResp.Data.Category) -// } -// if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" { -// t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location) -// } -// if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID { -// t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs) -// } -// if createResp.Data.Kandangs[0].Status != string(utils.KandangStatusPengajuan) { -// t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status) -// } -// if createResp.Data.Period != 1 { -// t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period) -// } - -// createdKandang := fetchKandang(t, db, kandangID) -// if createdKandang.Status != string(utils.KandangStatusPengajuan) { -// t.Fatalf("expected kandang status in DB to be PENGAJUAN, got %s", createdKandang.Status) -// } - -// var pivotRecords []entities.ProjectFlockKandang -// if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil { -// t.Fatalf("failed to fetch pivot records: %v", err) -// } -// if len(pivotRecords) != 1 { -// t.Fatalf("expected 1 pivot record, got %d", len(pivotRecords)) -// } -// firstPivotRecord := pivotRecords[0] -// if firstPivotRecord.KandangId != kandangID { -// t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId) -// } - -// secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1) -// secondPayload := map[string]any{ -// "flock_id": flockID, -// "area_id": areaID, -// "category": "laying", -// "fcr_id": fcrID, -// "location_id": locationID, -// "kandang_ids": []uint{secondKandangID}, -// } -// resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", secondPayload) -// if resp.StatusCode != fiber.StatusCreated { -// t.Fatalf("expected 201 when creating second project flock, got %d: %s", resp.StatusCode, string(body)) -// } -// var createRespSecond struct { -// Data struct { -// Id uint `json:"id"` -// Period int `json:"period"` -// Category string `json:"category"` -// } `json:"data"` -// } -// if err := json.Unmarshal(body, &createRespSecond); err != nil { -// t.Fatalf("failed to parse second create response: %v", err) -// } -// if createRespSecond.Data.Period != 2 { -// t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period) -// } -// if createRespSecond.Data.Category != string(utils.ProjectFlockCategoryLaying) { -// t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryLaying, createRespSecond.Data.Category) -// } - -// pivotRecords = nil -// if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil { -// t.Fatalf("failed to fetch second pivot records: %v", err) -// } -// if len(pivotRecords) != 1 { -// t.Fatalf("expected 1 pivot record for second project, got %d", len(pivotRecords)) -// } -// secondPivotRecord := pivotRecords[0] -// if secondPivotRecord.KandangId != secondKandangID { -// t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId) -// } - -// secondKandang := fetchKandang(t, db, secondKandangID) -// if secondKandang.Status != string(utils.KandangStatusPengajuan) { -// t.Fatalf("expected second kandang status in DB to be PENGAJUAN, got %s", secondKandang.Status) -// } - -// resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) -// if resp.StatusCode != fiber.StatusOK { -// t.Fatalf("expected 200 when fetching summary, got %d: %s", resp.StatusCode, string(body)) -// } - -// var summary struct { -// Data struct { -// NextPeriod int `json:"next_period"` -// } `json:"data"` -// } -// if err := json.Unmarshal(body, &summary); err != nil { -// t.Fatalf("failed to parse summary response: %v", err) -// } - -// if summary.Data.NextPeriod != 3 { -// t.Fatalf("expected next_period 3, got %d", summary.Data.NextPeriod) -// } - -// resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createResp.Data.Id), nil) -// if resp.StatusCode != fiber.StatusOK { -// t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body)) -// } - -// firstKandang := fetchKandang(t, db, kandangID) -// if firstKandang.ProjectFlockId != nil { -// t.Fatalf("expected project_flock_id to be nil after delete, got %v", *firstKandang.ProjectFlockId) -// } -// if firstKandang.Status != string(utils.KandangStatusNonActive) { -// t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status) -// } - -// var remainingFirst int64 -// if err := db.Model(&entities.ProjectFlockKandang{}). -// Where("project_flock_id = ? AND kandang_id = ?", createResp.Data.Id, kandangID). -// Count(&remainingFirst).Error; err != nil { -// t.Fatalf("failed to count first pivot records after delete: %v", err) -// } -// if remainingFirst != 0 { -// t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst) -// } - -// resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil) -// if resp.StatusCode != fiber.StatusOK { -// t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body)) -// } - -// secondKandang = fetchKandang(t, db, secondKandangID) -// if secondKandang.ProjectFlockId != nil { -// t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId) -// } -// if secondKandang.Status != string(utils.KandangStatusNonActive) { -// t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status) -// } - -// var remainingSecond int64 -// if err := db.Model(&entities.ProjectFlockKandang{}). -// Where("project_flock_id = ? AND kandang_id = ?", createRespSecond.Data.Id, secondKandangID). -// Count(&remainingSecond).Error; err != nil { -// t.Fatalf("failed to count second pivot records after delete: %v", err) -// } -// if remainingSecond != 0 { -// t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond) -// } - -// resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) -// if resp.StatusCode != fiber.StatusOK { -// t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body)) -// } - -// if err := json.Unmarshal(body, &summary); err != nil { -// t.Fatalf("failed to parse summary response after delete: %v", err) -// } - -// if summary.Data.NextPeriod != 1 { -// t.Fatalf("expected next_period 1 after soft deletes, got %d", summary.Data.NextPeriod) -// } -// } - -// func uintToString(v uint) string { -// return fmt.Sprintf("%d", v) -// } - -// func TestProjectFlockSearchByRelatedFields(t *testing.T) { -// app, _ := setupIntegrationApp(t) - -// areaID := createArea(t, app, "Area Search Target") -// locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID) -// flockID := createFlock(t, app, "Flock Search Target") -// fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{ -// {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, -// }) -// kandangID := createKandang(t, app, "Kandang Search Target", locationID, 1) - -// createPayload := map[string]any{ -// "flock_id": flockID, -// "area_id": areaID, -// "category": "growing", -// "fcr_id": fcrID, -// "location_id": locationID, -// "kandang_ids": []uint{kandangID}, -// } - -// resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload) -// if resp.StatusCode != fiber.StatusCreated { -// t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body)) -// } - -// var createResp struct { -// Data struct { -// Id uint `json:"id"` -// } `json:"data"` -// } -// if err := json.Unmarshal(body, &createResp); err != nil { -// t.Fatalf("failed to parse create response: %v", err) -// } - -// searchTerms := []string{ -// "Flock Search Target", -// "Area Search Target", -// string(utils.ProjectFlockCategoryGrowing), -// "growing", -// "FCR Search Target", -// "Kandang Search Target", -// "Location Search Target", -// "Location Address Target", -// "Tester", -// "1", -// } - -// for _, term := range searchTerms { -// path := "/api/production/project_flocks?search=" + url.QueryEscape(term) -// resp, body := doJSONRequest(t, app, http.MethodGet, path, nil) -// if resp.StatusCode != fiber.StatusOK { -// t.Fatalf("expected 200 when searching for %q, got %d: %s", term, resp.StatusCode, string(body)) -// } - -// var listResp struct { -// Data []struct { -// Id uint `json:"id"` -// } `json:"data"` -// Meta struct { -// TotalResults int64 `json:"total_results"` -// } `json:"meta"` -// } -// if err := json.Unmarshal(body, &listResp); err != nil { -// t.Fatalf("failed to parse list response for %q: %v", term, err) -// } -// if listResp.Meta.TotalResults == 0 { -// t.Fatalf("expected at least one result when searching for %q", term) -// } -// if len(listResp.Data) == 0 { -// t.Fatalf("expected data when searching for %q", term) -// } -// if listResp.Data[0].Id != createResp.Data.Id { -// t.Fatalf("expected project flock id %d for search term %q, got %d", createResp.Data.Id, term, listResp.Data[0].Id) -// } -// } -// } - -// func TestProjectFlockSorting(t *testing.T) { -// app, _ := setupIntegrationApp(t) - -// areaA := createArea(t, app, "Area Alpha") -// areaB := createArea(t, app, "Area Beta") - -// locationA := createLocation(t, app, "Location Alpha", "Address Alpha", areaA) -// locationB := createLocation(t, app, "Location Beta", "Address Beta", areaB) - -// flockOne := createFlock(t, app, "Flock Sort One") -// flockTwo := createFlock(t, app, "Flock Sort Two") - -// fcrID := createFcr(t, app, "FCR Sort", []map[string]any{ -// {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, -// }) - -// kandangOne := createKandang(t, app, "Kandang Sort One", locationA, 1) -// kandangTwo := createKandang(t, app, "Kandang Sort Two", locationB, 1) -// kandangThree := createKandang(t, app, "Kandang Sort Three", locationB, 1) - -// projectOnePayload := map[string]any{ -// "flock_id": flockOne, -// "area_id": areaA, -// "category": "growing", -// "fcr_id": fcrID, -// "location_id": locationA, -// "kandang_ids": []uint{kandangOne}, -// } -// resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload) -// if resp.StatusCode != fiber.StatusCreated { -// t.Fatalf("expected 201 for project one, got %d: %s", resp.StatusCode, string(body)) -// } -// projectOneID := parseProjectFlockID(t, body) - -// projectTwoPayload := map[string]any{ -// "flock_id": flockTwo, -// "area_id": areaB, -// "category": "laying", -// "fcr_id": fcrID, -// "location_id": locationB, -// "kandang_ids": []uint{kandangTwo, kandangThree}, -// } -// resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload) -// if resp.StatusCode != fiber.StatusCreated { -// t.Fatalf("expected 201 for project two, got %d: %s", resp.StatusCode, string(body)) -// } -// projectTwoID := parseProjectFlockID(t, body) - -// updatePeriodPayload := map[string]any{"period": 5} -// resp, body = doJSONRequest(t, app, http.MethodPatch, "/api/production/project_flocks/"+uintToString(projectTwoID), updatePeriodPayload) -// if resp.StatusCode != fiber.StatusOK { -// t.Fatalf("expected 200 when updating period, got %d: %s", resp.StatusCode, string(body)) -// } - -// assertOrder := func(t *testing.T, app *fiber.App, query string, expectedFirst uint) { -// t.Helper() -// resp, body := doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks?"+query, nil) -// if resp.StatusCode != fiber.StatusOK { -// t.Fatalf("expected 200 for query %q, got %d: %s", query, resp.StatusCode, string(body)) -// } -// var listResp struct { -// Data []struct { -// Id uint `json:"id"` -// } `json:"data"` -// } -// if err := json.Unmarshal(body, &listResp); err != nil { -// t.Fatalf("failed to parse list response for %q: %v", query, err) -// } -// if len(listResp.Data) == 0 { -// t.Fatalf("expected data for query %q", query) -// } -// if listResp.Data[0].Id != expectedFirst { -// t.Fatalf("expected first id %d for query %q, got %d", expectedFirst, query, listResp.Data[0].Id) -// } -// } - -// assertOrder(t, app, "sort_by=area&sort_order=asc", projectOneID) -// assertOrder(t, app, "sort_by=location&sort_order=desc", projectTwoID) -// assertOrder(t, app, "sort_by=period&sort_order=desc", projectTwoID) -// assertOrder(t, app, "sort_by=kandangs&sort_order=desc", projectTwoID) -// assertOrder(t, app, "sort_by=kandangs&sort_order=asc", projectOneID) -// } - -// func parseProjectFlockID(t *testing.T, body []byte) uint { -// t.Helper() -// var resp struct { -// Data struct { -// Id uint `json:"id"` -// } `json:"data"` -// } -// if err := json.Unmarshal(body, &resp); err != nil { -// t.Fatalf("failed to parse project flock response: %v", err) -// } -// return resp.Data.Id -// } diff --git a/test/integration/master_data/supplier_test.go b/test/integration/master_data/supplier_test.go deleted file mode 100644 index 2a53d9a5..00000000 --- a/test/integration/master_data/supplier_test.go +++ /dev/null @@ -1,238 +0,0 @@ -package test - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - "testing" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/utils" -) - -func TestSupplierIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - - t.Run("creating supplier with invalid type fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/suppliers", map[string]any{ - "name": "Invalid Supplier", - "alias": "inv01", - "pic": "Jane Doe", - "type": "random-type", - "category": utils.SupplierCategoryBOP, - "hatchery": "Hatchery X", - "phone": "081234567891", - "email": "invalid@supplier.com", - "address": "Somewhere", - "npwp": "NPWP-INVALID", - "account_number": "ACC-INVALID", - "due_date": 30, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when type is invalid, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("creating supplier with invalid category fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/suppliers", map[string]any{ - "name": "Invalid Category Supplier", - "alias": "cat01", - "pic": "Jane Doe", - "type": utils.CustomerSupplierTypeBisnis, - "category": "invalid", - "phone": "081234567892", - "email": "invalid-category@supplier.com", - "address": "Somewhere", - "due_date": 30, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when category is invalid, got %d: %s", resp.StatusCode, string(body)) - } - }) - - const supplierName = "Supplier Alpha" - const alias = "al001" - var supplierID uint - - t.Run("creating supplier succeeds", func(t *testing.T) { - supplierID = createSupplier(t, app, supplierName, alias, string(utils.SupplierCategoryBOP)) - supplier := fetchSupplier(t, db, supplierID) - if supplier.Name != supplierName { - t.Fatalf("expected name %q, got %q", supplierName, supplier.Name) - } - if supplier.Alias != strings.ToUpper(alias) { - t.Fatalf("expected alias %q, got %q", strings.ToUpper(alias), supplier.Alias) - } - if supplier.Type != string(utils.CustomerSupplierTypeBisnis) { - t.Fatalf("expected type %q, got %q", utils.CustomerSupplierTypeBisnis, supplier.Type) - } - if supplier.Category != string(utils.SupplierCategoryBOP) { - t.Fatalf("expected category %q, got %q", utils.SupplierCategoryBOP, supplier.Category) - } - if supplier.DueDate != 30 { - t.Fatalf("expected due date 30, got %d", supplier.DueDate) - } - if supplier.CreatedUser.Id != 1 { - t.Fatalf("expected created user id 1, got %d", supplier.CreatedUser.Id) - } - }) - - t.Run("creating supplier with duplicate name fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/suppliers", map[string]any{ - "name": supplierName, - "alias": "dup01", - "pic": "Jane Doe", - "type": utils.CustomerSupplierTypeBisnis, - "category": utils.SupplierCategoryBOP, - "phone": "0811111111", - "email": "duplicate@supplier.com", - "address": "Duplicate address", - "due_date": 15, - }) - if resp.StatusCode != fiber.StatusConflict { - t.Fatalf("expected 409 when creating duplicate supplier, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("getting existing supplier succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when fetching supplier, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Id uint `json:"id"` - Name string `json:"name"` - Alias string `json:"alias"` - Type string `json:"type"` - Category string `json:"category"` - CreatedUser *struct { - Id uint `json:"id"` - } `json:"created_user"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse supplier response: %v", err) - } - if payload.Data.Id != supplierID { - t.Fatalf("expected id %d, got %d", supplierID, payload.Data.Id) - } - if payload.Data.Name != supplierName { - t.Fatalf("expected name %q, got %q", supplierName, payload.Data.Name) - } - if payload.Data.Alias != strings.ToUpper(alias) { - t.Fatalf("expected alias %q, got %q", strings.ToUpper(alias), payload.Data.Alias) - } - if payload.Data.Type != string(utils.CustomerSupplierTypeBisnis) { - t.Fatalf("expected type %q, got %q", utils.CustomerSupplierTypeBisnis, payload.Data.Type) - } - if payload.Data.Category != string(utils.SupplierCategoryBOP) { - t.Fatalf("expected category %q, got %q", utils.SupplierCategoryBOP, payload.Data.Category) - } - if payload.Data.CreatedUser == nil || payload.Data.CreatedUser.Id != 1 { - t.Fatalf("expected created_user id 1, got %+v", payload.Data.CreatedUser) - } - }) - - const updatedAlias = "beta1" - - t.Run("updating supplier fields succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), map[string]any{ - "alias": updatedAlias, - "type": utils.CustomerSupplierTypeIndividual, - "category": utils.SupplierCategorySapronak, - "due_date": 45, - }) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when updating supplier, got %d: %s", resp.StatusCode, string(body)) - } - var payload struct { - Data struct { - Alias string `json:"alias"` - Type string `json:"type"` - Category string `json:"category"` - } `json:"data"` - } - if err := json.Unmarshal(body, &payload); err != nil { - t.Fatalf("failed to parse update response: %v", err) - } - if payload.Data.Alias != strings.ToUpper(updatedAlias) { - t.Fatalf("expected alias %q, got %q", strings.ToUpper(updatedAlias), payload.Data.Alias) - } - if payload.Data.Type != string(utils.CustomerSupplierTypeIndividual) { - t.Fatalf("expected type %q, got %q", utils.CustomerSupplierTypeIndividual, payload.Data.Type) - } - if payload.Data.Category != string(utils.SupplierCategorySapronak) { - t.Fatalf("expected category %q, got %q", utils.SupplierCategorySapronak, payload.Data.Category) - } - supplier := fetchSupplier(t, db, supplierID) - if supplier.Alias != strings.ToUpper(updatedAlias) { - t.Fatalf("expected persisted alias %q, got %q", strings.ToUpper(updatedAlias), supplier.Alias) - } - if supplier.Type != string(utils.CustomerSupplierTypeIndividual) { - t.Fatalf("expected persisted type %q, got %q", utils.CustomerSupplierTypeIndividual, supplier.Type) - } - if supplier.Category != string(utils.SupplierCategorySapronak) { - t.Fatalf("expected persisted category %q, got %q", utils.SupplierCategorySapronak, supplier.Category) - } - if supplier.DueDate != 45 { - t.Fatalf("expected due date 45, got %d", supplier.DueDate) - } - }) - - t.Run("updating supplier with invalid type fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), map[string]any{ - "type": "invalid-type", - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when updating with invalid type, got %d: %s", resp.StatusCode, string(body)) - } - supplier := fetchSupplier(t, db, supplierID) - if supplier.Type != string(utils.CustomerSupplierTypeIndividual) { - t.Fatalf("expected type to remain %q after invalid update, got %q", utils.CustomerSupplierTypeIndividual, supplier.Type) - } - }) - - t.Run("updating supplier with invalid category fails", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), map[string]any{ - "category": "invalid", - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400 when updating with invalid category, got %d: %s", resp.StatusCode, string(body)) - } - supplier := fetchSupplier(t, db, supplierID) - if supplier.Category != string(utils.SupplierCategorySapronak) { - t.Fatalf("expected category to remain %q after invalid update, got %q", utils.SupplierCategorySapronak, supplier.Category) - } - }) - - t.Run("deleting supplier succeeds", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), nil) - if resp.StatusCode != fiber.StatusOK { - t.Fatalf("expected 200 when deleting supplier, got %d: %s", resp.StatusCode, string(body)) - } - var supplier entities.Supplier - if err := db.First(&supplier, supplierID).Error; !errors.Is(err, gorm.ErrRecordNotFound) { - t.Fatalf("expected supplier to be deleted, got error %v", err) - } - }) - - t.Run("deleting non existent supplier returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when deleting missing supplier, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("getting deleted supplier returns 404", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID), nil) - if resp.StatusCode != fiber.StatusNotFound { - t.Fatalf("expected 404 when fetching deleted supplier, got %d: %s", resp.StatusCode, string(body)) - } - }) -} diff --git a/test/integration/master_data/warehouse_test.go b/test/integration/master_data/warehouse_test.go deleted file mode 100644 index 72b267c3..00000000 --- a/test/integration/master_data/warehouse_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package test - -import ( - "net/http" - "testing" - - "github.com/gofiber/fiber/v2" -) - -func TestWarehouseIntegration(t *testing.T) { - app, db := setupIntegrationApp(t) - areaID := createArea(t, app, "Warehouse Area") - locationID := createLocation(t, app, "Location WH", "Addr", areaID) - kandangID := createKandang(t, app, "Kandang WH", locationID, 1) - - t.Run("type AREA only needs area_id", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{ - "name": "WH Area", - "type": "AREA", - "area_id": areaID, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("type AREA rejects location_id", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{ - "name": "WH Area Invalid", - "type": "AREA", - "area_id": areaID, - "location_id": locationID, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("type LOKASI requires location_id", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{ - "name": "WH Lokasi Fail", - "type": "LOKASI", - "area_id": areaID, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("type LOKASI success", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{ - "name": "WH Lokasi", - "type": "LOKASI", - "area_id": areaID, - "location_id": locationID, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body)) - } - whID := parseID(t, body) - wh := fetchWarehouse(t, db, whID) - if wh.LocationId == nil || *wh.LocationId != locationID { - t.Fatalf("expected location_id %d, got %v", locationID, wh.LocationId) - } - }) - - t.Run("type KANDANG requires all ids", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{ - "name": "WH Kandang Fail", - "type": "KANDANG", - "area_id": areaID, - "location_id": locationID, - }) - if resp.StatusCode != fiber.StatusBadRequest { - t.Fatalf("expected 400, got %d: %s", resp.StatusCode, string(body)) - } - }) - - t.Run("type KANDANG success", func(t *testing.T) { - resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/warehouses", map[string]any{ - "name": "WH Kandang", - "type": "KANDANG", - "area_id": areaID, - "location_id": locationID, - "kandang_id": kandangID, - }) - if resp.StatusCode != fiber.StatusCreated { - t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body)) - } - whID := parseID(t, body) - wh := fetchWarehouse(t, db, whID) - if wh.KandangId == nil || *wh.KandangId != kandangID { - t.Fatalf("expected kandang_id %d, got %v", kandangID, wh.KandangId) - } - }) -} diff --git a/tools/templates/dto.tmpl b/tools/templates/dto.tmpl index 39b92884..484ee0ae 100644 --- a/tools/templates/dto.tmpl +++ b/tools/templates/dto.tmpl @@ -9,7 +9,7 @@ import ( // === DTO Structs === -type {{Pascal .Entity}}BaseDTO struct { +type {{Pascal .Entity}}RelationDTO struct { Id uint `json:"id"` Name string `json:"name"` } @@ -17,7 +17,7 @@ type {{Pascal .Entity}}BaseDTO struct { type {{Pascal .Entity}}ListDTO struct { Id uint `json:"id"` Name string `json:"name"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } @@ -28,17 +28,17 @@ type {{Pascal .Entity}}DetailDTO struct { // === Mapper Functions === -func To{{Pascal .Entity}}BaseDTO(e entity.{{Pascal .Entity}}) {{Pascal .Entity}}BaseDTO { - return {{Pascal .Entity}}BaseDTO{ +func To{{Pascal .Entity}}RelationDTO(e entity.{{Pascal .Entity}}) {{Pascal .Entity}}RelationDTO { + return {{Pascal .Entity}}RelationDTO{ Id: e.Id, Name: e.Name, } } func To{{Pascal .Entity}}ListDTO(e entity.{{Pascal .Entity}}) {{Pascal .Entity}}ListDTO { - var createdUser *userDTO.UserBaseDTO + var createdUser *userDTO.UserRelationDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) createdUser = &mapped } From da10861fd2a468ee9eb85b8c4e900efb876d0733 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 20 Nov 2025 21:17:04 +0700 Subject: [PATCH 53/59] fix merging --- .../project_flock_kandang.controller.go | 19 ++-- .../dto/project_flock_kandang.dto.go | 92 +++++++------------ .../project-flock-kandangs/module.go | 4 +- .../services/project_flock_kandang.service.go | 61 +++--------- .../project_flocks/dto/projectflock.dto.go | 76 ++++----------- .../dto/projectflock_kandang.dto.go | 7 -- .../services/projectflock.service.go | 13 +-- .../purchases/services/purchase.service.go | 8 +- 8 files changed, 83 insertions(+), 197 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index f3ef7271..4b6e605a 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -4,7 +4,6 @@ import ( "math" "strconv" - flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" @@ -41,18 +40,14 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } - results, totalResults, flockMap, err := u.ProjectFlockKandangService.GetAll(c, query) + results, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) if err != nil { return err } - data := make([]dto.ProjectFlockKandangListDTO, 0) + data := make([]dto.ProjectFlockKandangListDTO, 0, len(results)) for _, result := range results { - var flock *flockDTO.FlockRelationDTO - if flockMap != nil { - flock = flockMap[result.ProjectFlock.Id] - } - data = append(data, dto.ToProjectFlockKandangListDTOWithFlock(result, flock)) + data = append(data, dto.ToProjectFlockKandangListDTO(result)) } return c.Status(fiber.StatusOK). @@ -71,14 +66,12 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { } func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { - param := c.Params("id") - - id, err := strconv.Atoi(param) + id, err := strconv.Atoi(c.Params("id")) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, availableQtys, productWarehouses, flock, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) + result, availableQtys, productWarehouses, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) if err != nil { return err } @@ -88,6 +81,6 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get projectFlockKandang successfully", - Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(*result, availableQtys, productWarehouses, flock), + Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses), }) } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 1cc0c1f5..677f527b 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -5,9 +5,10 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/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" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" @@ -25,10 +26,7 @@ type ProjectFlockKandangRelationDTO struct { } type ProjectFlockDTO struct { - Id uint `json:"id"` - Name string `json:"flock_name,omitempty"` - Period int `json:"period"` - Flock *flockDTO.FlockRelationDTO `json:"flock,omitempty"` + projectFlockDTO.ProjectFlockRelationDTO Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Category string `json:"category"` Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` @@ -38,14 +36,8 @@ type ProjectFlockDTO struct { UpdatedAt time.Time `json:"updated_at"` } -type KandangDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Status string `json:"status"` -} - type ProductWarehouseDTO struct { - Id uint `json:"id"` + productWarehouseDTO.ProductWarehouseRelationDTO Product *productDTO.ProductRelationDTO `json:"product,omitempty"` Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` } @@ -58,7 +50,7 @@ type AvailableQtyDTO struct { type ProjectFlockKandangListDTO struct { ProjectFlockKandangRelationDTO ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` - Kandang *KandangDTO `json:"kandang,omitempty"` + Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` @@ -86,25 +78,18 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO } return &ProjectFlockDTO{ - Id: pf.Id, - Name: pf.FlockName, - Period: pf.Period, - Flock: pf.Flock, - Area: pf.Area, - Category: pf.Category, - Fcr: pf.Fcr, - Location: pf.Location, - CreatedUser: pf.CreatedUser, - CreatedAt: pf.CreatedAt, - UpdatedAt: pf.UpdatedAt, + ProjectFlockRelationDTO: pf.ProjectFlockRelationDTO, + Area: pf.Area, + Category: pf.Category, + Fcr: pf.Fcr, + Location: pf.Location, + CreatedUser: pf.CreatedUser, + CreatedAt: pf.CreatedAt, + UpdatedAt: pf.UpdatedAt, } } func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse) ProjectFlockKandangDetailDTO { - return ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e, availableQtyMap, productWarehouses, nil) -} - -func ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse, flock *flockDTO.FlockRelationDTO) ProjectFlockKandangDetailDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) @@ -114,7 +99,7 @@ func ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e entity.ProjectFloc listDTO := ProjectFlockKandangListDTO{ ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e), ProjectFlock: toProjectFlockDTO(projectFlockSummary), - Kandang: toKandangDTO(e.Kandang), + Kandang: toKandangRelation(e.Kandang), CreatedAt: e.CreatedAt, CreatedUser: toCreatedUserDTO(e.ProjectFlock), Approval: toApprovalDTO(e), @@ -127,29 +112,15 @@ func ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e entity.ProjectFloc } } -func toKandangDTO(kandang entity.Kandang) *KandangDTO { +func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO { if kandang.Id == 0 { return nil } - return &KandangDTO{ - Id: kandang.Id, - Name: kandang.Name, - Status: kandang.Status, - } + mapped := kandangDTO.ToKandangRelationDTO(kandang) + return &mapped } -// func toFlockDTO(flock *entity.Flock) *flockDTO.FlockRelationDTO { -// if flock == nil || flock.Id == 0 { -// return nil -// } - -// return &flockDTO.FlockRelationDTO{ -// Id: flock.Id, -// Name: flock.Name, -// } -// } - func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalRelationDTO { if e.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) @@ -159,20 +130,16 @@ func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalRelationDT } func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKandangListDTO { - return ToProjectFlockKandangListDTOWithFlock(e, nil) -} - -func ToProjectFlockKandangListDTOWithFlock(e entity.ProjectFlockKandang, flock *flockDTO.FlockRelationDTO) ProjectFlockKandangListDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { - mapped := projectFlockDTO.ToProjectFlockListDTOWithFlock(e.ProjectFlock, flock) + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) projectFlockSummary = &mapped } return ProjectFlockKandangListDTO{ ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e), ProjectFlock: toProjectFlockDTO(projectFlockSummary), - Kandang: toKandangDTO(e.Kandang), + Kandang: toKandangRelation(e.Kandang), CreatedAt: e.CreatedAt, CreatedUser: toCreatedUserDTO(e.ProjectFlock), Approval: toApprovalDTO(e), @@ -255,14 +222,23 @@ func ToProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO { return nil } - chickinPwDTO := chickinDTO.ToProductWarehouseDTO(pw) - if chickinPwDTO == nil { - return nil + base := productWarehouseDTO.ToProductWarehouseRelationDTO(*pw) + + var product *productDTO.ProductRelationDTO + if pw.Product.Id != 0 { + mapped := productDTO.ToProductRelationDTO(pw.Product) + product = &mapped + } + + var warehouse *warehouseDTO.WarehouseRelationDTO + if pw.Warehouse.Id != 0 { + mapped := warehouseDTO.ToWarehouseRelationDTO(pw.Warehouse) + warehouse = &mapped } return &ProductWarehouseDTO{ - Id: chickinPwDTO.Id, - Product: chickinPwDTO.Product, - Warehouse: chickinPwDTO.Warehouse, + ProductWarehouseRelationDTO: base, + Product: product, + Warehouse: warehouse, } } diff --git a/internal/modules/production/project-flock-kandangs/module.go b/internal/modules/production/project-flock-kandangs/module.go index 3e7e8fa0..160cec5e 100644 --- a/internal/modules/production/project-flock-kandangs/module.go +++ b/internal/modules/production/project-flock-kandangs/module.go @@ -13,7 +13,6 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" - rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -29,7 +28,6 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB userRepo := rUser.NewUserRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) - flockRepo := rFlock.NewFlockRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) @@ -38,7 +36,7 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err)) } - projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, flockRepo, validate) + projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate) userService := sUser.NewUserService(userRepo, validate) ProjectFlockKandangRoutes(router, userService, projectFlockKandangService) diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 6e28a245..11e8b0d5 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -6,12 +6,9 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" - flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" - flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" - pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" @@ -21,8 +18,8 @@ import ( ) type ProjectFlockKandangService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockRelationDTO, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockRelationDTO, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) } // Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity @@ -35,10 +32,9 @@ type projectFlockKandangService struct { WarehouseRepo rWarehouse.WarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository PopulationRepo repository.ProjectFlockPopulationRepository - FlockRepo flockRepository.FlockRepository } -func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, flockRepo flockRepository.FlockRepository, validate *validator.Validate) ProjectFlockKandangService { +func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService { return &projectFlockKandangService{ Log: utils.Log, Validate: validate, @@ -47,13 +43,12 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, PopulationRepo: populationRepo, - FlockRepo: flockRepo, } } -func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockRelationDTO, error) { +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) { if err := s.Validate.Struct(params); err != nil { - return nil, 0, nil, err + return nil, 0, err } offset := (params.Page - 1) * params.Limit @@ -62,7 +57,7 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) - return nil, 0, nil, err + return nil, 0, err } if s.ApprovalSvc != nil { @@ -85,35 +80,16 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } } - flockMap := make(map[uint]*flockDTO.FlockRelationDTO) - for i := range projectFlockKandangs { - if projectFlockKandangs[i].ProjectFlock.Id != 0 { - baseName := pfutils.DeriveBaseName(projectFlockKandangs[i].ProjectFlock.FlockName) - if baseName != "" { - flock, err := s.FlockRepo.GetByName(c.Context(), baseName) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) - } else if flock != nil { - - flockMap[projectFlockKandangs[i].ProjectFlock.Id] = &flockDTO.FlockRelationDTO{ - Id: flock.Id, - Name: flock.Name, - } - } - } - } - } - - return projectFlockKandangs, total, flockMap, nil + return projectFlockKandangs, total, nil } -func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockRelationDTO, error) { +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, err } if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil { @@ -147,23 +123,8 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje productWarehouses = []entity.ProductWarehouse{} } } - var flockResult *flockDTO.FlockRelationDTO - if projectFlockKandang.ProjectFlock.Id != 0 { - baseName := pfutils.DeriveBaseName(projectFlockKandang.ProjectFlock.FlockName) - if baseName != "" { - flock, err := s.FlockRepo.GetByName(c.Context(), baseName) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) - } else if flock != nil { - flockResult = &flockDTO.FlockRelationDTO{ - Id: flock.Id, - Name: flock.Name, - } - } - } - } - return projectFlockKandang, availableQtyMap, productWarehouses, flockResult, nil + return projectFlockKandang, availableQtyMap, productWarehouses, nil } func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) (map[uint]float64, error) { diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index f2bc95df..8324dd71 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -7,7 +7,6 @@ import ( 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" @@ -25,7 +24,6 @@ type ProjectFlockRelationDTO struct { type ProjectFlockListDTO struct { ProjectFlockRelationDTO - Flock *flockDTO.FlockRelationDTO `json:"flock,omitempty"` Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Category string `json:"category"` Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` @@ -40,17 +38,13 @@ type ProjectFlockListDTO struct { type KandangWithProjectFlockIdDTO struct { kandangDTO.KandangRelationDTO ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + Period int `json:"period"` } type ProjectFlockDetailDTO struct { ProjectFlockListDTO } -type FlockPeriodDTO struct { - Flock flockDTO.FlockRelationDTO `json:"flock"` - NextPeriod int `json:"next_period"` -} - type KandangPeriodSummaryDTO struct { Id uint `json:"id"` Name string `json:"name"` @@ -69,16 +63,21 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs)) for i, kandang := range e.Kandangs { - var pfkId uint + var ( + pfkId uint + period int + ) for _, kh := range e.KandangHistory { if kh.KandangId == kandang.Id { pfkId = kh.Id + period = kh.Period break } } kandangSummaries[i] = KandangWithProjectFlockIdDTO{ KandangRelationDTO: kandangDTO.ToKandangRelationDTO(kandang), ProjectFlockKandangId: pfkId, + Period: period, } } } @@ -101,11 +100,6 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF locationSummary = &mapped } - // var flockSummary *flockDTO.FlockRelationDTO - // if flock != nil && flock.Id != 0 { - // flockSummary = flock - // } - latestApproval := defaultProjectFlockLatestApproval(e) if e.LatestApproval != nil { snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval) @@ -114,23 +108,18 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF return ProjectFlockListDTO{ ProjectFlockRelationDTO: createProjectFlockRelationDTO(e, period), - // Flock: flockSummary, - Area: areaSummary, - Kandangs: kandangSummaries, - Category: e.Category, - Fcr: fcrSummary, - Location: locationSummary, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Approval: latestApproval, + Area: areaSummary, + Kandangs: kandangSummaries, + Category: e.Category, + Fcr: fcrSummary, + Location: locationSummary, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Approval: latestApproval, } } -func ToProjectFlockListDTOWithFlock(e entity.ProjectFlock, flock *flockDTO.FlockRelationDTO) ProjectFlockListDTO { - return ToProjectFlockListDTO(e) -} - func ToProjectFlockListDTOs(items []entity.ProjectFlock) []ProjectFlockListDTO { result := make([]ProjectFlockListDTO, len(items)) for i, item := range items { @@ -157,24 +146,7 @@ func ToProjectFlockListDTOsWithPeriods(items []entity.ProjectFlock, periods map[ return result } -func ToProjectFlockListDTOsWithFlocks(items []entity.ProjectFlock, flocks map[uint]*entity.Flock) []ProjectFlockListDTO { - result := make([]ProjectFlockListDTO, len(items)) - for i, item := range items { - var flock *flockDTO.FlockRelationDTO - if flocks != nil { - if f := flocks[item.Id]; f != nil { - flock = &flockDTO.FlockRelationDTO{ - Id: f.Id, - Name: f.Name, - } - } - } - result[i] = ToProjectFlockListDTOWithFlock(item, flock) - } - return result -} - -func ToProjectFlockDetailDTO(e entity.ProjectFlock, flock *flockDTO.FlockRelationDTO) ProjectFlockDetailDTO { +func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO { return ProjectFlockDetailDTO{ ProjectFlockListDTO: ToProjectFlockListDTOWithPeriod(e, 0), } @@ -212,17 +184,3 @@ func createProjectFlockRelationDTO(e entity.ProjectFlock, period int) ProjectFlo FlockName: e.FlockName, } } - -func ToFlockSummaryDTO(e entity.Flock) flockDTO.FlockRelationDTO { - return flockDTO.FlockRelationDTO{ - 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/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 416c7821..d5ed5a46 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -4,10 +4,8 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" 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" - pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -18,7 +16,6 @@ type KandangWithPivotDTO struct { type ProjectFlockWithPivotDTO struct { ProjectFlockRelationDTO - Flock *flockDTO.FlockRelationDTO `json:"flock,omitempty"` Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Category string `json:"category"` Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` @@ -56,10 +53,6 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD Category: e.ProjectFlock.Category, } - if base := pfutils.DeriveBaseName(e.ProjectFlock.FlockName); base != "" { - summary := flockDTO.FlockRelationDTO{Id: 0, Name: base} - pfLocal.Flock = &summary - } if e.ProjectFlock.Area.Id != 0 { mapped := areaDTO.ToAreaRelationDTO(e.ProjectFlock.Area) pfLocal.Area = &mapped diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index f6fa2638..7e3b94cb 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -10,7 +10,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" + // authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" @@ -1099,9 +1099,10 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka } func actorIDFromContext(c *fiber.Ctx) (uint, error) { - user, ok := authmiddleware.AuthenticatedUser(c) - if !ok || user == nil || user.Id == 0 { - return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - } - return user.Id, nil + // user, ok := authmiddleware.AuthenticatedUser(c) + // if !ok || user == nil || user.Id == 0 { + // return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + // } + // return user.Id, nil + return 1,nil } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 7f694a12..5035cbc6 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -11,6 +11,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" 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" rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" @@ -167,6 +168,11 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase return nil, err } + user, ok := authmiddleware.AuthenticatedUser(c) + if !ok || user == nil || user.Id == 0 { + return nil, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } + ctx := c.Context() if _, err := s.SupplierRepo.GetByID(ctx, req.SupplierID, nil); err != nil { @@ -257,7 +263,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase DueDate: dueDate, GrandTotal: 0, Notes: req.Notes, - CreatedBy: 1, // TODO: replace with authenticated user id once available + CreatedBy: uint64(user.Id), } items := make([]*entity.PurchaseItem, 0, len(aggregated)) From b8d1268dfae338939aa1a828435cb5f7085122e9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 21 Nov 2025 00:55:02 +0700 Subject: [PATCH 54/59] Feat[BE-261]: creating multiple Approval API, Update API, Delete API and some logic Adjustment --- ...0251117034511_create_expenses_table.up.sql | 2 + internal/entities/expense.go | 28 +- internal/entities/expense_nonstock.go | 6 +- .../controllers/expense.controller.go | 156 +++- internal/modules/expenses/dto/expense.dto.go | 290 +++---- internal/modules/expenses/route.go | 6 +- .../expenses/services/expense.service.go | 787 +++++++++++------- .../validations/expense.validation.go | 72 +- 8 files changed, 797 insertions(+), 550 deletions(-) diff --git a/internal/database/migrations/20251117034511_create_expenses_table.up.sql b/internal/database/migrations/20251117034511_create_expenses_table.up.sql index 819e25f4..8949d931 100644 --- a/internal/database/migrations/20251117034511_create_expenses_table.up.sql +++ b/internal/database/migrations/20251117034511_create_expenses_table.up.sql @@ -7,7 +7,9 @@ CREATE TABLE expenses ( ), po_number VARCHAR(50) NULL, document_path JSON, + realization_document_path JSON, expense_date DATE NOT NULL, + realization_date DATE, grand_total NUMERIC(15, 3) DEFAULT 0, note TEXT, created_by BIGINT, diff --git a/internal/entities/expense.go b/internal/entities/expense.go index b665a599..74998e6a 100644 --- a/internal/entities/expense.go +++ b/internal/entities/expense.go @@ -8,19 +8,21 @@ import ( ) type Expense struct { - Id uint64 `gorm:"primaryKey;autoIncrement"` - ReferenceNumber *string `gorm:"type:varchar(50);uniqueIndex"` - SupplierId *uint64 `gorm:""` - Category string `gorm:"type:varchar(50);not null"` - PoNumber *string `gorm:"type:varchar(50)"` - DocumentPath sql.NullString `gorm:"type:json"` - ExpenseDate time.Time `gorm:"type:date;not null"` - GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` - Note *string `gorm:"type:text"` - CreatedBy *uint64 `gorm:""` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint64 `gorm:"primaryKey;autoIncrement"` + ReferenceNumber string `gorm:"type:varchar(50);uniqueIndex"` + SupplierId uint64 `gorm:""` + Category string `gorm:"type:varchar(50);not null"` + PoNumber string `gorm:"type:varchar(50)"` + DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan + RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi + RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi + ExpenseDate time.Time `gorm:"type:date;not null"` + GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` + Note string `gorm:"type:text"` + CreatedBy uint64 `gorm:""` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // Relations Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"` diff --git a/internal/entities/expense_nonstock.go b/internal/entities/expense_nonstock.go index ae2d02fe..d483e4bf 100644 --- a/internal/entities/expense_nonstock.go +++ b/internal/entities/expense_nonstock.go @@ -10,11 +10,12 @@ type ExpenseNonstock struct { Id uint64 `gorm:"primaryKey;autoIncrement"` ExpenseId *uint64 `gorm:""` ProjectFlockKandangId *uint64 `gorm:""` + KandangId *uint64 `gorm:""` NonstockId *uint64 `gorm:""` Qty float64 `gorm:"type:numeric(15,3);not null"` UnitPrice float64 `gorm:"type:numeric(15,3);not null"` TotalPrice float64 `gorm:"type:numeric(15,3);not null"` - Note *string `gorm:"type:text"` + Note string `gorm:"type:text"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` @@ -22,6 +23,7 @@ type ExpenseNonstock struct { // Relations Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"` Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"` - Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` + Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` } diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go index 2d0bebac..08256b24 100644 --- a/internal/modules/expenses/controllers/expense.controller.go +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -103,11 +103,23 @@ func (u *ExpenseController) CreateOne(c *fiber.Ctx) error { var singleCostPerKandang validation.CostPerKandang if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err)) + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandangs JSON: %v", err)) + } + + if singleCostPerKandang.KandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required") } req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang} + } else { + for i, costPerKandang := range req.CostPerKandangs { + if costPerKandang.KandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandangs[%d]", i)) + } + } } + } else { + return fiber.NewError(fiber.StatusBadRequest, "Field cost_per_kandangs is required") } result, err := u.ExpenseService.CreateOne(c, req) @@ -133,8 +145,30 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + form, err := c.MultipartForm() + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") + } + + req.Documents = form.File["documents"] + if transactionDate := c.FormValue("transaction_date"); transactionDate != "" { + req.TransactionDate = &transactionDate + } + + costPerKandangJSON := c.FormValue("cost_per_kandang") + if costPerKandangJSON != "" { + var costPerKandang []validation.CostPerKandang + if err := json.Unmarshal([]byte(costPerKandangJSON), &costPerKandang); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err)) + } + + for i, costPerKandang := range costPerKandang { + if costPerKandang.KandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandang[%d]", i)) + } + } + + req.CostPerKandang = &costPerKandang } result, err := u.ExpenseService.UpdateOne(c, req, uint(id)) @@ -171,42 +205,45 @@ func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error { }) } -func (u *ExpenseController) ApproveExpense(c *fiber.Ctx) error { - expenseID := c.Params("id") - id, err := strconv.Atoi(expenseID) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") - } +func (u *ExpenseController) Approval(c *fiber.Ctx) error { + req := new(validation.ApprovalRequest) - // Extract step from URL path (manager or finance) - path := c.Path() - var step string - if strings.Contains(path, "/approvals/manager") { - step = "Manager" - } else if strings.Contains(path, "/approvals/finance") { - step = "Finance" - } else { - return fiber.NewError(fiber.StatusBadRequest, "Invalid approval step") - } - - // Parse approval request - var req validation.ApprovalRequest - if err := c.BodyParser(&req); err != nil { + if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - // Approve expense - expense, err := u.ExpenseService.ApproveExpense(c, uint(id), step, req.Action, req.Notes) + path := c.Path() + approvalType := "" + if strings.Contains(path, "/approvals/manager") { + approvalType = "manager" + } else if strings.Contains(path, "/approvals/finance") { + approvalType = "finance" + } else { + return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path") + } + + results, err := u.ExpenseService.Approval(c, req, approvalType) if err != nil { return err } + var ( + data interface{} + message = "Submit expense approval successfully" + ) + if len(results) == 1 { + data = results[0] + } else { + message = "Submit expense approvals successfully" + data = results + } + return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", - Message: "Approve expense successfully", - Data: expense, + Message: message, + Data: data, }) } @@ -224,8 +261,8 @@ func (u *ExpenseController) CreateRealization(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") } req.Documents = form.File["documents"] + req.RealizationDate = c.FormValue("realization_date") - // Parse realizations JSON realizationsJSON := c.FormValue("realizations") if realizationsJSON != "" { if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil { @@ -255,8 +292,21 @@ func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error { } var req validation.UpdateRealization - if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + + form, err := c.MultipartForm() + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") + } + + req.Documents = form.File["documents"] + + req.RealizationDate = c.FormValue("realization_date") + + realizationsJSON := c.FormValue("realizations") + if realizationsJSON != "" { + if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err)) + } } expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req) @@ -293,3 +343,49 @@ func (u *ExpenseController) CompleteExpense(c *fiber.Ctx) error { Data: expense, }) } + +func (u *ExpenseController) DeleteDocument(c *fiber.Ctx) error { + expenseID, err := strconv.Atoi(c.Params("id")) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") + } + + documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID") + } + + if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, false); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete document successfully", + }) +} + +func (u *ExpenseController) DeleteRealizationDocument(c *fiber.Ctx) error { + expenseID, err := strconv.Atoi(c.Params("id")) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") + } + + documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID") + } + + if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, true); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete realization document successfully", + }) +} diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index 70afd4d4..d87502ed 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -2,7 +2,6 @@ package dto import ( "encoding/json" - "fmt" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -13,21 +12,18 @@ import ( userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === Base DTO === - type ExpenseBaseDTO struct { - Id uint64 `json:"id"` - ReferenceNumber string `json:"reference_number"` - PoNumber *string `json:"po_number"` - Category string `json:"category"` - Documents []string `json:"documents,omitempty"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` + Id uint64 `json:"id"` + ReferenceNumber string `json:"reference_number"` + PoNumber string `json:"po_number"` + Category string `json:"category"` + Supplier *supplierDTO.SupplierBaseDTO `json:"supplier,omitempty"` + RealizationDate *time.Time `json:"realization_date,omitempty"` + ExpenseDate time.Time `json:"expense_date"` + GrandTotal float64 `json:"grand_total"` Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` } -// === List DTO (untuk GetAll) === - type ExpenseListDTO struct { ExpenseBaseDTO CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` @@ -36,95 +32,59 @@ type ExpenseListDTO struct { LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` } -// === Detail DTO (untuk GetOne) === - type ExpenseDetailDTO struct { ExpenseBaseDTO - Supplier *supplierDTO.SupplierBaseDTO `json:"supplier,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - Kandangs []KandangGroupDTO `json:"kandangs,omitempty"` - TotalPengajuan float64 `json:"total_pengajuan"` - TotalRealisasi float64 `json:"total_realisasi"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` + Documents []DocumentDTO `json:"documents,omitempty"` + RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + Kandangs []KandangGroupDTO `json:"kandangs,omitempty"` + TotalPengajuan float64 `json:"total_pengajuan"` + TotalRealisasi float64 `json:"total_realisasi"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` } -// === Nested DTO === - type ExpenseNonstockDTO struct { - Id uint64 `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` - Note *string `json:"note,omitempty"` - Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"` - ProjectFlockKandang *ProjectFlockKandangNestedDTO `json:"project_flock_kandang,omitempty"` - Realization *ExpenseRealizationDTO `json:"realization,omitempty"` -} - -type ProjectFlockKandangNestedDTO struct { - Id uint64 `json:"id"` - KandangId uint64 `json:"kandang_id"` + Id uint64 `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalPrice float64 `json:"total_price"` + Note *string `json:"note,omitempty"` + Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"` } type ExpenseRealizationDTO struct { - Id uint64 `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` - Date time.Time `json:"date"` - Note *string `json:"note,omitempty"` -} - -type RealizationOnlyDTO struct { - Id uint64 `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` - Date time.Time `json:"date"` - Note *string `json:"note,omitempty"` - Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"` - ProjectFlockKandang *ProjectFlockKandangNestedDTO `json:"project_flock_kandang,omitempty"` -} - -type KandangDTO struct { - Id uint64 `json:"id"` - KandangId uint64 `json:"kandang_id"` - Name string `json:"name,omitempty"` + Id uint64 `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalPrice float64 `json:"total_price"` + Note *string `json:"note,omitempty"` + Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"` } type KandangGroupDTO struct { - Id uint64 `json:"id"` - KandangId uint64 `json:"kandang_id"` - Name string `json:"name,omitempty"` - Pengajuans []ExpenseNonstockDTO `json:"pengajuans,omitempty"` - Realisasi []RealizationOnlyDTO `json:"realisasi,omitempty"` + Id uint64 `json:"id"` + KandangId uint64 `json:"kandang_id"` + Name string `json:"name,omitempty"` + Pengajuans []ExpenseNonstockDTO `json:"pengajuans,omitempty"` + Realisasi []ExpenseRealizationDTO `json:"realisasi,omitempty"` } -// === Helper Functions === - -func getStringValue(s *string) string { - if s == nil { - return "" - } - return *s +type DocumentDTO struct { + ID uint64 `json:"id"` + Path string `json:"path"` } -// === Mapper Functions === - func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO { - var documents []string var location *locationDTO.LocationBaseDTO + var supplier *supplierDTO.SupplierBaseDTO - // Parse document paths from JSON if available - if e.DocumentPath.Valid && e.DocumentPath.String != "" { - if err := json.Unmarshal([]byte(e.DocumentPath.String), &documents); err == nil { - // Successfully parsed documents - } + var realizationDate *time.Time + if !e.RealizationDate.IsZero() { + realizationDate = &e.RealizationDate } - // Get location from the first kandang if available if len(e.Nonstocks) > 0 && e.Nonstocks[0].ProjectFlockKandang != nil { if e.Nonstocks[0].ProjectFlockKandang.Kandang.Location.Id != 0 { mapped := locationDTO.ToLocationBaseDTO(e.Nonstocks[0].ProjectFlockKandang.Kandang.Location) @@ -132,12 +92,18 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO { } } + if e.Supplier != nil && e.Supplier.Id != 0 { + mapped := supplierDTO.ToSupplierBaseDTO(*e.Supplier) + supplier = &mapped + } + return ExpenseBaseDTO{ Id: e.Id, - ReferenceNumber: getStringValue(e.ReferenceNumber), - PoNumber: e.PoNumber, // Keep as pointer to allow null in JSON + ReferenceNumber: e.ReferenceNumber, + PoNumber: e.PoNumber, Category: e.Category, - Documents: documents, + Supplier: supplier, + RealizationDate: realizationDate, ExpenseDate: e.ExpenseDate, GrandTotal: e.GrandTotal, Location: location, @@ -175,18 +141,14 @@ func ToExpenseListDTOs(expenses []entity.Expense) []ExpenseListDTO { } func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { + var documents []DocumentDTO + var realizationDocs []DocumentDTO var createdUser *userDTO.UserBaseDTO if e.CreatedUser != nil && e.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } - var supplier *supplierDTO.SupplierBaseDTO - if e.Supplier != nil && e.Supplier.Id != 0 { - mapped := supplierDTO.ToSupplierBaseDTO(*e.Supplier) - supplier = &mapped - } - var latestApproval *approvalDTO.ApprovalBaseDTO if e.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) @@ -194,51 +156,45 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { } var pengajuans []ExpenseNonstockDTO - var realisasi []RealizationOnlyDTO + var realisasi []ExpenseRealizationDTO + + if e.DocumentPath.Valid && e.DocumentPath.String != "" { + json.Unmarshal([]byte(e.DocumentPath.String), &documents) + } + + if e.RealizationDocumentPath.Valid && e.RealizationDocumentPath.String != "" { + json.Unmarshal([]byte(e.RealizationDocumentPath.String), &realizationDocs) + } if len(e.Nonstocks) > 0 { pengajuans = make([]ExpenseNonstockDTO, 0) - realisasi = make([]RealizationOnlyDTO, 0) + realisasi = make([]ExpenseRealizationDTO, 0) for _, ns := range e.Nonstocks { - // Create DTO without realization for pengajuans pengajuanDTO := ToExpenseNonstockDTO(ns) - pengajuanDTO.Realization = nil // Remove realization from pengajuan + pengajuans = append(pengajuans, pengajuanDTO) - // Create separate DTO with realization data if it exists if ns.Realization != nil && ns.Realization.Id != 0 { - // Create realization DTO with only realization data var nonstock *nonstockDTO.NonstockBaseDTO if ns.Nonstock != nil && ns.Nonstock.Id != 0 { mapped := nonstockDTO.ToNonstockBaseDTO(*ns.Nonstock) nonstock = &mapped } - var projectFlockKandang *ProjectFlockKandangNestedDTO - if ns.ProjectFlockKandang != nil && ns.ProjectFlockKandang.Id != 0 { - projectFlockKandang = &ProjectFlockKandangNestedDTO{ - Id: uint64(ns.ProjectFlockKandang.Id), - KandangId: uint64(ns.ProjectFlockKandang.KandangId), - } - } - - realisasiDTO := RealizationOnlyDTO{ - Id: ns.Realization.Id, - Qty: ns.Realization.RealizationQty, - UnitPrice: ns.Realization.RealizationUnitPrice, - TotalPrice: ns.Realization.RealizationTotalPrice, - Date: ns.Realization.RealizationDate, - Note: ns.Realization.Note, - Nonstock: nonstock, - ProjectFlockKandang: projectFlockKandang, + realisasiDTO := ExpenseRealizationDTO{ + Id: ns.Realization.Id, + Qty: ns.Realization.RealizationQty, + UnitPrice: ns.Realization.RealizationUnitPrice, + TotalPrice: ns.Realization.RealizationTotalPrice, + Note: ns.Realization.Note, + Nonstock: nonstock, } realisasi = append(realisasi, realisasiDTO) } } } - // Calculate total pengajuan and realisasi var totalPengajuan float64 for _, p := range pengajuans { totalPengajuan += p.TotalPrice @@ -249,55 +205,76 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { totalRealisasi += r.TotalPrice } - // Group pengajuans and realisasi by kandang kandangMap := make(map[uint64]*KandangGroupDTO) - // Process pengajuans for _, p := range pengajuans { - if p.ProjectFlockKandang != nil { - kandangId := p.ProjectFlockKandang.KandangId + var kandangId uint64 + var kandangName string + + for _, ns := range e.Nonstocks { + if ns.Id == p.Id && ns.Kandang != nil { + kandangId = uint64(ns.Kandang.Id) + kandangName = ns.Kandang.Name + break + } + } + + if kandangId > 0 { if kandangMap[kandangId] == nil { kandangMap[kandangId] = &KandangGroupDTO{ - Id: p.ProjectFlockKandang.Id, - KandangId: kandangId, - Name: fmt.Sprintf("Kandang %d", kandangId), + Id: kandangId, + KandangId: kandangId, + Name: kandangName, + Pengajuans: []ExpenseNonstockDTO{}, + Realisasi: []ExpenseRealizationDTO{}, } } kandangMap[kandangId].Pengajuans = append(kandangMap[kandangId].Pengajuans, p) } } - // Process realisasi for _, r := range realisasi { - if r.ProjectFlockKandang != nil { - kandangId := r.ProjectFlockKandang.KandangId + var kandangId uint64 + var kandangName string + + for _, ns := range e.Nonstocks { + if ns.Realization != nil && ns.Realization.Id == r.Id && ns.Kandang != nil { + kandangId = uint64(ns.Kandang.Id) + kandangName = ns.Kandang.Name + break + } + } + + if kandangId > 0 { if kandangMap[kandangId] == nil { kandangMap[kandangId] = &KandangGroupDTO{ - Id: r.ProjectFlockKandang.Id, - KandangId: kandangId, - Name: fmt.Sprintf("Kandang %d", kandangId), + Id: kandangId, + KandangId: kandangId, + Name: kandangName, + Pengajuans: []ExpenseNonstockDTO{}, + Realisasi: []ExpenseRealizationDTO{}, } } kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r) } } - // Convert map to slice var kandangs []KandangGroupDTO for _, k := range kandangMap { kandangs = append(kandangs, *k) } return ExpenseDetailDTO{ - ExpenseBaseDTO: ToExpenseBaseDTO(e), - Supplier: supplier, - CreatedUser: createdUser, - Kandangs: kandangs, - TotalPengajuan: totalPengajuan, - TotalRealisasi: totalRealisasi, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - LatestApproval: latestApproval, + ExpenseBaseDTO: ToExpenseBaseDTO(e), + Documents: documents, + RealizationDocs: realizationDocs, + CreatedUser: createdUser, + Kandangs: kandangs, + TotalPengajuan: totalPengajuan, + TotalRealisasi: totalRealisasi, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + LatestApproval: latestApproval, } } @@ -308,39 +285,12 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO { nonstock = &mapped } - var projectFlockKandang *ProjectFlockKandangNestedDTO - if ns.ProjectFlockKandang != nil && ns.ProjectFlockKandang.Id != 0 { - projectFlockKandang = &ProjectFlockKandangNestedDTO{ - Id: uint64(ns.ProjectFlockKandang.Id), - KandangId: uint64(ns.ProjectFlockKandang.KandangId), - } - } - - var realization *ExpenseRealizationDTO - if ns.Realization != nil && ns.Realization.Id != 0 { - mapped := ToExpenseRealizationDTO(*ns.Realization) - realization = &mapped - } - return ExpenseNonstockDTO{ - Id: ns.Id, - Qty: ns.Qty, - UnitPrice: ns.UnitPrice, - TotalPrice: ns.TotalPrice, - Note: ns.Note, - Nonstock: nonstock, - ProjectFlockKandang: projectFlockKandang, - Realization: realization, - } -} - -func ToExpenseRealizationDTO(r entity.ExpenseRealization) ExpenseRealizationDTO { - return ExpenseRealizationDTO{ - Id: r.Id, - Qty: r.RealizationQty, - UnitPrice: r.RealizationUnitPrice, - TotalPrice: r.RealizationTotalPrice, - Date: r.RealizationDate, - Note: r.Note, + Id: ns.Id, + Qty: ns.Qty, + UnitPrice: ns.UnitPrice, + TotalPrice: ns.TotalPrice, + Note: &ns.Note, + Nonstock: nonstock, } } diff --git a/internal/modules/expenses/route.go b/internal/modules/expenses/route.go index eafa5b7c..805cb886 100644 --- a/internal/modules/expenses/route.go +++ b/internal/modules/expenses/route.go @@ -25,9 +25,11 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) - route.Post("/:id/approvals/manager", ctrl.ApproveExpense) - route.Post("/:id/approvals/finance", ctrl.ApproveExpense) + route.Post("/approvals/manager", ctrl.Approval) + route.Post("/approvals/finance", ctrl.Approval) route.Post("/:id/realizations", ctrl.CreateRealization) route.Patch("/:id/realizations", ctrl.UpdateRealization) route.Post("/:id/complete", ctrl.CompleteExpense) + route.Delete("/:id/documents/:documentId", ctrl.DeleteDocument) + route.Delete("/:id/realization-documents/:documentId", ctrl.DeleteRealizationDocument) } diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 3ed792fb..658537b7 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -2,9 +2,11 @@ package service import ( "context" + "database/sql" "encoding/json" "errors" "fmt" + "mime/multipart" "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" @@ -31,10 +33,11 @@ type ExpenseService interface { CreateOne(ctx *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*expenseDto.ExpenseDetailDTO, error) DeleteOne(ctx *fiber.Ctx, id uint) error - ApproveExpense(ctx *fiber.Ctx, id uint, step string, action string, notes *string) (*expenseDto.ExpenseDetailDTO, error) CreateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error) CompleteExpense(ctx *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) UpdateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) + DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error + Approval(ctx *fiber.Ctx, req *validation.ApprovalRequest, approvalType string) ([]expenseDto.ExpenseDetailDTO, error) } type expenseService struct { @@ -65,39 +68,14 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("Supplier"). - Preload("Nonstocks", func(db *gorm.DB) *gorm.DB { - return db.Preload("Nonstock").Preload("Realization").Preload("ProjectFlockKandang.Kandang.Location") - }) -} - -func (s expenseService) getExpenseWithDetails(c *fiber.Ctx, expenseId uint) (*expenseDto.ExpenseDetailDTO, error) { - expense, err := s.Repository.GetByID(c.Context(), expenseId, s.withRelations) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Expense not found for ID %d: %+v", expenseId, err) - return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") - } - s.Log.Errorf("Failed to get expense for ID %d: %+v", expenseId, err) - return nil, err - } - - // Load latest approval with ActionUser - latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseId, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - // Don't fail if approval loading fails, just log - s.Log.Warnf("Failed to load approval for expense %d: %+v", expenseId, err) - } - expense.LatestApproval = latestApproval - - responseDTO := expenseDto.ToExpenseDetailDTO(expense) - return &responseDTO, nil + Preload("Nonstocks.Nonstock"). + Preload("Nonstocks.Realization"). + Preload("Nonstocks.ProjectFlockKandang.Kandang.Location"). + Preload("Nonstocks.Kandang") } func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { - s.Log.Errorf("Validation failed for GetAll: %+v", err) return nil, 0, err } @@ -112,17 +90,16 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens }) if err != nil { - s.Log.Errorf("Failed to get expenses: %+v", err) + return nil, 0, err } - // Load approvals for each expense for i := range expenses { latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, uint(expenses[i].Id), func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { - s.Log.Warnf("Failed to load approval for expense %d: %+v", expenses[i].Id, err) + return nil, 0, err } expenses[i].LatestApproval = latestApproval } @@ -132,7 +109,26 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens } func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) { - return s.getExpenseWithDetails(c, id) + expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + + return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + return nil, err + } + + approval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + return nil, err + } + + expense.LatestApproval = approval + + responseDTO := expenseDto.ToExpenseDetailDTO(expense) + return &responseDTO, nil } func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error) { @@ -140,8 +136,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen return nil, err } - // Validate supplier exists using common service supplierID := uint(req.SupplierID) + supplierExistsFunc := func(ctx context.Context, id uint) (bool, error) { return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id) } @@ -195,6 +191,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen expenseRepoTx := repository.NewExpenseRepository(dbTransaction) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction) referenceNumber, err := s.generateReferenceNumber(dbTransaction) if err != nil { @@ -210,13 +207,13 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen createdBy := uint64(1) //todo get from auth expense = &entity.Expense{ - ReferenceNumber: &referenceNumber, + ReferenceNumber: referenceNumber, PoNumber: req.PoNumber, Category: req.Category, - SupplierId: &req.SupplierID, + SupplierId: req.SupplierID, ExpenseDate: expenseDate, GrandTotal: grandTotal, - CreatedBy: &createdBy, + CreatedBy: createdBy, } if err := expenseRepoTx.CreateOne(c.Context(), expense, nil); err != nil { @@ -227,31 +224,45 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen for _, costPerKandang := range req.CostPerKandangs { - projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction) - projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(costPerKandang.KandangID)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") - } + var projectFlockKandangId *uint64 - return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang") + if req.Category == "BOP" { + + projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(costPerKandang.KandangID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang") + } + id := uint64(projectFlockKandang.Id) + projectFlockKandangId = &id } for _, costItem := range costPerKandang.CostItems { - projectFlockKandangId := uint64(projectFlockKandang.Id) nonstockId := costItem.NonstockID + var kandangId *uint64 + if req.Category == "NON-BOP" { + id := uint64(costPerKandang.KandangID) + kandangId = &id + } else if req.Category == "BOP" { + if projectFlockKandangId != nil { + kandangId = &costPerKandang.KandangID + } + } + expenseNonstock := &entity.ExpenseNonstock{ ExpenseId: &expense.Id, - ProjectFlockKandangId: &projectFlockKandangId, + ProjectFlockKandangId: projectFlockKandangId, + KandangId: kandangId, NonstockId: &nonstockId, Qty: costItem.Quantity, TotalPrice: costItem.TotalCost, - Note: &costItem.Notes, + Note: costItem.Notes, } if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense cost item") } } @@ -268,36 +279,17 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen &approvalAction, createdByUint, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval") - } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval") + } - // Handle documents - save file paths as JSON array - if len(req.Documents) > 0 { - documentPaths := make([]string, len(req.Documents)) - for i, doc := range req.Documents { - // Generate dummy path for each document - // In real implementation, this would save the file and return the actual path - documentPath := fmt.Sprintf("/documents/expenses/%d_%d_%s", expense.Id, i+1, doc.Filename) - documentPaths[i] = documentPath - } - - // Save document paths as JSON in expense record - documentPathsJSON, err := json.Marshal(documentPaths) - if err != nil { - s.Log.Errorf("Failed to marshal document paths: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to save document references") - } - - if err := expenseRepoTx.PatchOne(c.Context(), uint(expense.Id), map[string]interface{}{ - "document_path": string(documentPathsJSON), - }, nil); err != nil { - s.Log.Errorf("Failed to save document paths: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to save document references") - } + if len(req.Documents) > 0 { + if err := s.processDocuments(c, expenseRepoTx, uint(expense.Id), req.Documents, false); err != nil { + return err } + } - return nil - }) + return nil + }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { @@ -306,16 +298,18 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense") } - return s.getExpenseWithDetails(c, uint(expense.Id)) + responseDTO, err := s.GetOne(c, uint(expense.Id)) + if err != nil { + return nil, err + } + return responseDTO, nil } func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*expenseDto.ExpenseDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { - s.Log.Errorf("Validation failed for UpdateOne: %+v", err) return nil, err } - // Validate expense exists using common service if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) @@ -324,41 +318,21 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return nil, err } - // Get latest approval to validate workflow latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Failed to get latest approval for expense %d: %+v", id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") } - // Check if expense can be updated (must be before Realisasi step) if latestApproval != nil { if latestApproval.StepNumber >= uint16(utils.ExpenseStepRealisasi) { currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] - s.Log.Errorf("Cannot update expense at %s step. Must be before Realisasi step", currentStepName) - return nil, fiber.NewError(fiber.StatusBadRequest, - fmt.Sprintf("Cannot update expense at %s step. Must be before Realisasi step", currentStepName)) + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot update expense at %s step. Must be before Realisasi step", currentStepName)) } } updateBody := make(map[string]any) - if req.SupplierID != nil { - // Validate supplier exists using common service - supplierID := uint(*req.SupplierID) - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: func(ctx context.Context, id uint) (bool, error) { - return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id) - }}, - ); err != nil { - return nil, err - } - - updateBody["supplier_id"] = *req.SupplierID - } - if req.TransactionDate != nil { - // Parse transaction_date expenseDate, err := utils.ParseDateString(*req.TransactionDate) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") @@ -366,40 +340,36 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody["expense_date"] = expenseDate } - if req.PoNumber != nil { - updateBody["po_number"] = *req.PoNumber + if len(updateBody) == 0 && req.CostPerKandang == nil && len(req.Documents) == 0 { + + responseDTO, err := s.GetOne(c, id) + if err != nil { + return nil, err + } + return responseDTO, nil } - if len(updateBody) == 0 && req.CostPerKandang == nil { - return s.getExpenseWithDetails(c, id) - } - - // Update expense using transaction err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + expenseRepoTx := repository.NewExpenseRepository(tx) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) - // Update expense if len(updateBody) > 0 { if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Expense not found") } - s.Log.Errorf("Failed to update expense: %+v", err) return err } } - // Update cost per kandang if provided if req.CostPerKandang != nil { - // First, delete existing expense nonstocks using GORM + if err := tx.Where("expense_id = ?", id).Delete(&entity.ExpenseNonstock{}).Error; err != nil { - s.Log.Errorf("Failed to delete existing expense nonstocks: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense items") } - // Calculate new grand total var grandTotal float64 for _, cpk := range *req.CostPerKandang { for _, costItem := range cpk.CostItems { @@ -407,28 +377,40 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } } - // Update expense grand total if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{ "grand_total": grandTotal, }, nil); err != nil { - s.Log.Errorf("Failed to update expense grand total: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense grand total") } - // Create new expense nonstocks for _, cpk := range *req.CostPerKandang { - // Get active project_flock_kandang for this kandang - projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) - projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID)) + var projectFlockKandangId *uint64 + + expense, err := expenseRepoTx.GetByID(c.Context(), id, nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") + return fiber.NewError(fiber.StatusNotFound, "Expense not found") } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang") + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense") + } + + if expense.Category == "BOP" { + + projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) + projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang") + } + id := uint64(projectFlockKandang.Id) + projectFlockKandangId = &id } for _, costItem := range cpk.CostItems { - // Validate nonstock exists + nonstockId := uint(costItem.NonstockID) if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Nonstock", ID: &nonstockId, Exists: s.NonstockRepo.IdExists}, @@ -436,46 +418,56 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return err } - projectFlockKandangId := uint64(projectFlockKandang.Id) + var kandangId *uint64 + if expense.Category == "NON-BOP" { + id := uint64(cpk.KandangID) + kandangId = &id + } else if expense.Category == "BOP" { + + if projectFlockKandangId != nil { + kandangId = &cpk.KandangID + } + } + expenseId := uint64(id) expenseNonstock := &entity.ExpenseNonstock{ ExpenseId: &expenseId, - ProjectFlockKandangId: &projectFlockKandangId, + ProjectFlockKandangId: projectFlockKandangId, + KandangId: kandangId, NonstockId: &costItem.NonstockID, Qty: costItem.Quantity, TotalPrice: costItem.TotalCost, - Note: &costItem.Notes, + Note: costItem.Notes, } if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { - s.Log.Errorf("Failed to create expense nonstock: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense cost item") } } } } - // Reset approval step to Pengajuan actorID := uint(1) // TODO: replace with authenticated user id - var approvalAction entity.ApprovalAction - - // Check if latest approval was Updated, then use Updated action, otherwise use Created - if latestApproval != nil && latestApproval.Action != nil && *latestApproval.Action == entity.ApprovalActionUpdated { - approvalAction = entity.ApprovalActionUpdated - } else { - approvalAction = entity.ApprovalActionCreated + if *latestApproval.Action != entity.ApprovalActionUpdated { + + approvalAction := entity.ApprovalActionUpdated + + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowExpense, + id, + utils.ExpenseStepPengajuan, + &approvalAction, + actorID, + nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step") + } } - - if _, err := approvalSvcTx.CreateApproval( - c.Context(), - utils.ApprovalWorkflowExpense, - id, - utils.ExpenseStepPengajuan, - &approvalAction, - actorID, - nil); err != nil { - s.Log.Errorf("Failed to reset approval to Pengajuan for expense %d: %+v", id, err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step") + + if len(req.Documents) > 0 { + if err := s.processDocuments(c, expenseRepoTx, id, req.Documents, false); err != nil { + return err + } } return nil @@ -488,11 +480,15 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense") } - return s.getExpenseWithDetails(c, id) + responseDTO, err := s.GetOne(c, id) + if err != nil { + return nil, err + } + return responseDTO, nil } func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error { - // Validate expense exists using common service + if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) @@ -513,109 +509,6 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error { return nil } -func (s *expenseService) ApproveExpense(c *fiber.Ctx, id uint, stepName string, action string, notes *string) (*expenseDto.ExpenseDetailDTO, error) { - - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { - return s.Repository.IdExists(ctx, uint64(id)) - }}, - ); err != nil { - return nil, err - } - - actorID := uint(1) // TODO: replace with authenticated user id - - var stepNumber approvalutils.ApprovalStep - switch stepName { - case "Manager": - stepNumber = utils.ExpenseStepManager - case "Finance": - stepNumber = utils.ExpenseStepFinance - default: - s.Log.Errorf("Invalid approval step: %s", stepName) - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid approval step") - } - - var expectedPreviousStep uint16 - switch stepName { - case "Manager": - expectedPreviousStep = uint16(utils.ExpenseStepPengajuan) - case "Finance": - expectedPreviousStep = uint16(utils.ExpenseStepManager) - } - - latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") - } - - if latestApproval == nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "No approval found. Please create expense first.") - } - - if latestApproval.StepNumber != expectedPreviousStep { - - expectedStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(expectedPreviousStep)] - currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] - - return nil, fiber.NewError(fiber.StatusBadRequest, - fmt.Sprintf("Cannot approve at step %s. Latest approval is at %s step. Expected previous step: %s", - stepName, currentStepName, expectedStepName)) - } - - var approvalAction entity.ApprovalAction - switch action { - case "APPROVED": - approvalAction = entity.ApprovalActionApproved - case "REJECTED": - approvalAction = entity.ApprovalActionRejected - default: - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid approval action") - } - - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { - - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) - expenseRepoTx := repository.NewExpenseRepository(tx) - - if _, err := approvalSvc.CreateApproval( - c.Context(), - utils.ApprovalWorkflowExpense, - id, - stepNumber, - &approvalAction, - actorID, - notes); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") - } - - if stepName == "Finance" && action == "APPROVED" { - poNumber, err := s.generatePoNumber(tx, id) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate PO number") - } - - updateData := map[string]interface{}{ - "po_number": poNumber, - } - if err := expenseRepoTx.PatchOne(c.Context(), id, updateData, nil); err != nil { - - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update PO number") - } - } - - return nil - }) - if err != nil { - if fiberErr, ok := err.(*fiber.Error); ok { - return nil, fiberErr - } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to Approve") - } - - return s.getExpenseWithDetails(c, id) -} - func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err @@ -628,7 +521,12 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va ); err != nil { return nil, err } - realizationDate := time.Now() + + realizationDate, err := utils.ParseDateString(req.RealizationDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format") + } + createdBy := uint64(1) // TODO: replace with authenticated user id if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { @@ -636,17 +534,14 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) realizationRepoTx := repository.NewExpenseRealizationRepository(tx) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) + expenseRepoTx := repository.NewExpenseRepository(tx) for _, realizationItem := range req.Realizations { expenseNonstockID := realizationItem.ExpenseNonstockID - belongsToExpense, err := expenseNonstockRepoTx.GetByExpenseID(c.Context(), uint64(expenseID), uint64(expenseNonstockID)) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate ExpenseNonstock relation") - } - if !belongsToExpense { - return fiber.NewError(fiber.StatusNotFound, "ExpenseNonstock not found or does not belong to this expense") + if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil { + return err } _, err = realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) @@ -671,6 +566,18 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va } } + if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{ + "realization_date": realizationDate, + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date") + } + + if len(req.Documents) > 0 { + if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil { + return err + } + } + approvalAction := entity.ApprovalActionCreated if _, err := approvalSvc.CreateApproval( c.Context(), @@ -689,11 +596,15 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va return nil, err } - return s.getExpenseWithDetails(c, expenseID) + responseDTO, err := s.GetOne(c, expenseID) + if err != nil { + return nil, err + } + return responseDTO, nil } func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) { - // Validate expense exists using common service + if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) @@ -704,27 +615,19 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) ( actorID := uint(1) // TODO: replace with authenticated user id - // Get latest approval to validate workflow latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Failed to get latest approval for expense %d: %+v", id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") } - - // Check if expense can be completed (must be at Realisasi step) if latestApproval == nil { - s.Log.Errorf("No approval found for expense %d", id) return nil, fiber.NewError(fiber.StatusBadRequest, "No approval found. Please create expense first.") } if latestApproval.StepNumber != uint16(utils.ExpenseStepRealisasi) { currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] - s.Log.Errorf("Cannot complete expense at step %s. Must be at Realisasi step", currentStepName) - return nil, fiber.NewError(fiber.StatusBadRequest, - fmt.Sprintf("Cannot complete expense at %s step. Must be at Realisasi step", currentStepName)) + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot complete expense at %s step. Must be at Realisasi step", currentStepName)) } - // Create approval for Selesai step (step 5) using transaction err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) approvalAction := entity.ApprovalActionApproved @@ -737,7 +640,7 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) ( &approvalAction, actorID, notes); err != nil { - s.Log.Errorf("Failed to create Selesai approval for expense %d: %+v", id, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to complete expense") } @@ -748,7 +651,11 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) ( return nil, err } - return s.getExpenseWithDetails(c, id) + responseDTO, err := s.GetOne(c, id) + if err != nil { + return nil, err + } + return responseDTO, nil } func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) { @@ -757,7 +664,6 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va return nil, err } - // Validate Expense exists using common service if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) @@ -766,46 +672,55 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va return nil, err } - // Use current date for realization date - realizationDate := time.Now() + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") + } + + if latestApproval != nil && latestApproval.StepNumber != uint16(utils.ExpenseStepRealisasi) { + currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] + return nil, fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Cannot update realization at %s step. Must be at Realisasi step", currentStepName)) + } + + var realizationDate *time.Time + if req.RealizationDate != "" { + parsedDate, err := utils.ParseDateString(req.RealizationDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format") + } + realizationDate = &parsedDate + } - // Update realizations using transaction if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + realizationRepoTx := repository.NewExpenseRealizationRepository(tx) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) + expenseRepoTx := repository.NewExpenseRepository(tx) - // Process each realization item for _, realizationItem := range req.Realizations { - // Validate ExpenseNonstock exists and belongs to this expense + expenseNonstockID := realizationItem.ExpenseNonstockID - belongsToExpense, err := expenseNonstockRepoTx.GetByExpenseID(c.Context(), uint64(expenseID), uint64(expenseNonstockID)) - if err != nil { - s.Log.Errorf("Failed to validate ExpenseNonstock relation: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate ExpenseNonstock relation") - } - if !belongsToExpense { - s.Log.Errorf("ExpenseNonstock not found or does not belong to expense %d", expenseID) - return fiber.NewError(fiber.StatusNotFound, "ExpenseNonstock not found or does not belong to this expense") + if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil { + return err } - // Get existing realization existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Realization not found for expense nonstock %d", expenseNonstockID) + return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock") } - s.Log.Errorf("Failed to get existing realization: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization") } - // Update realization updateData := map[string]interface{}{ "realization_qty": realizationItem.Qty, "realization_unit_price": realizationItem.UnitPrice, "realization_total_price": realizationItem.TotalPrice, - "realization_date": realizationDate, + "realization_date": *realizationDate, } if realizationItem.Notes != nil { @@ -818,13 +733,275 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va } } + if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{ + "realization_date": *realizationDate, + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date") + } + + if len(req.Documents) > 0 { + if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil { + return err + } + } + return nil }); err != nil { return nil, err } - // Return updated expense - return s.getExpenseWithDetails(c, expenseID) + responseDTO, err := s.GetOne(c, expenseID) + if err != nil { + return nil, err + } + return responseDTO, nil +} + +func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx repository.ExpenseRepository, expenseID uint, documents []*multipart.FileHeader, isRealization bool) error { + + if len(documents) == 0 { + return nil + } + + var existingDocuments []expenseDto.DocumentDTO + var fieldName string + + if isRealization { + fieldName = "realization_document_path" + } else { + fieldName = "document_path" + } + + expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil) + if err != nil { + + if !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document processing") + } + } else { + + var documentField sql.NullString + if isRealization { + documentField = expense.RealizationDocumentPath + } else { + documentField = expense.DocumentPath + } + + if documentField.Valid && documentField.String != "" { + if err := json.Unmarshal([]byte(documentField.String), &existingDocuments); err != nil { + existingDocuments = []expenseDto.DocumentDTO{} + } + } + } + + var startID uint64 = 1 + if len(existingDocuments) > 0 { + + maxID := uint64(0) + for _, doc := range existingDocuments { + if doc.ID > maxID { + maxID = doc.ID + } + } + startID = maxID + 1 + } + + for i, doc := range documents { + documentPath := doc.Filename + + document := expenseDto.DocumentDTO{ + ID: startID + uint64(i), + Path: documentPath, + } + existingDocuments = append(existingDocuments, document) + } + + documentJSON, err := json.Marshal(existingDocuments) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents") + } + + if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{ + fieldName: string(documentJSON), + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents") + } + + return nil +} + +func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error { + + if err := commonSvc.EnsureRelations(ctx.Context(), + commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { + return s.Repository.IdExists(ctx, uint64(id)) + }}, + ); err != nil { + return err + } + + if err := s.Repository.DB().WithContext(ctx.Context()).Transaction(func(tx *gorm.DB) error { + expenseRepoTx := repository.NewExpenseRepository(tx) + + expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document deletion") + } + + var existingDocuments []expenseDto.DocumentDTO + var fieldName string + + if isRealization { + fieldName = "realization_document_path" + if expense.RealizationDocumentPath.Valid && expense.RealizationDocumentPath.String != "" { + if err := json.Unmarshal([]byte(expense.RealizationDocumentPath.String), &existingDocuments); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing realization documents") + } + } + } else { + fieldName = "document_path" + if expense.DocumentPath.Valid && expense.DocumentPath.String != "" { + if err := json.Unmarshal([]byte(expense.DocumentPath.String), &existingDocuments); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing documents") + } + } + } + + var updatedDocuments []expenseDto.DocumentDTO + documentFound := false + + for _, doc := range existingDocuments { + if doc.ID == documentID { + documentFound = true + continue + } + updatedDocuments = append(updatedDocuments, doc) + } + + if !documentFound { + return fiber.NewError(fiber.StatusNotFound, "Document not found") + } + + documentJSON, err := json.Marshal(updatedDocuments) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents") + } + + if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{ + fieldName: string(documentJSON), + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents") + } + + return nil + }); err != nil { + return err + } + + return nil +} + +func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest, approvalType string) ([]expenseDto.ExpenseDetailDTO, error) { + if len(req.ApprovableIds) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "No expense IDs provided") + } + + actorID := uint(1) // TODO: replace with authenticated user id + + var results []expenseDto.ExpenseDetailDTO + + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + expenseRepoTx := repository.NewExpenseRepository(tx) + + for _, id := range req.ApprovableIds { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { + return s.Repository.IdExists(ctx, uint64(id)) + }}, + ); err != nil { + return err + } + + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") + } + + var stepNumber approvalutils.ApprovalStep + if approvalType == "manager" { + + stepNumber = utils.ExpenseStepManager + if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) { + currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] + return fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Cannot process at Manager step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName)) + } + } else if approvalType == "finance" { + + stepNumber = utils.ExpenseStepFinance + if latestApproval.StepNumber != uint16(utils.ExpenseStepManager) { + currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] + return fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Manager", currentStepName)) + } + } else { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid approval type: %v", approvalType)) + } + + var approvalAction entity.ApprovalAction + if req.Action == "APPROVED" { + approvalAction = entity.ApprovalActionApproved + } else if req.Action == "REJECTED" { + approvalAction = entity.ApprovalActionRejected + } else { + return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action") + } + + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowExpense, + id, + stepNumber, + &approvalAction, + actorID, + req.Notes); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") + } + + if stepNumber == utils.ExpenseStepFinance && req.Action == "APPROVED" { + poNumber, err := s.generatePoNumber(tx, id) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate PO number") + } + + updateData := map[string]interface{}{ + "po_number": poNumber, + } + if err := expenseRepoTx.PatchOne(c.Context(), id, updateData, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update PO number") + } + } + + responseDTO, err := s.GetOne(c, id) + if err != nil { + return err + } + results = append(results, *responseDTO) + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed approve expenses") + } + + return results, nil } func (s *expenseService) generateReferenceNumber(ctx *gorm.DB) (string, error) { @@ -845,11 +1022,29 @@ func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, if err != nil { return "", err } - if expense.ReferenceNumber == nil { - return "", errors.New("reference number is required") - } - - poNum := fmt.Sprintf("PO-%s", *expense.ReferenceNumber) + poNum := fmt.Sprintf("PO-%s", expense.ReferenceNumber) return poNum, nil } + +func (s *expenseService) validateExpenseNonstockRelation(ctx *fiber.Ctx, expenseNonstockRepoTx repository.ExpenseNonstockRepository, expenseID uint, expenseNonstockID uint64) error { + belongsToExpense, err := expenseNonstockRepoTx.GetByExpenseID(ctx.Context(), uint64(expenseID), expenseNonstockID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate ExpenseNonstock relation") + } + if !belongsToExpense { + return fiber.NewError(fiber.StatusNotFound, "ExpenseNonstock not found or does not belong to this expense") + } + return nil +} + +// func actorIDFromContext(c *fiber.Ctx) (uint, error) { +// user, ok := authmiddleware.AuthenticatedUser(c) +// if !ok || user == nil || user.Id == 0 { +// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") +// } +// return user.Id, nil +// } + +// return user.Id, nil +// } diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index 56420e06..4e909b66 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -4,39 +4,31 @@ import ( "mime/multipart" ) -// ApprovalRequest is used for expense approval endpoints -type ApprovalRequest struct { - Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` - Notes *string `json:"notes"` -} - type Create struct { - PoNumber *string `form:"po_number" validate:"omitempty,max=50"` - TransactionDate string `form:"transaction_date" validate:"required,datetime=2006-01-02"` - Category string `form:"category" validate:"required,oneof=BOP NON-BOP"` - SupplierID uint64 `form:"supplier_id" validate:"required,gt=0"` - Documents []*multipart.FileHeader `form:"documents" validate:"omitempty,dive"` - CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" validate:"required,min=1,dive"` + PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"` + TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"` + Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"` + SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"` + Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` + CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" json:"cost_per_kandangs" validate:"required,min=1,dive"` } type CostPerKandang struct { - KandangID uint64 `json:"kandang_id" form:"kandang_id" validate:"required,gt=0"` - CostItems []CostItem `json:"cost_items" form:"cost_items" validate:"required,min=1,dive"` + KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"` + CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"` } type CostItem struct { - NonstockID uint64 `json:"nonstock_id" form:"nonstock_id" validate:"required,gt=0"` - Quantity float64 `json:"quantity" form:"quantity" validate:"required,gt=0"` - TotalCost float64 `json:"total_cost" form:"total_cost" validate:"required,gt=0"` - Notes string `json:"notes" form:"notes" validate:"required,max=500"` + NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"` + Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"` + TotalCost float64 `form:"total_cost" json:"total_cost" validate:"required,gt=0"` + Notes string `form:"notes" json:"notes" validate:"required,max=500"` } type Update struct { - PoNumber *string `json:"po_number,omitempty" validate:"omitempty,max=50"` - TransactionDate *string `json:"transaction_date,omitempty" validate:"omitempty,datetime=2006-01-02"` - SupplierID *uint64 `json:"supplier_id,omitempty" validate:"omitempty,gt=0"` - Documents *[]string `json:"documents,omitempty" validate:"omitempty,dive,max=255"` - CostPerKandang *[]CostPerKandang `json:"cost_per_kandang,omitempty" validate:"omitempty,min=1,dive"` + TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"` + CostPerKandang *[]CostPerKandang `form:"cost_per_kandang" json:"cost_per_kandang" validate:"omitempty,min=1,dive"` + Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` } type Query struct { @@ -46,21 +38,27 @@ type Query struct { } type CreateRealization struct { - Documents []*multipart.FileHeader `form:"documents" validate:"omitempty,dive"` - Realizations []RealizationItem `json:"realizations" form:"realizations" validate:"required,min=1,dive"` -} - -type RealizationItem struct { - ExpenseNonstockID uint64 `json:"expense_nonstock_id" form:"expense_nonstock_id" validate:"required,gt=0"` - Qty float64 `json:"qty" form:"qty" validate:"required,gt=0"` - UnitPrice float64 `json:"unit_price" form:"unit_price" validate:"required,gt=0"` - TotalPrice float64 `json:"total_price" form:"total_price" validate:"required,gt=0"` - Notes *string `json:"notes" form:"notes" validate:"omitempty,max=500"` -} - -type CompleteExpense struct { + RealizationDate string `form:"realization_date" json:"realization_date" validate:"required,datetime=2006-01-02"` + Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` + Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"` } type UpdateRealization struct { - Realizations []RealizationItem `json:"realizations" validate:"required,min=1,dive"` + RealizationDate string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"` + Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` + Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"` +} + +type RealizationItem struct { + ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"` + Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"` + UnitPrice float64 `form:"unit_price" json:"unit_price" validate:"required,gt=0"` + TotalPrice float64 `form:"total_price" json:"total_price" validate:"required,gt=0"` + Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"` +} + +type ApprovalRequest struct { + Action string `json:"action" form:"action" validate:"required,oneof=APPROVED REJECTED"` + ApprovableIds []uint `json:"approvable_ids" validate:"required,min=1,dive,gt=0"` + Notes *string `json:"notes" form:"notes"` } From 5a73ad0164139e347e9f3663df1420c19096426c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 21 Nov 2025 01:06:48 +0700 Subject: [PATCH 55/59] Fix[BE-261]: delete timestampz on expense nonstock anda expense reaalizations table --- ...4529_create_expense_nonstocks_table.up.sql | 9 ++----- ...8_create_expense_realizations_table.up.sql | 9 ++----- internal/entities/expense_nonstock.go | 27 +++++++------------ internal/entities/expense_realization.go | 21 ++++++--------- 4 files changed, 21 insertions(+), 45 deletions(-) diff --git a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql index 330c4d7b..83542bb5 100644 --- a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql +++ b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql @@ -7,10 +7,7 @@ CREATE TABLE expense_nonstocks ( qty NUMERIC(15, 3) NOT NULL, unit_price NUMERIC(15, 3) NOT NULL, total_price NUMERIC(15, 3) NOT NULL, - note TEXT NULL, - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now(), - deleted_at TIMESTAMPTZ + note TEXT NULL ); -- Tambahkan Foreign Key ke expenses @@ -56,6 +53,4 @@ END $$; -- Index CREATE INDEX idx_expense_nonstocks_expense_id ON expense_nonstocks (expense_id); -CREATE INDEX idx_expense_nonstocks_nonstock_id ON expense_nonstocks (nonstock_id); - -CREATE INDEX idx_expense_nonstocks_deleted_at ON expense_nonstocks (deleted_at); \ No newline at end of file +CREATE INDEX idx_expense_nonstocks_nonstock_id ON expense_nonstocks (nonstock_id); \ No newline at end of file diff --git a/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql b/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql index 0886cd7a..ae58ca48 100644 --- a/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql +++ b/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql @@ -6,10 +6,7 @@ CREATE TABLE expense_realizations ( realization_total_price NUMERIC(15, 3) NOT NULL, realization_date DATE NOT NULL, note TEXT, - created_by BIGINT, - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now(), - deleted_at TIMESTAMPTZ + created_by BIGINT ); -- Tambahkan Foreign Key ke expense_nonstocks @@ -35,6 +32,4 @@ END $$; -- Index CREATE INDEX idx_expense_realizations_nonstock_id ON expense_realizations (expense_nonstock_id); -CREATE INDEX idx_expense_realizations_date ON expense_realizations (realization_date); - -CREATE INDEX idx_expense_realizations_deleted_at ON expense_realizations (deleted_at); \ No newline at end of file +CREATE INDEX idx_expense_realizations_date ON expense_realizations (realization_date); \ No newline at end of file diff --git a/internal/entities/expense_nonstock.go b/internal/entities/expense_nonstock.go index d483e4bf..7be2053a 100644 --- a/internal/entities/expense_nonstock.go +++ b/internal/entities/expense_nonstock.go @@ -1,24 +1,15 @@ package entities -import ( - "time" - - "gorm.io/gorm" -) - type ExpenseNonstock struct { - Id uint64 `gorm:"primaryKey;autoIncrement"` - ExpenseId *uint64 `gorm:""` - ProjectFlockKandangId *uint64 `gorm:""` - KandangId *uint64 `gorm:""` - NonstockId *uint64 `gorm:""` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` - Note string `gorm:"type:text"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseId *uint64 `gorm:""` + ProjectFlockKandangId *uint64 `gorm:""` + KandangId *uint64 `gorm:""` + NonstockId *uint64 `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + Note string `gorm:"type:text"` // Relations Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` diff --git a/internal/entities/expense_realization.go b/internal/entities/expense_realization.go index 45a87602..629fdfb7 100644 --- a/internal/entities/expense_realization.go +++ b/internal/entities/expense_realization.go @@ -2,22 +2,17 @@ package entities import ( "time" - - "gorm.io/gorm" ) type ExpenseRealization struct { - Id uint64 `gorm:"primaryKey;autoIncrement"` - ExpenseNonstockId *uint64 `gorm:""` - RealizationQty float64 `gorm:"type:numeric(15,3);not null"` - RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"` - RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"` - RealizationDate time.Time `gorm:"type:date;not null"` - Note *string `gorm:"type:text"` - CreatedBy *uint64 `gorm:""` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseNonstockId *uint64 `gorm:""` + RealizationQty float64 `gorm:"type:numeric(15,3);not null"` + RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"` + RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"` + RealizationDate time.Time `gorm:"type:date;not null"` + Note *string `gorm:"type:text"` + CreatedBy *uint64 `gorm:""` // Relations ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"` From 53b226f2430ac3d1a859aa7e43255fccbdd6675a Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Fri, 21 Nov 2025 09:53:33 +0700 Subject: [PATCH 56/59] fix(BE): adjust dto and project flock, master data, and marketing --- .../dto/delivery-orders.dto.go | 66 ++++++++--------- .../services/sales-orders.service.go | 2 +- .../master/customers/dto/customer.dto.go | 74 +++++++++++-------- .../master/kandangs/dto/kandang.dto.go | 20 ++--- .../master/products/dto/product.dto.go | 12 +-- .../services/projectflock.service.go | 11 +-- 6 files changed, 101 insertions(+), 84 deletions(-) diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go index b3ac5a05..d2f29fe9 100644 --- a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -21,27 +21,27 @@ type MarketingRelationDTO struct { type MarketingListDTO struct { MarketingRelationDTO - Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` - SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"` - SoDocs string `json:"so_docs,omitempty"` - SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` + Customer customerDTO.CustomerRelationDTO `json:"customer"` + SalesPerson userDTO.UserRelationDTO `json:"sales_person"` + SoDocs string `json:"so_docs"` + SalesOrder []MarketingProductDTO `json:"sales_order"` + CreatedUser userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"` } type MarketingDetailDTO struct { MarketingRelationDTO - Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` - SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"` - SoDocs string `json:"so_docs,omitempty"` - SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` - DeliveryOrder []DeliveryGroupDTO `json:"delivery_order,omitempty"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` + Customer customerDTO.CustomerRelationDTO `json:"customer"` + SalesPerson userDTO.UserRelationDTO `json:"sales_person"` + SoDocs string `json:"so_docs"` + SalesOrder []MarketingProductDTO `json:"sales_order"` + DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"` + CreatedUser userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"` } type MarketingDeliveryProductDTO struct { Id uint `json:"id"` @@ -131,28 +131,28 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD } func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingListDTO { - var createdUser *userDTO.UserRelationDTO + var createdUser userDTO.UserRelationDTO if marketing.CreatedUser.Id != 0 { mapped := userDTO.ToUserRelationDTO(marketing.CreatedUser) - createdUser = &mapped + createdUser = mapped } - var customer *customerDTO.CustomerRelationDTO + var customer customerDTO.CustomerRelationDTO if marketing.Customer.Id != 0 { mapped := customerDTO.ToCustomerRelationDTO(marketing.Customer) - customer = &mapped + customer = mapped } - var salesPerson *userDTO.UserRelationDTO + var salesPerson userDTO.UserRelationDTO if marketing.SalesPerson.Id != 0 { mapped := userDTO.ToUserRelationDTO(marketing.SalesPerson) - salesPerson = &mapped + salesPerson = mapped } - var latestApproval *approvalDTO.ApprovalRelationDTO + var latestApproval approvalDTO.ApprovalRelationDTO if marketing.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) - latestApproval = &mapped + latestApproval = mapped } var salesOrderProducts []MarketingProductDTO @@ -177,22 +177,22 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M } func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingDetailDTO { - var createdUser *userDTO.UserRelationDTO + var createdUser userDTO.UserRelationDTO if marketing.CreatedUser.Id != 0 { mapped := userDTO.ToUserRelationDTO(marketing.CreatedUser) - createdUser = &mapped + createdUser = mapped } - var customer *customerDTO.CustomerRelationDTO + var customer customerDTO.CustomerRelationDTO if marketing.Customer.Id != 0 { mapped := customerDTO.ToCustomerRelationDTO(marketing.Customer) - customer = &mapped + customer = mapped } - var salesPerson *userDTO.UserRelationDTO + var salesPerson userDTO.UserRelationDTO if marketing.SalesPerson.Id != 0 { mapped := userDTO.ToUserRelationDTO(marketing.SalesPerson) - salesPerson = &mapped + salesPerson = mapped } var salesOrderProducts []MarketingProductDTO @@ -214,10 +214,10 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs, marketing.SoNumber) - var latestApproval *approvalDTO.ApprovalRelationDTO + var latestApproval approvalDTO.ApprovalRelationDTO if marketing.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) - latestApproval = &mapped + latestApproval = mapped } return MarketingDetailDTO{ diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index c1cd8f5f..d750c4a4 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -58,7 +58,7 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). - Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Product.Flags"). Preload("Products.ProductWarehouse.Warehouse") } diff --git a/internal/modules/master/customers/dto/customer.dto.go b/internal/modules/master/customers/dto/customer.dto.go index 333c1297..444c6768 100644 --- a/internal/modules/master/customers/dto/customer.dto.go +++ b/internal/modules/master/customers/dto/customer.dto.go @@ -10,24 +10,28 @@ import ( // === DTO Structs === type CustomerRelationDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - PicId uint `json:"pic_id"` - Type string `json:"type"` - Address string `json:"address"` - Phone string `json:"phone"` - Email string `json:"email"` - AccountNumber string `json:"account_number"` - Balance float64 `json:"balance"` - - Pic *userDTO.UserRelationDTO `json:"pic,omitempty"` + Id uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + AccountNumber string `json:"account_number"` + Balance float64 `json:"balance"` + Pic *userDTO.UserRelationDTO `json:"pic,omitempty"` } type CustomerListDTO struct { - CustomerRelationDTO - CreatedUser *userDTO.UserRelationDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Id uint `json:"id"` + Name string `json:"name"` + PicId uint `json:"pic_id"` + Type string `json:"type"` + Address string `json:"address"` + Phone string `json:"phone"` + Email string `json:"email"` + AccountNumber string `json:"account_number"` + Balance float64 `json:"balance"` + Pic userDTO.UserRelationDTO `json:"pic"` + CreatedUser userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CustomerDetailDTO struct { @@ -44,6 +48,28 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO { } return CustomerRelationDTO{ + Id: e.Id, + Name: e.Name, + Type: e.Type, + AccountNumber: e.AccountNumber, + Pic: pic, + } +} + +func ToCustomerListDTO(e entity.Customer) CustomerListDTO { + var createdUser userDTO.UserRelationDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) + createdUser = mapped + } + + var pic userDTO.UserRelationDTO + if e.Pic.Id != 0 { + mapped := userDTO.ToUserRelationDTO(e.Pic) + pic = mapped + } + + return CustomerListDTO{ Id: e.Id, Name: e.Name, PicId: e.PicId, @@ -53,21 +79,9 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO { Email: e.Email, AccountNumber: e.AccountNumber, Pic: pic, - } -} - -func ToCustomerListDTO(e entity.Customer) CustomerListDTO { - var createdUser *userDTO.UserRelationDTO - if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserRelationDTO(e.CreatedUser) - createdUser = &mapped - } - - return CustomerListDTO{ - CustomerRelationDTO: ToCustomerRelationDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } diff --git a/internal/modules/master/kandangs/dto/kandang.dto.go b/internal/modules/master/kandangs/dto/kandang.dto.go index 9f99d5fa..1584b07f 100644 --- a/internal/modules/master/kandangs/dto/kandang.dto.go +++ b/internal/modules/master/kandangs/dto/kandang.dto.go @@ -11,12 +11,12 @@ import ( // === DTO Structs === type KandangRelationDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Capacity float64 `json:"capacity"` - Location locationDTO.LocationRelationDTO `json:"location,omitempty"` - Pic userDTO.UserRelationDTO `json:"pic,omitempty"` + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Capacity float64 `json:"capacity"` + Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` + Pic *userDTO.UserRelationDTO `json:"pic,omitempty"` } type KandangListDTO struct { @@ -38,16 +38,16 @@ type KandangDetailDTO struct { // === Mapper Functions === func ToKandangRelationDTO(e entity.Kandang) KandangRelationDTO { - var location locationDTO.LocationRelationDTO + var location *locationDTO.LocationRelationDTO if e.Location.Id != 0 { mapped := locationDTO.ToLocationRelationDTO(e.Location) - location = mapped + location = &mapped } - var pic userDTO.UserRelationDTO + var pic *userDTO.UserRelationDTO if e.Pic.Id != 0 { mapped := userDTO.ToUserRelationDTO(e.Pic) - pic = mapped + pic = &mapped } return KandangRelationDTO{ diff --git a/internal/modules/master/products/dto/product.dto.go b/internal/modules/master/products/dto/product.dto.go index f676f994..3b2370b2 100644 --- a/internal/modules/master/products/dto/product.dto.go +++ b/internal/modules/master/products/dto/product.dto.go @@ -17,7 +17,7 @@ type ProductRelationDTO struct { ProductPrice float64 `gorm:"type:numeric(15,3);not null"` SellingPrice *float64 `gorm:"type:numeric(15,3)"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` - Flags []string `json:"flags"` + Flags *[]string `json:"flags,omitempty"` } type ProductListDTO struct { @@ -56,10 +56,12 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO { } return ProductRelationDTO{ - Id: e.Id, - Name: e.Name, - Flags: flags, - Uom: uomRef, + Id: e.Id, + Name: e.Name, + ProductPrice: e.ProductPrice, + SellingPrice: e.SellingPrice, + Flags: &flags, + Uom: uomRef, } } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 7e3b94cb..f40ea97b 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -10,6 +10,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + // authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" @@ -124,7 +125,7 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") + return s.withRelations(db) }) if err != nil { s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err) @@ -170,7 +171,7 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr if s.ApprovalSvc != nil { approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") + return s.withRelations(db) }) if err != nil { s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) @@ -199,7 +200,7 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock if s.ApprovalSvc != nil { approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") + return s.withRelations(db) }) if err != nil { s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) @@ -1098,11 +1099,11 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka return kandangRepository.NewKandangRepository(s.Repository.DB()) } -func actorIDFromContext(c *fiber.Ctx) (uint, error) { +func actorIDFromContext(_ *fiber.Ctx) (uint, error) { // user, ok := authmiddleware.AuthenticatedUser(c) // if !ok || user == nil || user.Id == 0 { // return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // } // return user.Id, nil - return 1,nil + return 1, nil } From 6768092e3bdbe5cd4b3f6a53e3082589e04e650a Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 21 Nov 2025 10:20:07 +0700 Subject: [PATCH 57/59] Fix[BE-261]: fixing location not preloaded on get one non-bop API --- internal/modules/expenses/dto/expense.dto.go | 131 ++++++++++-------- .../expenses/services/expense.service.go | 4 +- .../production/project_flocks/route.go | 3 +- .../services/projectflock.service.go | 11 +- 4 files changed, 79 insertions(+), 70 deletions(-) diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index d87502ed..d9844014 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -12,6 +12,7 @@ import ( userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) +// === DTO Structs === type ExpenseBaseDTO struct { Id uint64 `json:"id"` ReferenceNumber string `json:"reference_number"` @@ -76,6 +77,8 @@ type DocumentDTO struct { Path string `json:"path"` } +// === MAPPERS === + func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO { var location *locationDTO.LocationBaseDTO var supplier *supplierDTO.SupplierBaseDTO @@ -85,9 +88,9 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO { realizationDate = &e.RealizationDate } - if len(e.Nonstocks) > 0 && e.Nonstocks[0].ProjectFlockKandang != nil { - if e.Nonstocks[0].ProjectFlockKandang.Kandang.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(e.Nonstocks[0].ProjectFlockKandang.Kandang.Location) + if len(e.Nonstocks) > 0 && e.Nonstocks[0].Kandang != nil { + if e.Nonstocks[0].Kandang.Location.Id != 0 { + mapped := locationDTO.ToLocationBaseDTO(e.Nonstocks[0].Kandang.Location) location = &mapped } } @@ -205,64 +208,7 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { totalRealisasi += r.TotalPrice } - kandangMap := make(map[uint64]*KandangGroupDTO) - - for _, p := range pengajuans { - var kandangId uint64 - var kandangName string - - for _, ns := range e.Nonstocks { - if ns.Id == p.Id && ns.Kandang != nil { - kandangId = uint64(ns.Kandang.Id) - kandangName = ns.Kandang.Name - break - } - } - - if kandangId > 0 { - if kandangMap[kandangId] == nil { - kandangMap[kandangId] = &KandangGroupDTO{ - Id: kandangId, - KandangId: kandangId, - Name: kandangName, - Pengajuans: []ExpenseNonstockDTO{}, - Realisasi: []ExpenseRealizationDTO{}, - } - } - kandangMap[kandangId].Pengajuans = append(kandangMap[kandangId].Pengajuans, p) - } - } - - for _, r := range realisasi { - var kandangId uint64 - var kandangName string - - for _, ns := range e.Nonstocks { - if ns.Realization != nil && ns.Realization.Id == r.Id && ns.Kandang != nil { - kandangId = uint64(ns.Kandang.Id) - kandangName = ns.Kandang.Name - break - } - } - - if kandangId > 0 { - if kandangMap[kandangId] == nil { - kandangMap[kandangId] = &KandangGroupDTO{ - Id: kandangId, - KandangId: kandangId, - Name: kandangName, - Pengajuans: []ExpenseNonstockDTO{}, - Realisasi: []ExpenseRealizationDTO{}, - } - } - kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r) - } - } - - var kandangs []KandangGroupDTO - for _, k := range kandangMap { - kandangs = append(kandangs, *k) - } + kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks) return ExpenseDetailDTO{ ExpenseBaseDTO: ToExpenseBaseDTO(e), @@ -294,3 +240,66 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO { Nonstock: nonstock, } } + +func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseRealizationDTO, nonstocks []entity.ExpenseNonstock) []KandangGroupDTO { + kandangMap := make(map[uint64]*KandangGroupDTO) + + for _, p := range pengajuans { + var kandangId uint64 + var kandangName string + + for _, ns := range nonstocks { + if ns.Id == p.Id && ns.Kandang != nil { + kandangId = uint64(ns.Kandang.Id) + kandangName = ns.Kandang.Name + break + } + } + + if kandangId > 0 { + if kandangMap[kandangId] == nil { + kandangMap[kandangId] = &KandangGroupDTO{ + Id: kandangId, + KandangId: kandangId, + Name: kandangName, + Pengajuans: []ExpenseNonstockDTO{}, + Realisasi: []ExpenseRealizationDTO{}, + } + } + kandangMap[kandangId].Pengajuans = append(kandangMap[kandangId].Pengajuans, p) + } + } + + for _, r := range realisasi { + var kandangId uint64 + var kandangName string + + for _, ns := range nonstocks { + if ns.Realization != nil && ns.Realization.Id == r.Id && ns.Kandang != nil { + kandangId = uint64(ns.Kandang.Id) + kandangName = ns.Kandang.Name + break + } + } + + if kandangId > 0 { + if kandangMap[kandangId] == nil { + kandangMap[kandangId] = &KandangGroupDTO{ + Id: kandangId, + KandangId: kandangId, + Name: kandangName, + Pengajuans: []ExpenseNonstockDTO{}, + Realisasi: []ExpenseRealizationDTO{}, + } + } + kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r) + } + } + + var kandangs []KandangGroupDTO + for _, k := range kandangMap { + kandangs = append(kandangs, *k) + } + + return kandangs +} diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 658537b7..03512998 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -71,7 +71,8 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB { Preload("Nonstocks.Nonstock"). Preload("Nonstocks.Realization"). Preload("Nonstocks.ProjectFlockKandang.Kandang.Location"). - Preload("Nonstocks.Kandang") + Preload("Nonstocks.Kandang"). + Preload("Nonstocks.Kandang.Location") } func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) { @@ -629,6 +630,7 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) ( } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) approvalAction := entity.ApprovalActionApproved diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index 39e283ab..ae2f1946 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -1,7 +1,6 @@ package project_flocks import ( - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers" projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -19,7 +18,7 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj // 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.Use(m.Auth(u)) + // route.Use(m.Auth(u)) route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 5b92b0db..b1679561 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -10,7 +10,6 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" @@ -1042,9 +1041,9 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka } func actorIDFromContext(c *fiber.Ctx) (uint, error) { - user, ok := authmiddleware.AuthenticatedUser(c) - if !ok || user == nil || user.Id == 0 { - return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - } - return user.Id, nil + // user, ok := authmiddleware.AuthenticatedUser(c) + // if !ok || user == nil || user.Id == 0 { + // return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + // } + return 1, nil } From 3fc330d8f78b95c76cc6103b6d64d624ee7b5060 Mon Sep 17 00:00:00 2001 From: ragilap Date: Fri, 21 Nov 2025 10:50:30 +0700 Subject: [PATCH 58/59] fix merging --- .../controllers/projectflock.controller.go | 4 +- .../repositories/projectflock.repository.go | 101 ++++++++++++++-- .../production/project_flocks/route.go | 3 +- .../services/projectflock.service.go | 110 +++++------------- .../purchases/services/purchase.service.go | 33 ++++-- 5 files changed, 151 insertions(+), 100 deletions(-) diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index 5d51b985..6d78520e 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -261,7 +261,7 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error { }) } -func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { +func (u *ProjectflockController) GetPeriodSummary(c *fiber.Ctx) error { param := c.Params("location_id") id, err := strconv.Atoi(param) @@ -269,7 +269,7 @@ func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id") } - summaries, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id)) + summaries, err := u.ProjectflockService.GetPeriodSummary(c, uint(id)) if err != nil { return err } diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 7221ebd0..de4df25d 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -11,17 +11,24 @@ import ( "gorm.io/gorm" ) -const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))" type ProjectflockRepository interface { repository.BaseRepository[entity.ProjectFlock] GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) + GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error) + GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error) + GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, error) AreaExists(ctx context.Context, id uint) (bool, error) FcrExists(ctx context.Context, id uint) (bool, error) LocationExists(ctx context.Context, id uint) (bool, error) } +type KandangPeriodRow struct { + Id uint + Name string + LatestPeriod int +} type ProjectflockRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.ProjectFlock] @@ -35,19 +42,13 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository { func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) { return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { - db = r.withDefaultRelations(db) return r.applyQueryFilters(db, params) }) } func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { - return r.withDefaultRelations(db) - } -} - -func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB { - return db. + return db. Preload("CreatedUser"). Preload("Area"). Preload("Fcr"). @@ -55,8 +56,10 @@ func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB Preload("Kandangs"). Preload("KandangHistory"). Preload("KandangHistory.Kandang") + } } + func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB { if params == nil { return db @@ -163,6 +166,88 @@ func (r *ProjectflockRepositoryImpl) LocationExists(ctx context.Context, id uint return repository.Exists[entity.Location](ctx, r.DB(), id) } +func (r *ProjectflockRepositoryImpl) GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error) { + result := make(map[uint]int) + if len(kandangIDs) == 0 { + return result, nil + } + + unique := uniqueUintSlice(kandangIDs) + for _, id := range unique { + result[id] = 1 + } + + type periodRow struct { + KandangID uint + MaxPeriod int + } + + var rows []periodRow + if err := r.DB().WithContext(ctx). + Table("project_flock_kandangs"). + Select("kandang_id, COALESCE(MAX(period), 0) AS max_period"). + Where("kandang_id IN ?", unique). + Group("kandang_id"). + Scan(&rows).Error; err != nil { + return nil, err + } + + for _, row := range rows { + if row.MaxPeriod > 0 { + result[row.KandangID] = row.MaxPeriod + 1 + } + } + + return result, nil +} + +func (r *ProjectflockRepositoryImpl) GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error) { + if projectFlockID == 0 { + return 0, nil + } + + var currentPeriod int + if err := r.DB().WithContext(ctx). + Table("project_flock_kandangs"). + Where("project_flock_id = ?", projectFlockID). + Select("COALESCE(MAX(period), 0)"). + Scan(¤tPeriod).Error; err != nil { + return 0, err + } + + return currentPeriod, nil +} + +func (r *ProjectflockRepositoryImpl) GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, error) { + rows := make([]KandangPeriodRow, 0) + if err := r.DB().WithContext(ctx). + Table("kandangs AS k"). + Select("k.id, k.name, COALESCE(MAX(pfk.period), 0) AS latest_period"). + Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.kandang_id = k.id"). + Where("k.location_id = ?", locationID). + Where("k.deleted_at IS NULL"). + Group("k.id, k.name"). + Order("k.id ASC"). + Scan(&rows).Error; err != nil { + return nil, err + } + + return rows, nil +} + +func uniqueUintSlice(values []uint) []uint { + seen := make(map[uint]struct{}, len(values)) + result := make([]uint, 0, len(values)) + for _, v := range values { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + result = append(result, v) + } + return result +} + func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder string) []string { direction := "ASC" if strings.ToLower(sortOrder) == "desc" { diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index deec1a05..eb806129 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -20,9 +20,8 @@ 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.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) - route.Get("/kandangs/:location_id/periods", ctrl.GetFlockPeriodSummary) + route.Get("/locations/:location_id/periods", ctrl.GetPeriodSummary) } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index f40ea97b..19b07447 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -37,7 +37,7 @@ type ProjectflockService interface { GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) - GetFlockPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) + GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) } @@ -85,18 +85,6 @@ func NewProjectflockService( } } -func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { - return db. - Preload("CreatedUser"). - Preload("Flock"). - Preload("Area"). - Preload("Fcr"). - Preload("Location"). - Preload("Kandangs"). - Preload("KandangHistory"). - Preload("KandangHistory.Kandang") -} - func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, nil, err @@ -124,9 +112,7 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e ids[i] = item.Id } - latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, func(db *gorm.DB) *gorm.DB { - return s.withRelations(db) - }) + latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, s.Repository.WithDefaultRelations()) if err != nil { s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err) } else if len(latestMap) > 0 { @@ -170,9 +156,7 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr } if s.ApprovalSvc != nil { - approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { - return s.withRelations(db) - }) + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.Repository.WithDefaultRelations()) if err != nil { s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) } else if len(approvals) > 0 { @@ -199,9 +183,7 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock } if s.ApprovalSvc != nil { - approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { - return s.withRelations(db) - }) + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.Repository.WithDefaultRelations()) if err != nil { s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) } else if len(approvals) > 0 { @@ -320,13 +302,12 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return err } - // Compute period based on location history (max period in that location + 1), - // and store it on project_flock_kandangs only. - nextPeriod, err := s.nextLocationPeriod(c.Context(), dbTransaction, req.LocationId) + // Compute period per kandang so every kandang maintains its own cycle history. + periods, err := projectRepo.GetNextPeriodsForKandangs(c.Context(), kandangIDs) if err != nil { return err } - if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, nextPeriod); err != nil { + if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, periods); err != nil { return err } @@ -544,19 +525,24 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if len(toAttach) > 0 { - var currentPeriod int - if err := dbTransaction.WithContext(c.Context()). - Table("project_flock_kandangs"). - Where("project_flock_id = ?", id). - Select("COALESCE(MAX(period), 0)"). - Scan(¤tPeriod).Error; err != nil { + currentPeriod, err := projectRepo.GetCurrentProjectPeriod(c.Context(), id) + if err != nil { return err } - if currentPeriod <= 0 { - currentPeriod = 1 + + periods := make(map[uint]int, len(toAttach)) + if currentPeriod > 0 { + for _, kid := range toAttach { + periods[kid] = currentPeriod + } + } else { + periods, err = projectRepo.GetNextPeriodsForKandangs(c.Context(), toAttach) + if err != nil { + return err + } } - if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, currentPeriod); err != nil { + if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, periods); err != nil { return err } } @@ -832,32 +818,6 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u return total, nil } -// nextLocationPeriod computes the next period number for a given location -// based on the maximum period that has ever been used by any kandang in that location. -func (s projectflockService) nextLocationPeriod(ctx context.Context, tx *gorm.DB, locationID uint) (int, error) { - if locationID == 0 { - return 0, fiber.NewError(fiber.StatusBadRequest, "location_id is required to compute period") - } - - db := s.Repository.DB() - if tx != nil { - db = tx - } - - var maxPeriod int - if err := db.WithContext(ctx). - Table("project_flock_kandangs pfk"). - Joins("JOIN kandangs k ON k.id = pfk.kandang_id"). - Where("k.location_id = ?", locationID). - Select("COALESCE(MAX(pfk.period), 0)"). - Scan(&maxPeriod).Error; err != nil { - s.Log.Errorf("Failed to compute max period for location %d: %+v", locationID, err) - return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute period for location") - } - - return maxPeriod + 1, nil -} - func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) { if len(projectIDs) == 0 { return map[uint]int{}, nil @@ -865,7 +825,7 @@ func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs) } -func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) { +func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) { if locationID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required") } @@ -879,24 +839,8 @@ func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, locationID uint return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } - type kandangPeriodRow struct { - Id uint - Name string - LatestPeriod int - } - - var rows []kandangPeriodRow - - db := s.Repository.DB().WithContext(c.Context()) - if err := db. - Table("kandangs AS k"). - Select("k.id, k.name, COALESCE(MAX(pfk.period), 0) AS latest_period"). - Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.kandang_id = k.id"). - Where("k.location_id = ?", locationID). - Where("k.deleted_at IS NULL"). - Group("k.id, k.name"). - Order("k.id ASC"). - Scan(&rows).Error; err != nil { + rows, err := s.Repository.GetKandangPeriodSummaryRows(c.Context(), locationID) + if err != nil { s.Log.Errorf("Failed to fetch kandang period summary for location %d: %+v", locationID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang period summary") } @@ -991,7 +935,7 @@ func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint return newFlock, nil } -func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, period int) error { +func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, periods map[uint]int) error { if len(kandangIDs) == 0 { return nil } @@ -1026,6 +970,10 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction * records := make([]*entity.ProjectFlockKandang, 0, len(toAttach)) for _, id := range toAttach { + period := periods[id] + if period <= 0 { + period = 1 + } records = append(records, &entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: id, diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 5035cbc6..b0d5311d 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -168,9 +168,9 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase return nil, err } - user, ok := authmiddleware.AuthenticatedUser(c) - if !ok || user == nil || user.Id == 0 { - return nil, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + actorID, err := actorIDFromContext(c) + if err != nil { + return nil, err } ctx := c.Context() @@ -263,7 +263,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase DueDate: dueDate, GrandTotal: 0, Notes: req.Notes, - CreatedBy: uint64(user.Id), + CreatedBy: uint64(actorID), } items := make([]*entity.PurchaseItem, 0, len(aggregated)) @@ -332,7 +332,10 @@ func (s *purchaseService) processStaffPurchaseApproval(c *fiber.Ctx, id uint64, return nil, err } - actorID := uint(1) // TODO: replace with authenticated user id once available + actorID, err := actorIDFromContext(c) + if err != nil { + return nil, err + } ctx := c.Context() purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) @@ -476,6 +479,11 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint64, req *v return nil, err } + actorID, err := actorIDFromContext(c) + if err != nil { + return nil, err + } + ctx := c.Context() purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) @@ -496,7 +504,6 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint64, req *v return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must reach staff purchase step before manager approval") } - actorID := uint(1) action := entity.ApprovalActionApproved now := time.Now().UTC() hasExistingPO := purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" @@ -577,6 +584,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati return nil, err } + actorID, err := actorIDFromContext(c) + if err != nil { + return nil, err + } + ctx := c.Context() purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) @@ -674,7 +686,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati receivingAction := entity.ApprovalActionApproved completedAction := entity.ApprovalActionApproved - actorID := uint(1) approvalSvc := s.approvalServiceForDB(nil) if approvalSvc != nil { @@ -1059,6 +1070,14 @@ func (s *purchaseService) notifyExpenseItemsDeleted(ctx context.Context, purchas } } +func actorIDFromContext(c *fiber.Ctx) (uint, error) { + user, ok := authmiddleware.AuthenticatedUser(c) + if !ok || user == nil || user.Id == 0 { + return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } + return user.Id, nil +} + func (s *purchaseService) buildStaffAdjustmentPayload( ctx context.Context, purchase *entity.Purchase, From b0dfa717d5bb68e25011a097f7259eb7dee8525f Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 21 Nov 2025 11:16:34 +0700 Subject: [PATCH 59/59] FIX[BE-261]: expense list dto ganti dari hardcoded ke ambil dari expensebasedto --- internal/modules/expenses/dto/expense.dto.go | 32 +++++++------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index 680407a4..bee50c6d 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -34,28 +34,23 @@ type ExpenseBaseDTO struct { } type ExpenseListDTO struct { - Id uint64 `json:"id"` - ReferenceNumber string `json:"reference_number"` - PoNumber string `json:"po_number"` - Category string `json:"category"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` + ExpenseBaseDTO + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` } type ExpenseDetailDTO struct { ExpenseBaseDTO Documents []DocumentDTO `json:"documents,omitempty"` RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` Kandangs []KandangGroupDTO `json:"kandangs,omitempty"` TotalPengajuan float64 `json:"total_pengajuan"` TotalRealisasi float64 `json:"total_realisasi"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` } @@ -149,16 +144,11 @@ func ToExpenseListDTO(e entity.Expense) ExpenseListDTO { } return ExpenseListDTO{ - Id: e.Id, - ReferenceNumber: e.ReferenceNumber, - PoNumber: e.PoNumber, - Category: e.Category, - ExpenseDate: e.ExpenseDate, - GrandTotal: e.GrandTotal, - CreatedUser: createdUser, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - LatestApproval: latestApproval, + ExpenseBaseDTO: ToExpenseBaseDTO(&e), + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + LatestApproval: latestApproval, } }