Feat[BE]: add multilpple type of chickin growing and laying, make convertion product when chickin approved, add projectflockkandangid on projectflock api

This commit is contained in:
aguhh18
2025-11-03 09:16:29 +07:00
parent 20f1be2ef8
commit 86f37a89c1
9 changed files with 485 additions and 346 deletions
@@ -1,7 +1,6 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
@@ -22,30 +21,84 @@ func NewChickinController(chickinService service.ChickinService) *ChickinControl
}
}
func (u *ChickinController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
// func (u *ChickinController) GetAll(c *fiber.Ctx) error {
// query := &validation.Query{
// Page: c.QueryInt("page", 1),
// Limit: c.QueryInt("limit", 10),
// ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
// }
// result, totalResults, err := u.ChickinService.GetAll(c, query)
// if err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Get all chickins successfully",
// Meta: response.Meta{
// Page: query.Page,
// Limit: query.Limit,
// TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
// TotalResults: totalResults,
// },
// Data: dto.ToChickinListDTOs(result),
// })
// }
// func (u *ChickinController) GetOne(c *fiber.Ctx) error {
// param := c.Params("id")
// id, err := strconv.Atoi(param)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
// }
// result, err := u.ChickinService.GetOne(c, uint(id))
// if err != nil {
// return err
// }
// return c.Status(fiber.StatusOK).
// JSON(response.Success{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Get chickin successfully",
// Data: dto.ToChickinListDTO(*result),
// })
// }
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, totalResults, err := u.ChickinService.GetAll(c, query)
results, err := u.ChickinService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
Code: fiber.StatusOK,
var (
data interface{}
message = "Create chickin successfully"
)
if len(results) == 1 {
data = dto.ToChickinListDTO(results[0])
} else {
message = "Create chickins successfully"
data = dto.ToChickinListDTOs(results)
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Get all chickins successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToChickinListDTOs(result),
Message: message,
Data: data,
})
}
@@ -67,80 +120,60 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error {
Code: fiber.StatusOK,
Status: "success",
Message: "Get chickin successfully",
Data: dto.ToChickinListDTO(*result),
Data: dto.ToChickinDetailDTO(*result),
})
}
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
// func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
// req := new(validation.Update)
// param := c.Params("id")
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
// id, err := strconv.Atoi(param)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
// }
result, err := u.ChickinService.CreateOne(c, req)
if err != nil {
return err
}
// if err := c.BodyParser(req); err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
// }
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create chickin successfully",
Data: dto.ToChickinListDTO(*result),
})
}
// result, err := u.ChickinService.UpdateOne(c, req, uint(id))
// if err != nil {
// return err
// }
func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
// return c.Status(fiber.StatusOK).
// JSON(response.Success{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Update chickin successfully",
// Data: dto.ToChickinListDTO(*result),
// })
// }
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
// param := c.Params("id")
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
// id, err := strconv.Atoi(param)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
// }
result, err := u.ChickinService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
// if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
// return err
// }
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update chickin successfully",
Data: dto.ToChickinListDTO(*result),
})
}
func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete chickin successfully",
})
}
// return c.Status(fiber.StatusOK).
// JSON(response.Common{
// Code: fiber.StatusOK,
// Status: "success",
// Message: "Delete chickin successfully",
// })
// }
func (u *ChickinController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
@@ -15,13 +15,13 @@ import (
// === DTO Structs (ordered) ===
type ChickinBaseDTO struct {
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
}
type ProjectFlockDTO struct {
@@ -61,7 +61,17 @@ type ChickinListDTO struct {
}
type ChickinDetailDTO struct {
ChickinListDTO
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"`
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
CreatedBy uint `json:"created_by"`
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// === Mapper Functions (ordered) ===
@@ -149,13 +159,13 @@ func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO {
projectFlockKandangId = e.ProjectFlockKandangId
}
return ChickinBaseDTO{
Id: e.Id,
Id: e.Id,
ProjectFlockKandangId: projectFlockKandangId,
ChickInDate: e.ChickInDate,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
ChickInDate: e.ChickInDate,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
}
}
@@ -203,7 +213,31 @@ func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO {
}
func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO {
var createdUser *userBaseDTO.UserBaseDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
return ChickinDetailDTO{
ChickinListDTO: ToChickinListDTO(e),
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
ChickInDate: e.ChickInDate,
ProductWarehouseId: e.ProductWarehouseId,
UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
CreatedBy: e.CreatedBy,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO {
result := make([]ChickinDetailDTO, len(e))
for i, r := range e {
result[i] = ToChickinDetailDTO(r)
}
return result
}
@@ -20,10 +20,10 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
// route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
// route.Patch("/:id", ctrl.UpdateOne)
// route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval)
}
@@ -25,7 +25,7 @@ import (
type ChickinService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
@@ -109,7 +109,7 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e
return chickin, nil
}
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) {
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
@@ -150,6 +150,11 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
actorID := uint(1) // todo nanti ambil dari auth context
newChikins := make([]*entity.ProjectChickin, 0)
for _, productWarehouse := range productWarehouses {
if productWarehouse.Quantity <= 0 {
continue
}
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: req.ProjectFlockKandangId,
ChickInDate: chickinDate,
@@ -167,6 +172,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create")
}
existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins")
}
isFirstTime := len(existingChikins) == 0
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
@@ -187,16 +198,27 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to update product warehouse quantity for id %d", chickin.ProductWarehouseId)
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId))
}
return err
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity")
}
}
var approvalAction entity.ApprovalAction
if isFirstTime {
approvalAction = entity.ApprovalActionCreated
} else {
approvalAction = entity.ApprovalActionUpdated
}
if latest == nil {
action := entity.ApprovalActionCreated
if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &action, actorID, nil); err != nil {
if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil {
lower := strings.ToLower(err.Error())
if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) {
return err
}
}
} else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil {
lower := strings.ToLower(err.Error())
if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) {
return err
@@ -210,7 +232,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return newChikins[0], nil
result := make([]entity.ProjectChickin, 0, len(newChikins))
for _, chickin := range newChikins {
loaded, err := s.GetOne(c, chickin.Id)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reload chickin %d with relations: %v", chickin.Id, err))
}
result = append(result, *loaded)
}
if len(result) == 0 {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins")
}
return result, nil
}
func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) {
@@ -257,7 +291,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID := uint(1) // todo nanti ambil dari auth context
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
@@ -269,21 +304,19 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
approvableIDs := uniqueUintSlice(req.ApprovableIds)
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
if len(approvableIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
// Validate all ProjectFlockKandang IDs exist and have valid approval status
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
for _, id := range approvableIDs {
idCopy := id
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
return nil, err
}
// Check latest approval status - must be PENGAJUAN to be approvable
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
}
@@ -308,6 +341,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
for _, approvableID := range approvableIDs {
actorID := uint(1) // todo nanti ambil dari auth context
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
@@ -348,21 +382,34 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return err
}
pulletPW, err := s.getOrCreatePulletProductWarehouse(c, warehouse.Id, dbTransaction, actorID)
if err != nil {
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
var conversionCategoryCode string
continue
switch category {
case string(utils.ProjectFlockCategoryGrowing):
conversionCategoryCode = "PULLET"
case string(utils.ProjectFlockCategoryLaying):
conversionCategoryCode = "LAYER"
default:
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category for conversion: %s", category))
}
if err := s.convertChickinsToPullet(c, chickins, pulletPW, dbTransaction, actorID); err != nil {
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, conversionCategoryCode, dbTransaction, actorID)
if err != nil {
return fmt.Errorf("failed to get/create %s product warehouse: %w", conversionCategoryCode, err)
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return err
}
} else if action == entity.ApprovalActionRejected {
chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID)
if err != nil {
s.Log.Warnf("failed to get chickins for rejection %d: %v", approvableID, err)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to get chickins for rejection %d: %w", approvableID, err)
}
if len(chickins) == 0 {
continue
}
@@ -371,12 +418,21 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
updates := map[string]any{"quantity": chickin.PendingUsageQty}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
s.Log.Warnf("failed to restore product warehouse quantity for rejection: %v", err)
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId))
}
return fmt.Errorf("failed to restore product warehouse quantity for chickin %d: %w", chickin.Id, err)
}
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to delete rejected chickin %d: %w", chickin.Id, err)
}
s.Log.Infof("chickin %d already deleted during rejection", chickin.Id)
}
}
}
}
return nil
})
@@ -387,7 +443,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
}
s.Log.Errorf("Failed to record approval for chickins %+v: %+v", approvableIDs, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
@@ -403,39 +458,39 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return updated, nil
}
func (s *chickinService) getOrCreatePulletProductWarehouse(ctx *fiber.Ctx, warehouseId uint, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) {
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) {
pulletProducts, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "PULLET", warehouseId)
if err == nil && len(pulletProducts) > 0 {
return &pulletProducts[0], nil
products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
if err == nil && len(products) > 0 {
return &products[0], nil
}
pulletProduct, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), "PULLET")
product, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), categoryCode)
if err != nil {
return nil, fmt.Errorf("failed to get PULLET product: %w", err)
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
}
if pulletProduct == nil {
return nil, fmt.Errorf("no PULLET product found in system")
if product == nil {
return nil, fmt.Errorf("no %s product found in system", categoryCode)
}
newPulletPW := &entity.ProductWarehouse{
ProductId: pulletProduct.Id,
newPW := &entity.ProductWarehouse{
ProductId: product.Id,
WarehouseId: warehouseId,
Quantity: 0,
CreatedBy: actorID,
}
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPulletPW, nil); err != nil {
return nil, fmt.Errorf("failed to create PULLET product warehouse: %w", err)
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err)
}
return newPulletPW, nil
return newPW, nil
}
func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []entity.ProjectChickin, pulletPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
if pulletPW == nil || pulletPW.Id == 0 {
return fmt.Errorf("invalid PULLET product warehouse")
if targetPW == nil || targetPW.Id == 0 {
return fmt.Errorf("invalid target product warehouse")
}
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
@@ -444,6 +499,16 @@ func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []enti
for _, chickin := range chickins {
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
if err != nil {
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
}
if populationExists {
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
continue
}
quantityToConvert := chickin.PendingUsageQty
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
@@ -453,52 +518,31 @@ func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []enti
return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err)
}
// Update quantity di PULLET product warehouse dengan quantity dari chickin
if err := productWarehouseTx.PatchOne(ctx.Context(), pulletPW.Id, map[string]any{
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
}, nil); err != nil {
s.Log.Warnf("failed to update PULLET warehouse quantity: %v", err)
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
}
return fmt.Errorf("failed to update target warehouse quantity: %w", err)
}
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
if err != nil {
s.Log.Warnf("failed to check population existence for chickin %d: %v", chickin.Id, err)
continue
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: targetPW.Id,
TotalQty: quantityToConvert,
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
}
if !populationExists {
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: pulletPW.Id,
TotalQty: quantityToConvert,
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
lower := strings.ToLower(err.Error())
if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) {
return err
}
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
lower := strings.ToLower(err.Error())
if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) {
return err
}
s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err)
}
} else {
s.Log.Infof("population already exists for chickin %d", chickin.Id)
s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err)
}
}
return nil
}
func uniqueUintSlice(values []uint) []uint {
seen := make(map[uint]struct{}, len(values))
result := make([]uint, 0, len(values))
for _, v := range values {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
result = append(result, v)
}
return result
}
@@ -8,8 +8,9 @@ import (
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
@@ -27,7 +28,7 @@ type ProjectFlockCustomDTO struct {
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"`
Kandangs []KandangCustomDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -39,22 +40,11 @@ type KandangCustomDTO struct {
Status string `json:"status"`
}
type ProductBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type WarehouseBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
type ProductWarehouseDTO struct {
Id uint `json:"id"`
Quantity float64 `json:"quantity"`
Product *ProductBaseDTO `json:"product,omitempty"`
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
Id uint `json:"id"`
Quantity float64 `json:"quantity"`
Product *productDTO.ProductBaseDTO `json:"product,omitempty"`
Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"`
}
type AvailableQtyDTO struct {
@@ -95,74 +85,38 @@ func toProjectFlockCustomDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFl
Category: pf.Category,
Fcr: pf.Fcr,
Location: pf.Location,
Kandangs: pf.Kandangs,
Kandangs: buildKandangCustomDTOs(pf.Kandangs),
CreatedUser: pf.CreatedUser,
CreatedAt: pf.CreatedAt,
UpdatedAt: pf.UpdatedAt,
}
}
func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO {
if len(chickins) == 0 {
return nil
func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO {
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
if e.ProjectFlock.Id != 0 {
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
projectFlockSummary = &mapped
}
availableQtyMap := make(map[uint]AvailableQtyDTO)
for _, ch := range chickins {
if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 {
continue
}
if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists {
continue
}
pwDTO := &ProductWarehouseDTO{
Id: ch.ProductWarehouse.Id,
Quantity: ch.ProductWarehouse.Quantity,
}
if ch.ProductWarehouse.Product.Id != 0 {
pwDTO.Product = &ProductBaseDTO{
Id: ch.ProductWarehouse.Product.Id,
Name: ch.ProductWarehouse.Product.Name,
}
}
if ch.ProductWarehouse.Warehouse.Id != 0 {
pwDTO.Warehouse = &WarehouseBaseDTO{
Id: ch.ProductWarehouse.Warehouse.Id,
Name: ch.ProductWarehouse.Warehouse.Name,
Type: ch.ProductWarehouse.Warehouse.Type,
}
}
availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{
ProductWarehouse: pwDTO,
}
return ProjectFlockKandangListDTO{
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary),
Kandang: buildKandangFromEntity(e.Kandang),
Chickins: buildChickins(e.Chickins),
AvailableQtys: buildAvailableQtysFromRaw(availableQtysRaw),
CreatedAt: e.CreatedAt,
CreatedUser: buildCreatedUser(e.ProjectFlock),
Approval: defaultProjectFlockKandangLatestApproval(e),
}
if len(availableQtyMap) == 0 {
return nil
}
result := make([]AvailableQtyDTO, 0, len(availableQtyMap))
for _, v := range availableQtyMap {
result = append(result, v)
}
return result
}
func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO {
if len(chickins) == 0 {
return nil
func toKandangCustomDTO(k projectFlockDTO.KandangWithProjectFlockIdDTO) KandangCustomDTO {
return KandangCustomDTO{
Id: k.Id,
Name: k.Name,
Status: k.Status,
}
result := make([]chickinDTO.ChickinBaseDTO, len(chickins))
for i, ch := range chickins {
result[i] = chickinDTO.ToChickinBaseDTO(ch)
}
return result
}
func defaultProjectFlockKandangLatestApproval(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO {
@@ -180,32 +134,14 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
projectFlockSummary = &mapped
}
var kandangSummary *KandangCustomDTO
if e.Kandang.Id != 0 {
kandangSummary = &KandangCustomDTO{
Id: e.Kandang.Id,
Name: e.Kandang.Name,
Status: e.Kandang.Status,
}
}
var createdUser *userDTO.UserBaseDTO
if e.ProjectFlock.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser)
createdUser = &mapped
} else if e.ProjectFlock.CreatedBy != 0 {
mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)}
createdUser = &mapped
}
return ProjectFlockKandangListDTO{
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary),
Kandang: kandangSummary,
Kandang: buildKandangFromEntity(e.Kandang),
Chickins: buildChickins(e.Chickins),
AvailableQtys: buildAvailableQtys(e.Chickins),
CreatedAt: e.CreatedAt,
CreatedUser: createdUser,
CreatedUser: buildCreatedUser(e.ProjectFlock),
Approval: defaultProjectFlockKandangLatestApproval(e),
}
}
@@ -224,69 +160,6 @@ func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKa
}
}
func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO {
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
if e.ProjectFlock.Id != 0 {
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
projectFlockSummary = &mapped
}
var kandangSummary *KandangCustomDTO
if e.Kandang.Id != 0 {
kandangSummary = &KandangCustomDTO{
Id: e.Kandang.Id,
Name: e.Kandang.Name,
Status: e.Kandang.Status,
}
}
var createdUser *userDTO.UserBaseDTO
if e.ProjectFlock.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser)
createdUser = &mapped
} else if e.ProjectFlock.CreatedBy != 0 {
mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)}
createdUser = &mapped
}
// convert available qtys from raw map data
var availableQtys []AvailableQtyDTO
if len(availableQtysRaw) > 0 {
availableQtys = buildAvailableQtysFromRaw(availableQtysRaw)
}
return ProjectFlockKandangListDTO{
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary),
Kandang: kandangSummary,
Chickins: buildChickins(e.Chickins),
AvailableQtys: availableQtys,
CreatedAt: e.CreatedAt,
CreatedUser: createdUser,
Approval: defaultProjectFlockKandangLatestApproval(e),
}
}
func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO {
if len(availableQtysRaw) == 0 {
return nil
}
result := make([]AvailableQtyDTO, len(availableQtysRaw))
for i, v := range availableQtysRaw {
pwData, ok := v["product_warehouse"].(map[string]interface{})
if !ok {
continue
}
pwDTO := buildProductWarehouseFromMap(pwData)
result[i] = AvailableQtyDTO{
ProductWarehouse: pwDTO,
}
}
return result
}
func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehouseDTO {
if pwData == nil {
return nil
@@ -315,12 +188,12 @@ func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehou
return dto
}
func buildProductFromMap(pData map[string]interface{}) *ProductBaseDTO {
func buildProductFromMap(pData map[string]interface{}) *productDTO.ProductBaseDTO {
if pData == nil {
return nil
}
product := &ProductBaseDTO{}
product := &productDTO.ProductBaseDTO{}
if id, ok := pData["id"].(float64); ok {
product.Id = uint(id)
} else if id, ok := pData["id"].(uint); ok {
@@ -332,12 +205,12 @@ func buildProductFromMap(pData map[string]interface{}) *ProductBaseDTO {
return product
}
func buildWarehouseFromMap(wData map[string]interface{}) *WarehouseBaseDTO {
func buildWarehouseFromMap(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO {
if wData == nil {
return nil
}
warehouse := &WarehouseBaseDTO{}
warehouse := &warehouseDTO.WarehouseBaseDTO{}
if id, ok := wData["id"].(float64); ok {
warehouse.Id = uint(id)
} else if id, ok := wData["id"].(uint); ok {
@@ -351,3 +224,123 @@ func buildWarehouseFromMap(wData map[string]interface{}) *WarehouseBaseDTO {
}
return warehouse
}
func buildCreatedUser(pf entity.ProjectFlock) *userDTO.UserBaseDTO {
if pf.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(pf.CreatedUser)
return &mapped
} else if pf.CreatedBy != 0 {
return &userDTO.UserBaseDTO{
Id: pf.CreatedBy,
IdUser: int64(pf.CreatedBy),
}
}
return nil
}
func buildKandangCustomDTOs(kandangs []projectFlockDTO.KandangWithProjectFlockIdDTO) []KandangCustomDTO {
if len(kandangs) == 0 {
return nil
}
result := make([]KandangCustomDTO, len(kandangs))
for i, k := range kandangs {
result[i] = toKandangCustomDTO(k)
}
return result
}
func buildKandangFromEntity(kandang entity.Kandang) *KandangCustomDTO {
if kandang.Id == 0 {
return nil
}
return &KandangCustomDTO{
Id: kandang.Id,
Name: kandang.Name,
Status: kandang.Status,
}
}
func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO {
if len(chickins) == 0 {
return nil
}
result := make([]chickinDTO.ChickinBaseDTO, len(chickins))
for i, ch := range chickins {
result[i] = chickinDTO.ToChickinBaseDTO(ch)
}
return result
}
func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO {
if len(chickins) == 0 {
return nil
}
availableQtyMap := make(map[uint]AvailableQtyDTO)
for _, ch := range chickins {
if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 {
continue
}
if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists {
continue
}
pwDTO := &ProductWarehouseDTO{
Id: ch.ProductWarehouse.Id,
Quantity: ch.ProductWarehouse.Quantity,
}
if ch.ProductWarehouse.Product.Id != 0 {
pwDTO.Product = &productDTO.ProductBaseDTO{
Id: ch.ProductWarehouse.Product.Id,
Name: ch.ProductWarehouse.Product.Name,
}
}
if ch.ProductWarehouse.Warehouse.Id != 0 {
pwDTO.Warehouse = &warehouseDTO.WarehouseBaseDTO{
Id: ch.ProductWarehouse.Warehouse.Id,
Name: ch.ProductWarehouse.Warehouse.Name,
Type: ch.ProductWarehouse.Warehouse.Type,
}
}
availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{
ProductWarehouse: pwDTO,
}
}
if len(availableQtyMap) == 0 {
return nil
}
result := make([]AvailableQtyDTO, 0, len(availableQtyMap))
for _, v := range availableQtyMap {
result = append(result, v)
}
return result
}
func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO {
if len(availableQtysRaw) == 0 {
return nil
}
result := make([]AvailableQtyDTO, len(availableQtysRaw))
for i, v := range availableQtysRaw {
pwData, ok := v["product_warehouse"].(map[string]interface{})
if !ok {
continue
}
pwDTO := buildProductWarehouseFromMap(pwData)
result[i] = AvailableQtyDTO{
ProductWarehouse: pwDTO,
}
}
return result
}
@@ -20,18 +20,25 @@ type ProjectFlockBaseDTO struct {
Period int `json:"period"`
}
type KandangWithProjectFlockIdDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
}
type ProjectFlockListDTO struct {
ProjectFlockBaseDTO
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
}
type ProjectFlockDetailDTO struct {
@@ -50,11 +57,24 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
createdUser = &mapped
}
var kandangSummaries []kandangDTO.KandangBaseDTO
var kandangSummaries []KandangWithProjectFlockIdDTO
if len(e.Kandangs) > 0 {
kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs))
kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs))
// Create a map of KandangId -> ProjectFlockKandangId from KandangHistory
kandangIdToProjectFlockKandangId := make(map[uint]uint)
for _, kh := range e.KandangHistory {
kandangIdToProjectFlockKandangId[kh.KandangId] = kh.Id
}
for i, kandang := range e.Kandangs {
kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang)
baseDTO := kandangDTO.ToKandangBaseDTO(kandang)
kandangSummaries[i] = KandangWithProjectFlockIdDTO{
Id: baseDTO.Id,
Name: baseDTO.Name,
Status: baseDTO.Status,
ProjectFlockKandangId: kandangIdToProjectFlockKandangId[kandang.Id],
}
}
}
@@ -85,7 +85,9 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs")
Preload("Kandangs").
Preload("KandangHistory").
Preload("KandangHistory.Kandang")
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {