Feat[BE]: refactor expense API and expense table match with new ERD

This commit is contained in:
aguhh18
2025-11-27 13:53:35 +07:00
parent 99688c8e11
commit 886446b55f
10 changed files with 278 additions and 169 deletions
@@ -1,7 +1,7 @@
CREATE TABLE expenses ( CREATE TABLE expenses (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
reference_number VARCHAR(50) UNIQUE NOT NULL, reference_number VARCHAR(50) UNIQUE NOT NULL,
supplier_id BIGINT NULL, supplier_id BIGINT NOT NULL,
category VARCHAR(50) NOT NULL CHECK ( category VARCHAR(50) NOT NULL CHECK (
category IN ('BOP', 'NON-BOP') category IN ('BOP', 'NON-BOP')
), ),
@@ -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();
+5 -7
View File
@@ -13,18 +13,16 @@ type Expense struct {
SupplierId uint64 `gorm:""` SupplierId uint64 `gorm:""`
Category string `gorm:"type:varchar(50);not null"` Category string `gorm:"type:varchar(50);not null"`
PoNumber string `gorm:"type:varchar(50)"` PoNumber string `gorm:"type:varchar(50)"`
DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan DocumentPath sql.NullString `gorm:"type:json"`
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi RealizationDate time.Time `gorm:"type:date;column:realization_date"`
ExpenseDate time.Time `gorm:"type:date;not null"` TransactionDate time.Time `gorm:"type:date;not null"`
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` Notes string `gorm:"type:text;column:notes"`
Note string `gorm:"type:text"`
CreatedBy uint64 `gorm:""` CreatedBy uint64 `gorm:""`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// Relations
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"` Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"` Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
+15 -12
View File
@@ -1,20 +1,23 @@
package entities package entities
type ExpenseNonstock struct { import (
Id uint64 `gorm:"primaryKey;autoIncrement"` "time"
ExpenseId *uint64 `gorm:""` )
ProjectFlockKandangId *uint64 `gorm:""`
KandangId *uint64 `gorm:""` type ExpenseNonstock struct {
NonstockId *uint64 `gorm:""` Id uint64 `gorm:"primaryKey;autoIncrement"`
Qty float64 `gorm:"type:numeric(15,3);not null"` ExpenseId *uint64 `gorm:""`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"` ProjectFlockKandangId *uint64 `gorm:""`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"` KandangId *uint64 `gorm:""`
Note string `gorm:"type:text"` 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"` Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"` Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"` Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` Realization *ExpenseRealization `gorm:"foreignKey:Id;references:ExpenseNonstockId"`
} }
+6 -10
View File
@@ -5,16 +5,12 @@ import (
) )
type ExpenseRealization struct { type ExpenseRealization struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseNonstockId *uint64 `gorm:""` ExpenseNonstockId *uint64 `gorm:""`
RealizationQty float64 `gorm:"type:numeric(15,3);not null"` Qty float64 `gorm:"type:numeric(15,3);not null;"`
RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"` Price float64 `gorm:"type:numeric(15,3);not null;"`
RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"` Notes string `gorm:"type:text;"`
RealizationDate time.Time `gorm:"type:date;not null"` CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
Note *string `gorm:"type:text"`
CreatedBy *uint64 `gorm:""`
// Relations
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"` ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
} }
@@ -155,6 +155,18 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
req.TransactionDate = &transactionDate 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") costPerKandangJSON := c.FormValue("cost_per_kandang")
if costPerKandangJSON != "" { if costPerKandangJSON != "" {
var costPerKandang []validation.CostPerKandang var costPerKandang []validation.CostPerKandang
+54 -45
View File
@@ -15,10 +15,9 @@ import (
// === DTO Structs === // === DTO Structs ===
type ExpenseRelationDTO struct { type ExpenseRelationDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
PoNumber string `json:"po_number"` PoNumber string `json:"po_number"`
ExpenseDate time.Time `json:"expense_date"` TransactionDate time.Time `json:"transaction_date"`
GrandTotal float64 `json:"grand_total"`
} }
type ExpenseBaseDTO struct { type ExpenseBaseDTO struct {
@@ -28,8 +27,8 @@ type ExpenseBaseDTO struct {
Category string `json:"category"` Category string `json:"category"`
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"` Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
RealizationDate *time.Time `json:"realization_date,omitempty"` RealizationDate *time.Time `json:"realization_date,omitempty"`
ExpenseDate time.Time `json:"expense_date"` TransactionDate time.Time `json:"transaction_date"`
GrandTotal float64 `json:"grand_total"` Notes string `json:"notes"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
} }
@@ -55,21 +54,26 @@ type ExpenseDetailDTO struct {
} }
type ExpenseNonstockDTO struct { type ExpenseNonstockDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Qty float64 `json:"qty"` ExpenseId *uint64 `json:"expense_id,omitempty"`
UnitPrice float64 `json:"unit_price"` ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"`
TotalPrice float64 `json:"total_price"` KandangId *uint64 `json:"kandang_id,omitempty"`
Note *string `json:"note,omitempty"` NonstockId *uint64 `json:"nonstock_id,omitempty"`
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,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 { type ExpenseRealizationDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Qty float64 `json:"qty"` ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"`
UnitPrice float64 `json:"unit_price"` Qty float64 `json:"qty"`
TotalPrice float64 `json:"total_price"` Price float64 `json:"price"`
Note *string `json:"note,omitempty"` Notes string `json:"notes"`
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
CreatedAt time.Time `json:"created_at"`
} }
type KandangGroupDTO struct { type KandangGroupDTO struct {
@@ -89,10 +93,9 @@ type DocumentDTO struct {
func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO { func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO {
return ExpenseRelationDTO{ return ExpenseRelationDTO{
Id: e.Id, Id: e.Id,
PoNumber: e.PoNumber, PoNumber: e.PoNumber,
ExpenseDate: e.ExpenseDate, TransactionDate: e.TransactionDate,
GrandTotal: e.GrandTotal,
} }
} }
@@ -124,8 +127,8 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
Category: e.Category, Category: e.Category,
Supplier: supplier, Supplier: supplier,
RealizationDate: realizationDate, RealizationDate: realizationDate,
ExpenseDate: e.ExpenseDate, TransactionDate: e.TransactionDate,
GrandTotal: e.GrandTotal, Notes: e.Notes,
Location: location, Location: location,
} }
} }
@@ -192,10 +195,9 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
for _, ns := range e.Nonstocks { for _, ns := range e.Nonstocks {
pengajuanDTO := ToExpenseNonstockDTO(ns) pengajuanDTO := ToExpenseNonstockDTO(ns)
pengajuans = append(pengajuans, pengajuanDTO) pengajuans = append(pengajuans, pengajuanDTO)
if ns.Realization != nil && ns.Realization.Id != 0 { if ns.Realization != nil {
var nonstock *nonstockDTO.NonstockRelationDTO var nonstock *nonstockDTO.NonstockRelationDTO
if ns.Nonstock != nil && ns.Nonstock.Id != 0 { if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock) mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
@@ -203,12 +205,13 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
} }
realisasiDTO := ExpenseRealizationDTO{ realisasiDTO := ExpenseRealizationDTO{
Id: ns.Realization.Id, Id: ns.Realization.Id,
Qty: ns.Realization.RealizationQty, ExpenseNonstockId: ns.Realization.ExpenseNonstockId,
UnitPrice: ns.Realization.RealizationUnitPrice, Qty: ns.Realization.Qty,
TotalPrice: ns.Realization.RealizationTotalPrice, Price: ns.Realization.Price,
Note: ns.Realization.Note, Notes: ns.Realization.Notes,
Nonstock: nonstock, Nonstock: nonstock,
CreatedAt: ns.Realization.CreatedAt,
} }
realisasi = append(realisasi, realisasiDTO) realisasi = append(realisasi, realisasiDTO)
} }
@@ -217,12 +220,12 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
var totalPengajuan float64 var totalPengajuan float64
for _, p := range pengajuans { for _, p := range pengajuans {
totalPengajuan += p.TotalPrice totalPengajuan += p.Qty * p.Price
} }
var totalRealisasi float64 var totalRealisasi float64
for _, r := range realisasi { for _, r := range realisasi {
totalRealisasi += r.TotalPrice totalRealisasi += r.Qty * r.Price
} }
kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks) kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks)
@@ -248,12 +251,16 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO {
} }
return ExpenseNonstockDTO{ return ExpenseNonstockDTO{
Id: ns.Id, Id: ns.Id,
Qty: ns.Qty, ExpenseId: ns.ExpenseId,
UnitPrice: ns.UnitPrice, ProjectFlockKandangId: ns.ProjectFlockKandangId,
TotalPrice: ns.TotalPrice, KandangId: ns.KandangId,
Note: &ns.Note, NonstockId: ns.NonstockId,
Nonstock: nonstock, 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 kandangId uint64
var kandangName string var kandangName string
for _, ns := range nonstocks { if p.KandangId != nil {
if ns.Id == p.Id && ns.Kandang != nil { kandangId = *p.KandangId
kandangId = uint64(ns.Kandang.Id) for _, ns := range nonstocks {
kandangName = ns.Kandang.Name if ns.Id == p.Id && ns.Kandang != nil {
break kandangName = ns.Kandang.Name
break
}
} }
} }
@@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" 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") 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 createdBy := uint64(1) //todo get from auth
expense = &entity.Expense{ expense = &entity.Expense{
ReferenceNumber: referenceNumber, ReferenceNumber: referenceNumber,
PoNumber: req.PoNumber, PoNumber: req.PoNumber,
Category: req.Category, Category: req.Category,
SupplierId: req.SupplierID, SupplierId: req.SupplierID,
ExpenseDate: expenseDate, TransactionDate: expenseDate,
GrandTotal: grandTotal,
CreatedBy: createdBy, CreatedBy: createdBy,
} }
@@ -249,8 +240,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
KandangId: kandangId, KandangId: kandangId,
NonstockId: &nonstockId, NonstockId: &nonstockId,
Qty: costItem.Quantity, Qty: costItem.Quantity,
TotalPrice: costItem.TotalCost, Price: costItem.Price,
Note: costItem.Notes, Notes: costItem.Notes,
} }
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { 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 { if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") 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 { 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)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(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 len(updateBody) > 0 {
if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { 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 req.CostPerKandang != nil {
if err := tx.Where("expense_id = ?", id).Delete(&entity.ExpenseNonstock{}).Error; err != nil { var existingExpenseNonstocks []entity.ExpenseNonstock
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense items") 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 _, ens := range existingExpenseNonstocks {
for _, cpk := range *req.CostPerKandang { if err := expenseNonstockRepoTx.DeleteOne(c.Context(), uint(ens.Id)); err != nil {
for _, costItem := range cpk.CostItems { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete expense nonstock")
grandTotal += costItem.TotalCost
} }
} }
if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{ updatedExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil)
"grand_total": grandTotal, if err != nil {
}, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense grand total") }
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get updated expense")
} }
for _, cpk := range *req.CostPerKandang { for _, cpk := range *req.CostPerKandang {
var projectFlockKandangId *uint64 var projectFlockKandangId *uint64
expense, err := expenseRepoTx.GetByID(c.Context(), id, nil) if updatedExpense.Category == "BOP" {
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" {
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID)) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID))
if err != nil { if err != nil {
@@ -408,11 +469,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
var kandangId *uint64 var kandangId *uint64
if expense.Category == "NON-BOP" { if updatedExpense.Category == "NON-BOP" {
id := uint64(cpk.KandangID) id := uint64(cpk.KandangID)
kandangId = &id kandangId = &id
} else if expense.Category == "BOP" { } else if updatedExpense.Category == "BOP" {
if projectFlockKandangId != nil { if projectFlockKandangId != nil {
kandangId = &cpk.KandangID kandangId = &cpk.KandangID
} }
@@ -425,8 +485,8 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
KandangId: kandangId, KandangId: kandangId,
NonstockId: &costItem.NonstockID, NonstockId: &costItem.NonstockID,
Qty: costItem.Quantity, Qty: costItem.Quantity,
TotalPrice: costItem.TotalCost, Price: costItem.Price,
Note: costItem.Notes, Notes: costItem.Notes,
} }
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { 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") 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 { if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
@@ -537,13 +595,14 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
} }
realization := &entity.ExpenseRealization{ realization := &entity.ExpenseRealization{
ExpenseNonstockId: &expenseNonstockID, ExpenseNonstockId: &expenseNonstockID,
RealizationQty: realizationItem.Qty, Qty: realizationItem.Qty,
RealizationUnitPrice: realizationItem.UnitPrice, Price: realizationItem.Price,
RealizationTotalPrice: realizationItem.TotalPrice, Notes: "",
RealizationDate: realizationDate, }
Note: realizationItem.Notes,
CreatedBy: &createdBy, if realizationItem.Notes != nil {
realization.Notes = *realizationItem.Notes
} }
if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil { 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, expenseID,
utils.ExpenseStepRealisasi, utils.ExpenseStepRealisasi,
&approvalAction, &approvalAction,
uint(createdBy), uint(1), // TODO: replace with authenticated user id
nil); err != nil { nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval") 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)) 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 { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) 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) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
expenseRepoTx := repository.NewExpenseRepository(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 { expenseNonstockID := realizationItem.ExpenseNonstockID
return err
}
existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil {
if err != nil { return err
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") 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 { 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 actorID := uint(1) // TODO: replace with authenticated user id
approvalAction := entity.ApprovalActionUpdated approvalAction := entity.ApprovalActionUpdated
if _, err := approvalSvcTx.CreateApproval( if _, err := approvalSvcTx.CreateApproval(
@@ -21,7 +21,7 @@ type CostPerKandang struct {
type CostItem struct { type CostItem struct {
NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"` NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"`
Quantity float64 `form:"quantity" json:"quantity" 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"` Notes string `form:"notes" json:"notes" validate:"required,max=500"`
} }
@@ -54,8 +54,7 @@ type UpdateRealization struct {
type RealizationItem struct { type RealizationItem struct {
ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"` ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"`
Qty float64 `form:"qty" json:"qty" 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"` Price float64 `form:"price" json:"price" validate:"required,gt=0"`
TotalPrice float64 `form:"total_price" json:"total_price" validate:"required,gt=0"`
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"` Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
} }