feat[BE-127]: Createing transfer laying create one, approvals, get one, get all, update, delete, but Still unfinished

This commit is contained in:
aguhh18
2025-11-05 08:56:18 +07:00
parent 3a5c49c511
commit 1ee97b91a5
13 changed files with 928 additions and 74 deletions
@@ -4,7 +4,8 @@ CREATE TABLE IF NOT EXISTS laying_transfers (
from_project_flock_id BIGINT NOT NULL, from_project_flock_id BIGINT NOT NULL,
to_project_flock_id BIGINT NOT NULL, to_project_flock_id BIGINT NOT NULL,
transfer_date DATE 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, notes TEXT,
created_at TIMESTAMPTZ DEFAULT now(), created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(),
+3 -2
View File
@@ -12,7 +12,8 @@ type LayingTransfer struct {
FromProjectFlockId uint `gorm:"not null"` FromProjectFlockId uint `gorm:"not null"`
ToProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"`
TransferDate time.Time `gorm:"type:date;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"` Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
@@ -24,5 +25,5 @@ type LayingTransfer struct {
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
LatestApproval *Approval `gorm:"-" json:"-"`
} }
@@ -16,6 +16,7 @@ type WarehouseRepository interface {
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
} }
type WarehouseRepositoryImpl struct { type WarehouseRepositoryImpl struct {
@@ -60,3 +61,16 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
} }
return &warehouse, nil 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
}
@@ -13,6 +13,7 @@ type ProjectChickinRepository interface {
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetPendingByProjectFlockKandangID(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 { type ChickinRepositoryImpl struct {
@@ -64,3 +65,17 @@ func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Co
} }
return chickins, nil 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
}
@@ -10,7 +10,7 @@ import (
type ProjectFlockPopulationRepository interface { type ProjectFlockPopulationRepository interface {
// domain-specific // 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) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID 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() return r.BaseRepositoryImpl.DB()
} }
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) { func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) {
var record entity.ProjectFlockPopulation var records []entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
Where("project_flock_kandang_id = ?", projectFlockKandangID). Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
First(&record).Error Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Preload("ProjectChickin").
Find(&records).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &record, nil return records, nil
} }
func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) { func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) {
@@ -30,6 +30,10 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)), TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)),
TransferDateFrom: c.Query("transfer_date_from", ""), TransferDateFrom: c.Query("transfer_date_from", ""),
TransferDateTo: c.Query("transfer_date_to", ""), 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 { 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") 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 { if err != nil {
return err return err
} }
@@ -74,7 +78,7 @@ func (u *TransferLayingController) GetOne(c *fiber.Ctx) error {
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get transferLaying successfully", 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", 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,
})
}
@@ -4,61 +4,252 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" 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" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
// === DTO Structs === // === DTO Structs ===
type TransferLayingBaseDTO struct { type TransferLayingBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` 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 { type TransferLayingListDTO struct {
TransferLayingBaseDTO TransferLayingBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"`
CreatedAt time.Time `json:"created_at"` ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"`
UpdatedAt time.Time `json:"updated_at"` 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 { type TransferLayingDetailDTO struct {
TransferLayingListDTO TransferLayingListDTO
Sources []LayingTransferSourceDTO `json:"sources,omitempty"`
Targets []LayingTransferTargetDTO `json:"targets,omitempty"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
} }
// === Mapper Functions === // === 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 { func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO {
return TransferLayingBaseDTO{ return TransferLayingBaseDTO{
Id: e.Id, Id: e.Id,
TransferNumber: e.TransferNumber,
TransferDate: e.TransferDate,
Notes: e.Notes,
} }
} }
func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
var createdUser *userDTO.UserBaseDTO var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 { if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped createdUser = &mapped
} }
return TransferLayingListDTO{ return TransferLayingListDTO{
TransferLayingBaseDTO: ToTransferLayingBaseDTO(e), TransferLayingBaseDTO: ToTransferLayingBaseDTO(e),
CreatedAt: e.CreatedAt, FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock),
UpdatedAt: e.UpdatedAt, ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock),
CreatedUser: createdUser, PendingUsageQty: e.PendingUsageQty,
UsageQty: e.UsageQty,
CreatedBy: e.CreatedBy,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
} }
} }
func ToTransferLayingListDTOs(e []entity.LayingTransfer) []TransferLayingListDTO { func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
result := make([]TransferLayingListDTO, len(e)) var latestApproval *approvalDTO.ApprovalBaseDTO
for i, r := range e {
result[i] = ToTransferLayingListDTO(r) // 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 return result
} }
func ToTransferLayingDetailDTO(e entity.LayingTransfer) TransferLayingDetailDTO {
return TransferLayingDetailDTO{
TransferLayingListDTO: ToTransferLayingListDTO(e),
}
}
@@ -1,14 +1,21 @@
package transfer_layings package transfer_layings
import ( import (
"fmt"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "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" 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" 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" 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" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" 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) userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(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) userService := sUser.NewUserService(userRepo, validate)
TransferLayingRoutes(router, userService, transferLayingService) TransferLayingRoutes(router, userService, transferLayingService)
@@ -11,6 +11,7 @@ import (
type TransferLayingRepository interface { type TransferLayingRepository interface {
repository.BaseRepository[entity.LayingTransfer] repository.BaseRepository[entity.LayingTransfer]
GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error)
IdExists(ctx context.Context, id uint) (bool, error)
} }
type TransferLayingRepositoryImpl struct { type TransferLayingRepositoryImpl struct {
@@ -24,6 +25,9 @@ func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository {
db: db, 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) { func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) {
var transfer entity.LayingTransfer var transfer entity.LayingTransfer
@@ -19,10 +19,12 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
// route.Get("/:id", m.Auth(u), ctrl.GetOne) // route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
// route.Post("/approval", m.Auth(u), ctrl.Approval)
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne) route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne) route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval)
} }
@@ -1,10 +1,17 @@
package service package service
import ( import (
"context"
"errors" "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" 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" 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" 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" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
@@ -19,31 +26,61 @@ import (
type TransferLayingService interface { type TransferLayingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, 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) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
} }
type transferLayingService struct { type transferLayingService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.TransferLayingRepository Repository repository.TransferLayingRepository
ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository
ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository 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{ return &transferLayingService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
ProjectFlockRepo: projectFlockRepo, ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ProductWarehouseRepo: productWarehouseRepo,
WarehouseRepo: warehouseRepo,
ApprovalService: approvalService,
} }
} }
func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { 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) { 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 != "" { if params.TransferDateTo != "" {
db = db.Where("transfer_date <= ?", 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 { if err != nil {
s.Log.Errorf("Failed to get transferLayings: %+v", err) s.Log.Errorf("Failed to get transferLayings: %+v", err)
return nil, 0, 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 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) s.Log.Errorf("Failed get transferLaying by id: %+v", err)
return nil, 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 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) { func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
if err := common.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, commonSvc.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}, commonSvc.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
// Validate source kandangs
for _, detail := range req.SourceKandangs { for _, detail := range req.SourceKandangs {
if err := common.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, commonSvc.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil { ); err != nil {
return nil, err 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 { for _, detail := range req.TargetKandangs {
if err := common.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, commonSvc.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil { ); err != nil {
return nil, err 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) 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") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format")
} }
var totalQty float64 var totalSourceQty, totalTargetQty float64
for _, item := range req.SourceKandangs { sourceWarehouseMap := make(map[uint]uint)
totalQty += item.Quantity
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 { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
createBody := &entity.LayingTransfer{ if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil {
Notes: req.Reason, return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record")
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 { productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction)
return err 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 return nil
}) })
if err != 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) { 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 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) updateBody := make(map[string]any)
if req.TransferDate != nil { 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") return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
} }
s.Log.Errorf("Failed to update transferLaying: %+v", err) 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) return s.GetOne(c, id)
} }
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { 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) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
} }
s.Log.Errorf("Failed to delete transferLaying: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
return err
} }
// 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 return nil
} }
@@ -31,4 +31,14 @@ type Query struct {
TargetProjectFlockId uint `query:"target_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"` TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"`
TransferDateTo string `query:"transfer_date_to" 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"`
} }
+16
View File
@@ -176,6 +176,22 @@ var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{
ProjectFlockKandangStepDisetujui: "Disetujui", 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 // Validators
// ------------------------------------------------------------------- // -------------------------------------------------------------------