From 886446b55f820c271862fb3ca426b2e4ab31a29f Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 27 Nov 2025 13:53:35 +0700 Subject: [PATCH] Feat[BE]: refactor expense API and expense table match with new ERD --- ...0251117034511_create_expenses_table.up.sql | 2 +- ...se_nostock_and_expense_ralization.down.sql | 0 ...ense_nostock_and_expense_ralization.up.sql | 44 ++++ internal/entities/expense.go | 12 +- internal/entities/expense_nonstock.go | 27 +- internal/entities/expense_realization.go | 16 +- .../controllers/expense.controller.go | 12 + internal/modules/expenses/dto/expense.dto.go | 99 ++++---- .../expenses/services/expense.service.go | 230 +++++++++++------- .../validations/expense.validation.go | 5 +- 10 files changed, 278 insertions(+), 169 deletions(-) create mode 100644 internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.down.sql create mode 100644 internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.up.sql diff --git a/internal/database/migrations/20251117034511_create_expenses_table.up.sql b/internal/database/migrations/20251117034511_create_expenses_table.up.sql index 8949d931..f5f20f2c 100644 --- a/internal/database/migrations/20251117034511_create_expenses_table.up.sql +++ b/internal/database/migrations/20251117034511_create_expenses_table.up.sql @@ -1,7 +1,7 @@ CREATE TABLE expenses ( id BIGSERIAL PRIMARY KEY, reference_number VARCHAR(50) UNIQUE NOT NULL, - supplier_id BIGINT NULL, + supplier_id BIGINT NOT NULL, category VARCHAR(50) NOT NULL CHECK ( category IN ('BOP', 'NON-BOP') ), diff --git a/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.down.sql b/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.up.sql b/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.up.sql new file mode 100644 index 00000000..ce71256b --- /dev/null +++ b/internal/database/migrations/20251125055613_update_expneses_expense_nostock_and_expense_ralization.up.sql @@ -0,0 +1,44 @@ +-- ============================ +-- EXPENSES +-- ============================ +ALTER TABLE expenses DROP COLUMN IF EXISTS grand_total; + +ALTER TABLE expenses RENAME COLUMN note TO notes; + +ALTER TABLE expenses RENAME COLUMN expense_date TO transaction_date; + +-- ============================ +-- EXPENSE_REALIZATIONS +-- ============================ +ALTER TABLE expense_realizations +RENAME COLUMN realization_qty TO qty; + +ALTER TABLE expense_realizations +RENAME COLUMN realization_unit_price TO price; + +ALTER TABLE expense_realizations RENAME COLUMN note TO notes; + +ALTER TABLE expense_realizations +DROP COLUMN IF EXISTS realization_total_price; + +ALTER TABLE expense_realizations +DROP COLUMN IF EXISTS realization_date; + +ALTER TABLE expense_realizations DROP COLUMN IF EXISTS created_by; + +ALTER TABLE expense_realizations +ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(); + +-- ============================ +-- EXPENSE_NONSTOCKS +-- ============================ +ALTER TABLE expense_nonstocks RENAME COLUMN note TO notes; + +ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS total_price; + +ALTER TABLE expense_nonstocks RENAME COLUMN unit_price TO price; + +ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS created_by; + +ALTER TABLE expense_nonstocks +ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(); \ No newline at end of file diff --git a/internal/entities/expense.go b/internal/entities/expense.go index 74998e6a..e6ab1d77 100644 --- a/internal/entities/expense.go +++ b/internal/entities/expense.go @@ -13,18 +13,16 @@ type Expense struct { SupplierId uint64 `gorm:""` Category string `gorm:"type:varchar(50);not null"` PoNumber string `gorm:"type:varchar(50)"` - DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan - RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi - RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi - ExpenseDate time.Time `gorm:"type:date;not null"` - GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` - Note string `gorm:"type:text"` + DocumentPath sql.NullString `gorm:"type:json"` + RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` + RealizationDate time.Time `gorm:"type:date;column:realization_date"` + TransactionDate time.Time `gorm:"type:date;not null"` + Notes string `gorm:"type:text;column:notes"` CreatedBy uint64 `gorm:""` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - // Relations Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"` diff --git a/internal/entities/expense_nonstock.go b/internal/entities/expense_nonstock.go index 7be2053a..ccd4194c 100644 --- a/internal/entities/expense_nonstock.go +++ b/internal/entities/expense_nonstock.go @@ -1,20 +1,23 @@ package entities -type ExpenseNonstock struct { - Id uint64 `gorm:"primaryKey;autoIncrement"` - ExpenseId *uint64 `gorm:""` - ProjectFlockKandangId *uint64 `gorm:""` - KandangId *uint64 `gorm:""` - NonstockId *uint64 `gorm:""` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` - Note string `gorm:"type:text"` +import ( + "time" +) + +type ExpenseNonstock struct { + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseId *uint64 `gorm:""` + ProjectFlockKandangId *uint64 `gorm:""` + KandangId *uint64 `gorm:""` + NonstockId *uint64 `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null"` + Price float64 `gorm:"type:numeric(15,3);not null;column:price"` + Notes string `gorm:"type:text;column:notes"` + CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` - // Relations Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"` Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"` - Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` + Realization *ExpenseRealization `gorm:"foreignKey:Id;references:ExpenseNonstockId"` } diff --git a/internal/entities/expense_realization.go b/internal/entities/expense_realization.go index 629fdfb7..3c4b1f07 100644 --- a/internal/entities/expense_realization.go +++ b/internal/entities/expense_realization.go @@ -5,16 +5,12 @@ import ( ) type ExpenseRealization struct { - Id uint64 `gorm:"primaryKey;autoIncrement"` - ExpenseNonstockId *uint64 `gorm:""` - RealizationQty float64 `gorm:"type:numeric(15,3);not null"` - RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"` - RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"` - RealizationDate time.Time `gorm:"type:date;not null"` - Note *string `gorm:"type:text"` - CreatedBy *uint64 `gorm:""` + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseNonstockId *uint64 `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null;"` + Price float64 `gorm:"type:numeric(15,3);not null;"` + Notes string `gorm:"type:text;"` + CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` - // Relations ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"` - CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go index 08256b24..16c07fda 100644 --- a/internal/modules/expenses/controllers/expense.controller.go +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -155,6 +155,18 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error { req.TransactionDate = &transactionDate } + categoryVal := c.FormValue("category") + req.Category = &categoryVal + + supplierIDVal := c.FormValue("supplier_id") + if supplierIDVal != "" { + supplierID, err := strconv.ParseUint(supplierIDVal, 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format") + } + req.SupplierID = &supplierID + } + costPerKandangJSON := c.FormValue("cost_per_kandang") if costPerKandangJSON != "" { var costPerKandang []validation.CostPerKandang diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index bee50c6d..ea407512 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -15,10 +15,9 @@ import ( // === DTO Structs === type ExpenseRelationDTO struct { - Id uint64 `json:"id"` - PoNumber string `json:"po_number"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` + Id uint64 `json:"id"` + PoNumber string `json:"po_number"` + TransactionDate time.Time `json:"transaction_date"` } type ExpenseBaseDTO struct { @@ -28,8 +27,8 @@ type ExpenseBaseDTO struct { Category string `json:"category"` Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"` RealizationDate *time.Time `json:"realization_date,omitempty"` - ExpenseDate time.Time `json:"expense_date"` - GrandTotal float64 `json:"grand_total"` + TransactionDate time.Time `json:"transaction_date"` + Notes string `json:"notes"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` } @@ -55,21 +54,26 @@ type ExpenseDetailDTO struct { } type ExpenseNonstockDTO struct { - Id uint64 `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` - Note *string `json:"note,omitempty"` - Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + Id uint64 `json:"id"` + ExpenseId *uint64 `json:"expense_id,omitempty"` + ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"` + KandangId *uint64 `json:"kandang_id,omitempty"` + NonstockId *uint64 `json:"nonstock_id,omitempty"` + Qty float64 `json:"qty"` + Price float64 `json:"price"` + Notes string `json:"notes"` + Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + CreatedAt time.Time `json:"created_at"` } type ExpenseRealizationDTO struct { - Id uint64 `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` - Note *string `json:"note,omitempty"` - Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + Id uint64 `json:"id"` + ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"` + Qty float64 `json:"qty"` + Price float64 `json:"price"` + Notes string `json:"notes"` + Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` + CreatedAt time.Time `json:"created_at"` } type KandangGroupDTO struct { @@ -89,10 +93,9 @@ type DocumentDTO struct { func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO { return ExpenseRelationDTO{ - Id: e.Id, - PoNumber: e.PoNumber, - ExpenseDate: e.ExpenseDate, - GrandTotal: e.GrandTotal, + Id: e.Id, + PoNumber: e.PoNumber, + TransactionDate: e.TransactionDate, } } @@ -124,8 +127,8 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO { Category: e.Category, Supplier: supplier, RealizationDate: realizationDate, - ExpenseDate: e.ExpenseDate, - GrandTotal: e.GrandTotal, + TransactionDate: e.TransactionDate, + Notes: e.Notes, Location: location, } } @@ -192,10 +195,9 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { for _, ns := range e.Nonstocks { pengajuanDTO := ToExpenseNonstockDTO(ns) - pengajuans = append(pengajuans, pengajuanDTO) - if ns.Realization != nil && ns.Realization.Id != 0 { + if ns.Realization != nil { var nonstock *nonstockDTO.NonstockRelationDTO if ns.Nonstock != nil && ns.Nonstock.Id != 0 { mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock) @@ -203,12 +205,13 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { } realisasiDTO := ExpenseRealizationDTO{ - Id: ns.Realization.Id, - Qty: ns.Realization.RealizationQty, - UnitPrice: ns.Realization.RealizationUnitPrice, - TotalPrice: ns.Realization.RealizationTotalPrice, - Note: ns.Realization.Note, - Nonstock: nonstock, + Id: ns.Realization.Id, + ExpenseNonstockId: ns.Realization.ExpenseNonstockId, + Qty: ns.Realization.Qty, + Price: ns.Realization.Price, + Notes: ns.Realization.Notes, + Nonstock: nonstock, + CreatedAt: ns.Realization.CreatedAt, } realisasi = append(realisasi, realisasiDTO) } @@ -217,12 +220,12 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO { var totalPengajuan float64 for _, p := range pengajuans { - totalPengajuan += p.TotalPrice + totalPengajuan += p.Qty * p.Price } var totalRealisasi float64 for _, r := range realisasi { - totalRealisasi += r.TotalPrice + totalRealisasi += r.Qty * r.Price } kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks) @@ -248,12 +251,16 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO { } return ExpenseNonstockDTO{ - Id: ns.Id, - Qty: ns.Qty, - UnitPrice: ns.UnitPrice, - TotalPrice: ns.TotalPrice, - Note: &ns.Note, - Nonstock: nonstock, + Id: ns.Id, + ExpenseId: ns.ExpenseId, + ProjectFlockKandangId: ns.ProjectFlockKandangId, + KandangId: ns.KandangId, + NonstockId: ns.NonstockId, + Qty: ns.Qty, + Price: ns.Price, + Notes: ns.Notes, + Nonstock: nonstock, + CreatedAt: ns.CreatedAt, } } @@ -264,11 +271,13 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali var kandangId uint64 var kandangName string - for _, ns := range nonstocks { - if ns.Id == p.Id && ns.Kandang != nil { - kandangId = uint64(ns.Kandang.Id) - kandangName = ns.Kandang.Name - break + if p.KandangId != nil { + kandangId = *p.KandangId + for _, ns := range nonstocks { + if ns.Id == p.Id && ns.Kandang != nil { + kandangName = ns.Kandang.Name + break + } } } diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index eb760494..8f1cf450 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "mime/multipart" - "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" @@ -189,21 +188,13 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number") } - var grandTotal float64 - for _, costPerKandang := range req.CostPerKandangs { - for _, costItem := range costPerKandang.CostItems { - grandTotal += costItem.TotalCost - } - } - createdBy := uint64(1) //todo get from auth expense = &entity.Expense{ ReferenceNumber: referenceNumber, PoNumber: req.PoNumber, Category: req.Category, SupplierId: req.SupplierID, - ExpenseDate: expenseDate, - GrandTotal: grandTotal, + TransactionDate: expenseDate, CreatedBy: createdBy, } @@ -249,8 +240,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen KandangId: kandangId, NonstockId: &nonstockId, Qty: costItem.Quantity, - TotalPrice: costItem.TotalCost, - Note: costItem.Notes, + Price: costItem.Price, + Notes: costItem.Notes, } if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { @@ -326,7 +317,24 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") } - updateBody["expense_date"] = expenseDate + updateBody["transaction_date"] = expenseDate + } + + if req.Category != nil { + updateBody["category"] = *req.Category + } + + if req.SupplierID != nil { + supplierID := uint(*req.SupplierID) + supplierExistsFunc := func(ctx context.Context, id uint) (bool, error) { + return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id) + } + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: supplierExistsFunc}, + ); err != nil { + return nil, err + } + updateBody["supplier_id"] = *req.SupplierID } if len(updateBody) == 0 && req.CostPerKandang == nil && len(req.Documents) == 0 { @@ -344,6 +352,21 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) + currentExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense") + } + + categoryChanged := false + var newCategory string + if req.Category != nil && *req.Category != currentExpense.Category { + categoryChanged = true + newCategory = *req.Category + } + if len(updateBody) > 0 { if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -353,39 +376,77 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } } + if categoryChanged { + if currentExpense.Category == "BOP" && newCategory == "NON-BOP" { + + var existingExpenseNonstocks []entity.ExpenseNonstock + if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks") + } + + for _, ens := range existingExpenseNonstocks { + updateData := map[string]interface{}{ + "project_flock_kandang_id": nil, + } + if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null") + } + } + } else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" { + + var existingExpenseNonstocks []entity.ExpenseNonstock + if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks") + } + + projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) + for _, ens := range existingExpenseNonstocks { + if ens.KandangId != nil { + projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.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") + } + projectFlockKandangId := uint64(projectFlockKandang.Id) + + updateData := map[string]interface{}{ + "project_flock_kandang_id": projectFlockKandangId, + } + if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id") + } + } + } + } + } + if req.CostPerKandang != nil { - if err := tx.Where("expense_id = ?", id).Delete(&entity.ExpenseNonstock{}).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense items") + var existingExpenseNonstocks []entity.ExpenseNonstock + if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks for deletion") } - var grandTotal float64 - for _, cpk := range *req.CostPerKandang { - for _, costItem := range cpk.CostItems { - grandTotal += costItem.TotalCost + for _, ens := range existingExpenseNonstocks { + if err := expenseNonstockRepoTx.DeleteOne(c.Context(), uint(ens.Id)); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete expense nonstock") } } - if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{ - "grand_total": grandTotal, - }, nil); err != nil { - - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense grand total") + updatedExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get updated expense") } for _, cpk := range *req.CostPerKandang { var projectFlockKandangId *uint64 - expense, err := expenseRepoTx.GetByID(c.Context(), id, nil) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Expense not found") - } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense") - } - - if expense.Category == "BOP" { - + if updatedExpense.Category == "BOP" { projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID)) if err != nil { @@ -408,11 +469,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } var kandangId *uint64 - if expense.Category == "NON-BOP" { + if updatedExpense.Category == "NON-BOP" { id := uint64(cpk.KandangID) kandangId = &id - } else if expense.Category == "BOP" { - + } else if updatedExpense.Category == "BOP" { if projectFlockKandangId != nil { kandangId = &cpk.KandangID } @@ -425,8 +485,8 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) KandangId: kandangId, NonstockId: &costItem.NonstockID, Qty: costItem.Quantity, - TotalPrice: costItem.TotalCost, - Note: costItem.Notes, + Price: costItem.Price, + Notes: costItem.Notes, } if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { @@ -512,8 +572,6 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format") } - createdBy := uint64(1) // TODO: replace with authenticated user id - if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) @@ -537,13 +595,14 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va } realization := &entity.ExpenseRealization{ - ExpenseNonstockId: &expenseNonstockID, - RealizationQty: realizationItem.Qty, - RealizationUnitPrice: realizationItem.UnitPrice, - RealizationTotalPrice: realizationItem.TotalPrice, - RealizationDate: realizationDate, - Note: realizationItem.Notes, - CreatedBy: &createdBy, + ExpenseNonstockId: &expenseNonstockID, + Qty: realizationItem.Qty, + Price: realizationItem.Price, + Notes: "", + } + + if realizationItem.Notes != nil { + realization.Notes = *realizationItem.Notes } if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil { @@ -570,7 +629,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va expenseID, utils.ExpenseStepRealisasi, &approvalAction, - uint(createdBy), + uint(1), // TODO: replace with authenticated user id nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval") @@ -665,15 +724,6 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va fmt.Sprintf("tidak bisa update realisasi pada step %s. Harus pada step Realisasi atau selesai", currentStepName)) } - var realizationDate *time.Time - if req.RealizationDate != "" { - parsedDate, err := utils.ParseDateString(req.RealizationDate) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format") - } - realizationDate = &parsedDate - } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) @@ -681,45 +731,43 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) expenseRepoTx := repository.NewExpenseRepository(tx) - for _, realizationItem := range req.Realizations { + // Check if only updating documents + updateDataOnly := len(req.Realizations) == 0 && len(req.Documents) > 0 - expenseNonstockID := realizationItem.ExpenseNonstockID + if len(req.Realizations) > 0 { + for _, realizationItem := range req.Realizations { - if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil { - return err - } + expenseNonstockID := realizationItem.ExpenseNonstockID - existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - - return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock") + if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil { + return err } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization") + existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + + return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock") + } + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization") + } + + updateData := map[string]interface{}{ + "qty": realizationItem.Qty, + "price": realizationItem.Price, + } + + if realizationItem.Notes != nil { + updateData["notes"] = *realizationItem.Notes + } + + if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil { + s.Log.Errorf("Failed to update realization: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization") + } } - updateData := map[string]interface{}{ - "realization_qty": realizationItem.Qty, - "realization_unit_price": realizationItem.UnitPrice, - "realization_total_price": realizationItem.TotalPrice, - "realization_date": *realizationDate, - } - - if realizationItem.Notes != nil { - updateData["note"] = *realizationItem.Notes - } - - if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil { - s.Log.Errorf("Failed to update realization: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization") - } - } - - if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{ - "realization_date": *realizationDate, - }, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date") } if len(req.Documents) > 0 { @@ -728,7 +776,7 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va } } - if *latestApproval.Action == entity.ApprovalActionUpdated { + if !updateDataOnly && *latestApproval.Action == entity.ApprovalActionUpdated { actorID := uint(1) // TODO: replace with authenticated user id approvalAction := entity.ApprovalActionUpdated if _, err := approvalSvcTx.CreateApproval( diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index cdc79ebd..9d327a40 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -21,7 +21,7 @@ type CostPerKandang struct { type CostItem struct { NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"` Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"` - TotalCost float64 `form:"total_cost" json:"total_cost" validate:"required,gt=0"` + Price float64 `form:"price" json:"price" validate:"required,gt=0"` Notes string `form:"notes" json:"notes" validate:"required,max=500"` } @@ -54,8 +54,7 @@ type UpdateRealization struct { type RealizationItem struct { ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"` Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"` - UnitPrice float64 `form:"unit_price" json:"unit_price" validate:"required,gt=0"` - TotalPrice float64 `form:"total_price" json:"total_price" validate:"required,gt=0"` + Price float64 `form:"price" json:"price" validate:"required,gt=0"` Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"` }