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 // -------------------------------------------------------------------