Feat[BE-261,265]: add category request body on create on

This commit is contained in:
aguhh18
2025-11-20 08:27:02 +07:00
parent 105b20c333
commit 4c7e5b0731
4 changed files with 206 additions and 28 deletions
@@ -2,6 +2,7 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
@@ -65,7 +66,7 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
Preload("CreatedUser").
Preload("Supplier").
Preload("Nonstocks", func(db *gorm.DB) *gorm.DB {
return db.Preload("Nonstock").Preload("Realization").Preload("ProjectFlockKandang")
return db.Preload("Nonstock").Preload("Realization").Preload("ProjectFlockKandang.Kandang.Location")
})
}
@@ -150,15 +151,6 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return nil, err
}
// Get supplier for category
supplierEntity, err := s.SupplierRepo.GetByID(c.Context(), uint(req.SupplierID), nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier")
}
for _, costPerKandang := range req.CostPerKandangs {
for _, costItem := range costPerKandang.CostItems {
nonstockId := uint(costItem.NonstockID)
@@ -220,7 +212,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
expense = &entity.Expense{
ReferenceNumber: &referenceNumber,
PoNumber: req.PoNumber,
Category: supplierEntity.Category,
Category: req.Category,
SupplierId: &req.SupplierID,
ExpenseDate: expenseDate,
GrandTotal: grandTotal,
@@ -276,12 +268,36 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
&approvalAction,
createdByUint,
nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval")
} // TODO: Handle documents (save file references)
// Handle documents - save file paths as JSON array
if len(req.Documents) > 0 {
documentPaths := make([]string, len(req.Documents))
for i, doc := range req.Documents {
// Generate dummy path for each document
// In real implementation, this would save the file and return the actual path
documentPath := fmt.Sprintf("/documents/expenses/%d_%d_%s", expense.Id, i+1, doc.Filename)
documentPaths[i] = documentPath
}
// Save document paths as JSON in expense record
documentPathsJSON, err := json.Marshal(documentPaths)
if err != nil {
s.Log.Errorf("Failed to marshal document paths: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save document references")
}
if err := expenseRepoTx.PatchOne(c.Context(), uint(expense.Id), map[string]interface{}{
"document_path": string(documentPathsJSON),
}, nil); err != nil {
s.Log.Errorf("Failed to save document paths: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save document references")
}
}
return nil
})
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
@@ -308,6 +324,23 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
return nil, err
}
// Get latest approval to validate workflow
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to get latest approval for expense %d: %+v", id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
}
// Check if expense can be updated (must be before Realisasi step)
if latestApproval != nil {
if latestApproval.StepNumber >= uint16(utils.ExpenseStepRealisasi) {
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
s.Log.Errorf("Cannot update expense at %s step. Must be before Realisasi step", currentStepName)
return nil, fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("Cannot update expense at %s step. Must be before Realisasi step", currentStepName))
}
}
updateBody := make(map[string]any)
if req.SupplierID != nil {
@@ -324,16 +357,135 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["supplier_id"] = *req.SupplierID
}
if len(updateBody) == 0 {
if req.TransactionDate != nil {
// Parse transaction_date
expenseDate, err := utils.ParseDateString(*req.TransactionDate)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format")
}
updateBody["expense_date"] = expenseDate
}
if req.PoNumber != nil {
updateBody["po_number"] = *req.PoNumber
}
if len(updateBody) == 0 && req.CostPerKandang == nil {
return s.getExpenseWithDetails(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
// Update expense using transaction
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
expenseRepoTx := repository.NewExpenseRepository(tx)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
// Update expense
if len(updateBody) > 0 {
if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
}
s.Log.Errorf("Failed to update expense: %+v", err)
return err
}
}
s.Log.Errorf("Failed to update expense: %+v", err)
return nil, err
// Update cost per kandang if provided
if req.CostPerKandang != nil {
// First, delete existing expense nonstocks using GORM
if err := tx.Where("expense_id = ?", id).Delete(&entity.ExpenseNonstock{}).Error; err != nil {
s.Log.Errorf("Failed to delete existing expense nonstocks: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense items")
}
// Calculate new grand total
var grandTotal float64
for _, cpk := range *req.CostPerKandang {
for _, costItem := range cpk.CostItems {
grandTotal += costItem.TotalCost
}
}
// Update expense grand total
if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{
"grand_total": grandTotal,
}, nil); err != nil {
s.Log.Errorf("Failed to update expense grand total: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense grand total")
}
// Create new expense nonstocks
for _, cpk := range *req.CostPerKandang {
// Get active project_flock_kandang for this kandang
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang")
}
for _, costItem := range cpk.CostItems {
// Validate nonstock exists
nonstockId := uint(costItem.NonstockID)
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Nonstock", ID: &nonstockId, Exists: s.NonstockRepo.IdExists},
); err != nil {
return err
}
projectFlockKandangId := uint64(projectFlockKandang.Id)
expenseId := uint64(id)
expenseNonstock := &entity.ExpenseNonstock{
ExpenseId: &expenseId,
ProjectFlockKandangId: &projectFlockKandangId,
NonstockId: &costItem.NonstockID,
Qty: costItem.Quantity,
TotalPrice: costItem.TotalCost,
Note: &costItem.Notes,
}
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
s.Log.Errorf("Failed to create expense nonstock: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense cost item")
}
}
}
}
// Reset approval step to Pengajuan
actorID := uint(1) // TODO: replace with authenticated user id
var approvalAction entity.ApprovalAction
// Check if latest approval was Updated, then use Updated action, otherwise use Created
if latestApproval != nil && latestApproval.Action != nil && *latestApproval.Action == entity.ApprovalActionUpdated {
approvalAction = entity.ApprovalActionUpdated
} else {
approvalAction = entity.ApprovalActionCreated
}
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowExpense,
id,
utils.ExpenseStepPengajuan,
&approvalAction,
actorID,
nil); err != nil {
s.Log.Errorf("Failed to reset approval to Pengajuan for expense %d: %+v", id, err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense")
}
return s.getExpenseWithDetails(c, id)