feat[BE-127]: create available qty API and inisiate sales order and delivery order

This commit is contained in:
aguhh18
2025-11-07 13:24:48 +07:00
parent ba12320d12
commit 6e69e97d26
24 changed files with 695 additions and 68 deletions
@@ -71,6 +71,10 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
if period := c.QueryInt("period", 0); period > 0 {
query.Period = period
}
if category := c.Query("category", ""); category != "" {
query.Category = category
}
if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" {
ids, err := parseUintList(kandangRaw)
@@ -14,6 +14,7 @@ type ProjectFlockPopulationRepository interface {
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
// subset of base repository methods used by services
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
@@ -91,3 +92,17 @@ func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProd
}
return records, nil
}
func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
var total float64
err := r.DB().WithContext(ctx).
Table("project_flock_populations").
Select("COALESCE(SUM(total_qty), 0) AS total_qty").
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
@@ -127,6 +127,9 @@ func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *vali
return db
}
if params.Category != "" {
db = db.Where("project_flocks.category = ?", params.Category)
}
if params.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", params.AreaId)
}
@@ -28,5 +28,5 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval)
route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary)
}
@@ -27,6 +27,7 @@ type Query struct {
AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"`
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
Period int `query:"period" validate:"omitempty,number,gt=0"`
Category string `query:"category" validate:"omitempty"`
KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"`
}
@@ -1,6 +1,7 @@
package controller
import (
"fmt"
"math"
"strconv"
@@ -24,16 +25,8 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)),
TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)),
TransferDateFrom: c.Query("transfer_date_from", ""),
TransferDateTo: c.Query("transfer_date_to", ""),
ApprovalStatus: c.Query("approval_status", ""),
TransferNumber: c.Query("transfer_number", ""),
Sort: c.Query("sort", "created_at"),
Order: c.Query("order", "desc"),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
}
if query.Page < 1 || query.Limit < 1 {
@@ -45,8 +38,13 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
return err
}
data := make([]dto.TransferLayingDetailDTO, len(result))
for i, transfer := range result {
data[i] = dto.ToTransferLayingDetailDTOWithSingleApproval(transfer, transfer.LatestApproval)
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{
JSON(response.SuccessWithPaginate[dto.TransferLayingDetailDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transferLayings successfully",
@@ -56,7 +54,7 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransferLayingListDTOs(result),
Data: data,
})
}
@@ -113,7 +111,7 @@ func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error {
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %s", err.Error()))
}
result, err := u.TransferLayingService.UpdateOne(c, req, uint(id))
@@ -126,7 +124,7 @@ func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error {
Code: fiber.StatusOK,
Status: "success",
Message: "Update transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, result.LatestApproval),
})
}
@@ -180,3 +178,40 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
Data: data,
})
}
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pf, kandangQtyMap, err := u.TransferLayingService.GetAvailableQtyPerKandang(c, uint(projectFlockID))
if err != nil {
return err
}
// Build kandang list response
kandangs := make([]dto.KandangAvailableQtyDTO, 0, len(kandangQtyMap))
for kandangPFID, qty := range kandangQtyMap {
kandangs = append(kandangs, dto.KandangAvailableQtyDTO{
ProjectFlockKandangId: kandangPFID,
AvailableQty: qty,
})
}
resp := dto.AvailableQtyForTransferDTO{
ProjectFlockId: pf.Id,
ProjectFlockCode: pf.FlockName,
Category: pf.Category,
Kandangs: kandangs,
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get available quantity successfully",
Data: resp,
})
}
@@ -77,6 +77,20 @@ type TransferLayingDetailDTO struct {
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
}
// === Available Quantity DTOs ===
type KandangAvailableQtyDTO struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
AvailableQty float64 `json:"available_qty"`
}
type AvailableQtyForTransferDTO struct {
ProjectFlockId uint `json:"project_flock_id"`
ProjectFlockCode string `json:"project_flock_code"`
Category string `json:"category"`
Kandangs []KandangAvailableQtyDTO `json:"kandangs"`
}
// === Mapper Functions ===
func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO {
@@ -207,7 +221,6 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
var latestApproval *approvalDTO.ApprovalBaseDTO
// Use LatestApproval from entity if available
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = &mapped
@@ -27,4 +27,5 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals", ctrl.Approval)
route.Get("/project-flocks/:project_flock_id/available-qty", ctrl.GetAvailableQtyPerKandang)
}
@@ -31,6 +31,7 @@ type TransferLayingService interface {
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error)
}
type transferLayingService struct {
@@ -92,32 +93,7 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.SourceProjectFlockId != 0 {
db = db.Where("from_project_flock_id = ?", params.SourceProjectFlockId)
}
if params.TargetProjectFlockId != 0 {
db = db.Where("to_project_flock_id = ?", params.TargetProjectFlockId)
}
if params.TransferDateFrom != "" {
db = db.Where("transfer_date >= ?", params.TransferDateFrom)
}
if params.TransferDateTo != "" {
db = db.Where("transfer_date <= ?", params.TransferDateTo)
}
if params.TransferNumber != "" {
db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%")
}
sortField := "created_at"
if params.Sort != "" {
sortField = params.Sort
}
sortOrder := "DESC"
if params.Order == "asc" {
sortOrder = "ASC"
}
db = db.Order(fmt.Sprintf("%s %s", sortField, sortOrder))
db = db.Order("created_at DESC")
return db
})
@@ -126,19 +102,14 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
return nil, 0, err
}
if params.ApprovalStatus != "" {
var filtered []entity.LayingTransfer
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
for _, transfer := range transferLayings {
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil)
if err == nil && latestApproval != nil && latestApproval.Action != nil {
if string(*latestApproval.Action) == params.ApprovalStatus {
filtered = append(filtered, transfer)
}
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
for i, transfer := range transferLayings {
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err == nil && latestApproval != nil {
transferLayings[i].LatestApproval = latestApproval
}
transferLayings = filtered
}
return transferLayings, total, nil
@@ -155,7 +126,9 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran
}
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil)
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err == nil && latestApproval != nil {
transferLaying.LatestApproval = latestApproval
}
@@ -547,7 +520,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action))
}
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
@@ -851,8 +823,6 @@ func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Contex
return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration")
}
// Restore in LIFO order (from newest to oldest)
// Add all quantity back to the last (newest) population
if len(populations) > 0 {
lastPop := populations[len(populations)-1]
newQty := lastPop.TotalQty + quantityToRestore
@@ -863,3 +833,37 @@ func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Contex
return nil
}
func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) {
pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db
})
if err != nil {
s.Log.Errorf("Failed to get project flock %d: %+v", projectFlockID, err)
return nil, nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
}
kandangs, _, err := s.ProjectFlockKandangRepo.GetAll(ctx.Context(), 0, 1000, func(db *gorm.DB) *gorm.DB {
return db.Where("project_flock_id = ?", projectFlockID).Order("kandang_id ASC")
})
if err != nil {
s.Log.Errorf("Failed to get kandangs for project flock %d: %+v", projectFlockID, err)
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
}
kandangAvailableQty := make(map[uint]float64)
for _, kandang := range kandangs {
totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
if err != nil {
s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err)
kandangAvailableQty[kandang.Id] = 0
continue
}
kandangAvailableQty[kandang.Id] = totalQty
}
return pf, kandangAvailableQty, nil
}
@@ -29,16 +29,8 @@ type Update struct {
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
SourceProjectFlockId uint `query:"source_project_flock_id" validate:"omitempty"`
TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"`
TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"`
TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"`
ApprovalStatus string `query:"approval_status" validate:"omitempty,oneof=PENDING APPROVED REJECTED"` // Filter by latest approval status
TransferNumber string `query:"transfer_number" validate:"omitempty"` // Search by transfer number
Sort string `query:"sort" validate:"omitempty,oneof=created_at transfer_date"` // Sort by field
Order string `query:"order" validate:"omitempty,oneof=asc desc"` // Sort order
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
}
type Approve struct {