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 { 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) req := new(validation.Approve)
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") 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 { if err != nil {
return err 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). return c.Status(fiber.StatusOK).
JSON(response.Success{ JSON(response.Success{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Submit projectflock approval successfully", Message: message,
Data: dto.ToProjectFlockListDTO(*result), Data: data,
}) })
} }
@@ -16,13 +16,8 @@ import (
) )
type ProjectFlockBaseDTO struct { type ProjectFlockBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Period int `json:"period"` 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"`
} }
type ProjectFlockListDTO struct { 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) latestApproval := defaultProjectFlockLatestApproval(e)
if e.LatestApproval != nil { if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval) snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
@@ -71,7 +90,12 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
return ProjectFlockListDTO{ return ProjectFlockListDTO{
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
Flock: flockSummary,
Area: areaSummary,
Kandangs: kandangSummaries, Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser, CreatedUser: createdUser,
@@ -105,13 +129,6 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv
result.StepName = label result.StepName = label
} }
} }
if result.StepName == "" {
result.StepName = "Pengajuan"
}
if !e.CreatedAt.IsZero() {
result.ActionAt = e.CreatedAt
}
if e.CreatedUser.Id != 0 { if e.CreatedUser.Id != 0 {
result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser)
@@ -126,38 +143,9 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv
} }
func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { 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{ return ProjectFlockBaseDTO{
Id: e.Id, Id: e.Id,
Period: e.Period, Period: e.Period,
Category: e.Category,
Flock: flock,
Area: area,
Fcr: fcr,
Location: location,
} }
} }
@@ -25,6 +25,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
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("/:id/approvals", ctrl.Approval) route.Post("/approvals", ctrl.Approval)
route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary) 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) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, 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 { type projectflockService struct {
@@ -75,7 +75,7 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Area"). Preload("Area").
Preload("Fcr"). Preload("Fcr").
Preload("Location"). Preload("Location").
Preload("Kandangs.Location") Preload("Kandangs")
} }
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { 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 return nil, err
} }
category, ok := utils.NormalizeProjectFlockCategory(req.Category) cat := strings.ToUpper(req.Category)
if !ok { if !utils.IsValidProjectFlockCategory(cat) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") 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{ createBody := &entity.ProjectFlock{
FlockId: req.FlockId, FlockId: req.FlockId,
AreaId: req.AreaId, AreaId: req.AreaId,
Category: string(category), Category: cat,
FcrId: req.FcrId, FcrId: req.FcrId,
LocationId: req.LocationId, LocationId: req.LocationId,
CreatedBy: 1, CreatedBy: 1,
@@ -342,11 +342,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}) })
} }
if req.Category != nil { if req.Category != nil {
if normalized, ok := utils.NormalizeProjectFlockCategory(*req.Category); ok { cat := strings.ToUpper(*req.Category)
updateBody["category"] = string(normalized) if !utils.IsValidProjectFlockCategory(cat) {
} else {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
} }
updateBody["category"] = cat
} }
if req.FcrId != nil { if req.FcrId != nil {
updateBody["fcr_id"] = *req.FcrId 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) 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 { if err := s.Validate.Struct(req); err != nil {
return nil, err 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 actorID := uint(1) // TODO: change from auth context
var action entity.ApprovalAction var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) { 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") 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 step := utils.ProjectFlockStepPengajuan
if action == entity.ApprovalActionApproved { if action == entity.ApprovalActionApproved {
step = utils.ProjectFlockStepAktif 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)) 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) kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
switch action { projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
case entity.ApprovalActionApproved:
if err := kandangRepoTx.UpdateStatusByProjectFlockID( 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(), c.Context(),
project.Id, utils.ApprovalWorkflowProjectFlock,
utils.KandangStatusActive, approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil { ); err != nil {
return err return err
} }
case entity.ApprovalActionRejected:
if err := kandangRepoTx.UpdateStatusByProjectFlockID( switch action {
c.Context(), case entity.ApprovalActionApproved:
project.Id, if err := kandangRepoTx.UpdateStatusByProjectFlockID(
utils.KandangStatusNonActive, c.Context(),
); err != nil { approvableID,
return err 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 err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") 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 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 { func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
@@ -3,7 +3,7 @@ package validation
type Create struct { type Create struct {
FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"`
AreaId uint `json:"area_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"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_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"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
@@ -12,7 +12,7 @@ type Create struct {
type Update struct { type Update struct {
FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"`
AreaId *uint `json:"area_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"` FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_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"` 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"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"` 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"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"`
LocationId uint `query:"location_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 { type Approve struct {
Action string `json:"action" validate:"required_strict"` Action string `json:"action" validate:"required_strict"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` 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 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 { func IsValidProjectFlockCategory(v string) bool {
_, ok := NormalizeProjectFlockCategory(v) switch ProjectFlockCategory(v) {
return ok case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying:
return true
}
return false
} }
func IsValidSupplierCategory(v string) bool { func IsValidSupplierCategory(v string) bool {