Merge branch 'dev/hafizh' into 'feat/BE/US-82/approval-workflow'

[FIX/BE] Req body approval and fix projectflock.dto

See merge request mbugroup/lti-api!34
This commit is contained in:
Hafizh A. Y.
2025-10-22 09:53:36 +00:00
6 changed files with 124 additions and 115 deletions
@@ -191,29 +191,33 @@ func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error {
}
func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProjectflockService.Approval(c, uint(id), req)
results, err := u.ProjectflockService.Approval(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit projectflock approval successfully"
)
if len(results) == 1 {
data = dto.ToProjectFlockListDTO(results[0])
} else {
message = "Submit projectflock approvals successfully"
data = dto.ToProjectFlockListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Submit projectflock approval successfully",
Data: dto.ToProjectFlockListDTO(*result),
Message: message,
Data: data,
})
}
@@ -16,13 +16,8 @@ import (
)
type ProjectFlockBaseDTO struct {
Id uint `json:"id"`
Period int `json:"period"`
Category string `json:"category"`
Flock *flockDTO.FlockBaseDTO `json:"flock"`
Area *areaDTO.AreaBaseDTO `json:"area"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Id uint `json:"id"`
Period int `json:"period"`
}
type ProjectFlockListDTO struct {
@@ -63,6 +58,30 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
}
}
var flockSummary *flockDTO.FlockBaseDTO
if e.Flock.Id != 0 {
mapped := flockDTO.ToFlockBaseDTO(e.Flock)
flockSummary = &mapped
}
var areaSummary *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
areaSummary = &mapped
}
var fcrSummary *fcrDTO.FcrBaseDTO
if e.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrBaseDTO(e.Fcr)
fcrSummary = &mapped
}
var locationSummary *locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
locationSummary = &mapped
}
latestApproval := defaultProjectFlockLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
@@ -71,7 +90,12 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
return ProjectFlockListDTO{
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
Flock: flockSummary,
Area: areaSummary,
Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
@@ -105,13 +129,6 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv
result.StepName = label
}
}
if result.StepName == "" {
result.StepName = "Pengajuan"
}
if !e.CreatedAt.IsZero() {
result.ActionAt = e.CreatedAt
}
if e.CreatedUser.Id != 0 {
result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser)
@@ -126,38 +143,9 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv
}
func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
var flock *flockDTO.FlockBaseDTO
if e.Flock.Id != 0 {
mapped := flockDTO.ToFlockBaseDTO(e.Flock)
flock = &mapped
}
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
var fcr *fcrDTO.FcrBaseDTO
if e.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrBaseDTO(e.Fcr)
fcr = &mapped
}
var location *locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
location = &mapped
}
return ProjectFlockBaseDTO{
Id: e.Id,
Period: e.Period,
Category: e.Category,
Flock: flock,
Area: area,
Fcr: fcr,
Location: location,
Id: e.Id,
Period: e.Period,
}
}
@@ -25,6 +25,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Post("/:id/approvals", ctrl.Approval)
route.Post("/approvals", ctrl.Approval)
route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary)
}
@@ -29,7 +29,7 @@ type ProjectflockService interface {
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
Approval(ctx *fiber.Ctx, id uint, req *validation.Approve) (*entity.ProjectFlock, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
}
type projectflockService struct {
@@ -75,7 +75,7 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs.Location")
Preload("Kandangs")
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
@@ -219,8 +219,8 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err
}
category, ok := utils.NormalizeProjectFlockCategory(req.Category)
if !ok {
cat := strings.ToUpper(req.Category)
if !utils.IsValidProjectFlockCategory(cat) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
}
@@ -257,7 +257,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
createBody := &entity.ProjectFlock{
FlockId: req.FlockId,
AreaId: req.AreaId,
Category: string(category),
Category: cat,
FcrId: req.FcrId,
LocationId: req.LocationId,
CreatedBy: 1,
@@ -342,11 +342,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
})
}
if req.Category != nil {
if normalized, ok := utils.NormalizeProjectFlockCategory(*req.Category); ok {
updateBody["category"] = string(normalized)
} else {
cat := strings.ToUpper(*req.Category)
if !utils.IsValidProjectFlockCategory(cat) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
}
updateBody["category"] = cat
}
if req.FcrId != nil {
updateBody["fcr_id"] = *req.FcrId
@@ -500,17 +501,11 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return s.GetOne(c, id)
}
func (s projectflockService) Approval(c *fiber.Ctx, id uint, req *validation.Approve) (*entity.ProjectFlock, error) {
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
project, err := s.GetOne(c, id)
if err != nil {
s.Log.Errorf("Failed to fetch projectflock %d before approval: %+v", id, err)
return nil, err
}
actorID := uint(1) // TODO: change from auth context
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
@@ -522,42 +517,58 @@ func (s projectflockService) Approval(c *fiber.Ctx, id uint, req *validation.App
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
approvableIDs := uniqueUintSlice(req.ApprovableIds)
if len(approvableIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
step := utils.ProjectFlockStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.ProjectFlockStepAktif
}
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 {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
project.Id,
step,
&action,
actorID,
req.Notes,
); err != nil {
return err
}
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
switch action {
case entity.ApprovalActionApproved:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
for _, approvableID := range approvableIDs {
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
}
return err
}
if _, err := approvalSvc.CreateApproval(
c.Context(),
project.Id,
utils.KandangStatusActive,
utils.ApprovalWorkflowProjectFlock,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
return err
}
case entity.ApprovalActionRejected:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
project.Id,
utils.KandangStatusNonActive,
); err != nil {
return err
switch action {
case entity.ApprovalActionApproved:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
approvableID,
utils.KandangStatusActive,
); err != nil {
return err
}
case entity.ApprovalActionRejected:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
approvableID,
utils.KandangStatusNonActive,
); err != nil {
return err
}
}
}
@@ -565,14 +576,26 @@ func (s projectflockService) Approval(c *fiber.Ctx, id uint, req *validation.App
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
s.Log.Errorf("Failed to record approval for projectflock %d: %+v", id, err)
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
return s.GetOne(c, id)
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
for _, approvableID := range approvableIDs {
project, err := s.GetOne(c, approvableID)
if err != nil {
return nil, err
}
updated = append(updated, *project)
}
return updated, nil
}
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
@@ -3,7 +3,7 @@ package validation
type Create struct {
FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict,oneof=growing laying GROWING LAYING"`
Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
@@ -12,7 +12,7 @@ type Create struct {
type Update struct {
FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
Category *string `json:"category,omitempty" validate:"omitempty,oneof=growing laying GROWING LAYING"`
Category *string `json:"category,omitempty" validate:"omitempty"`
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"`
@@ -22,7 +22,7 @@ type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=area location kandangs period"`
SortBy string `query:"sort_by" validate:"omitempty"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"`
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
@@ -31,6 +31,7 @@ type Query struct {
}
type Approve struct {
Action string `json:"action" validate:"required_strict"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
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"`
}
+5 -12
View File
@@ -250,19 +250,12 @@ func IsValidCustomerSupplierType(v string) bool {
return false
}
func NormalizeProjectFlockCategory(v string) (ProjectFlockCategory, bool) {
normalized := ProjectFlockCategory(strings.ToUpper(strings.TrimSpace(v)))
switch normalized {
case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying:
return normalized, true
default:
return "", false
}
}
func IsValidProjectFlockCategory(v string) bool {
_, ok := NormalizeProjectFlockCategory(v)
return ok
switch ProjectFlockCategory(v) {
case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying:
return true
}
return false
}
func IsValidSupplierCategory(v string) bool {