From 1b7bec4f813cc89c1896d2987daa4d19a5254d9e Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 29 May 2026 13:25:38 +0700 Subject: [PATCH] update expense item name --- internal/contract/expense_contract.go | 6 ++--- internal/entities/analytics.go | 4 ++-- internal/entities/expense.go | 1 - internal/entities/expense_item.go | 1 + internal/mappers/expense_mapper.go | 4 +--- internal/models/expense.go | 8 +++---- internal/processor/analytics_processor.go | 2 +- internal/processor/expense_processor.go | 10 ++++---- internal/repository/analytics_repository.go | 4 ++-- internal/repository/expense_repository.go | 13 +++++++++-- internal/transformer/expense_transformer.go | 6 ++--- internal/validator/expense_validator.go | 14 +++++------ ...0072_add_expense_name_to_expenses.down.sql | 18 +++++++++++++-- ...000072_add_expense_name_to_expenses.up.sql | 23 +++++++++++++++++-- 14 files changed, 77 insertions(+), 37 deletions(-) diff --git a/internal/contract/expense_contract.go b/internal/contract/expense_contract.go index c451e69..dab91c7 100644 --- a/internal/contract/expense_contract.go +++ b/internal/contract/expense_contract.go @@ -7,7 +7,6 @@ import ( ) type CreateExpenseRequest struct { - ExpenseName string `json:"expense_name" validate:"required"` Receiver string `json:"receiver" validate:"required"` TransactionDate string `json:"transaction_date" validate:"required"` CodeNumber string `json:"code_number" validate:"required"` @@ -20,12 +19,12 @@ type CreateExpenseRequest struct { type CreateExpenseItemRequest struct { ChartOfAccountID string `json:"chart_of_account_id" validate:"required"` + Item string `json:"item" validate:"required"` Description *string `json:"description,omitempty"` Amount float64 `json:"amount" validate:"required"` } type UpdateExpenseRequest struct { - ExpenseName *string `json:"expense_name,omitempty"` Receiver *string `json:"receiver,omitempty"` TransactionDate *string `json:"transaction_date,omitempty"` CodeNumber *string `json:"code_number,omitempty"` @@ -39,6 +38,7 @@ type UpdateExpenseRequest struct { type UpdateExpenseItemRequest struct { ChartOfAccountID *string `json:"chart_of_account_id,omitempty"` + Item *string `json:"item,omitempty"` Description *string `json:"description,omitempty"` Amount *float64 `json:"amount,omitempty"` } @@ -47,7 +47,6 @@ type ExpenseResponse struct { ID uuid.UUID `json:"id"` OrganizationID uuid.UUID `json:"organization_id"` OutletID uuid.UUID `json:"outlet_id"` - ExpenseName string `json:"expense_name"` Receiver string `json:"receiver"` TransactionDate time.Time `json:"transaction_date"` CodeNumber string `json:"code_number"` @@ -65,6 +64,7 @@ type ExpenseItemResponse struct { ExpenseID uuid.UUID `json:"expense_id"` ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` ChartOfAccountName string `json:"chart_of_account_name,omitempty"` + Item string `json:"item"` Description *string `json:"description"` Amount float64 `json:"amount"` CreatedAt time.Time `json:"created_at"` diff --git a/internal/entities/analytics.go b/internal/entities/analytics.go index f06aff9..5f0a894 100644 --- a/internal/entities/analytics.go +++ b/internal/entities/analytics.go @@ -129,6 +129,6 @@ type ExpenseCategoryTotal struct { } type OperationalExpenseItem struct { - Description string - Amount float64 + Item string + Amount float64 } diff --git a/internal/entities/expense.go b/internal/entities/expense.go index 622b955..a9ba53e 100644 --- a/internal/entities/expense.go +++ b/internal/entities/expense.go @@ -12,7 +12,6 @@ type Expense struct { ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"` OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"` - ExpenseName string `gorm:"not null;size:255" json:"expense_name"` Receiver string `gorm:"not null;size:255" json:"receiver"` TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date"` CodeNumber string `gorm:"not null;size:50" json:"code_number"` diff --git a/internal/entities/expense_item.go b/internal/entities/expense_item.go index 72b3c24..f5bd7fc 100644 --- a/internal/entities/expense_item.go +++ b/internal/entities/expense_item.go @@ -12,6 +12,7 @@ type ExpenseItem struct { ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` ExpenseID uuid.UUID `gorm:"type:uuid;not null;index" json:"expense_id"` ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"` + Item string `gorm:"not null;size:255" json:"item"` Description *string `gorm:"type:text" json:"description"` Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` diff --git a/internal/mappers/expense_mapper.go b/internal/mappers/expense_mapper.go index 17324a6..3bb111b 100644 --- a/internal/mappers/expense_mapper.go +++ b/internal/mappers/expense_mapper.go @@ -14,7 +14,6 @@ func ExpenseEntityToModel(entity *entities.Expense) *models.Expense { ID: entity.ID, OrganizationID: entity.OrganizationID, OutletID: entity.OutletID, - ExpenseName: entity.ExpenseName, Receiver: entity.Receiver, TransactionDate: entity.TransactionDate, CodeNumber: entity.CodeNumber, @@ -36,7 +35,6 @@ func ExpenseModelToEntity(model *models.Expense) *entities.Expense { ID: model.ID, OrganizationID: model.OrganizationID, OutletID: model.OutletID, - ExpenseName: model.ExpenseName, Receiver: model.Receiver, TransactionDate: model.TransactionDate, CodeNumber: model.CodeNumber, @@ -58,7 +56,6 @@ func ExpenseEntityToResponse(entity *entities.Expense) *models.ExpenseResponse { ID: entity.ID, OrganizationID: entity.OrganizationID, OutletID: entity.OutletID, - ExpenseName: entity.ExpenseName, Receiver: entity.Receiver, TransactionDate: entity.TransactionDate, CodeNumber: entity.CodeNumber, @@ -98,6 +95,7 @@ func ExpenseItemEntityToResponse(entity *entities.ExpenseItem) *models.ExpenseIt ID: entity.ID, ExpenseID: entity.ExpenseID, ChartOfAccountID: entity.ChartOfAccountID, + Item: entity.Item, Description: entity.Description, Amount: entity.Amount, CreatedAt: entity.CreatedAt, diff --git a/internal/models/expense.go b/internal/models/expense.go index 68e8645..8ee1011 100644 --- a/internal/models/expense.go +++ b/internal/models/expense.go @@ -10,7 +10,6 @@ type Expense struct { ID uuid.UUID `json:"id"` OrganizationID uuid.UUID `json:"organization_id"` OutletID uuid.UUID `json:"outlet_id"` - ExpenseName string `json:"expense_name"` Receiver string `json:"receiver"` TransactionDate time.Time `json:"transaction_date"` CodeNumber string `json:"code_number"` @@ -26,6 +25,7 @@ type ExpenseItem struct { ID uuid.UUID `json:"id"` ExpenseID uuid.UUID `json:"expense_id"` ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` + Item string `json:"item"` Description *string `json:"description"` Amount float64 `json:"amount"` CreatedAt time.Time `json:"created_at"` @@ -36,7 +36,6 @@ type ExpenseResponse struct { ID uuid.UUID `json:"id"` OrganizationID uuid.UUID `json:"organization_id"` OutletID uuid.UUID `json:"outlet_id"` - ExpenseName string `json:"expense_name"` Receiver string `json:"receiver"` TransactionDate time.Time `json:"transaction_date"` CodeNumber string `json:"code_number"` @@ -54,6 +53,7 @@ type ExpenseItemResponse struct { ExpenseID uuid.UUID `json:"expense_id"` ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` ChartOfAccountName string `json:"chart_of_account_name,omitempty"` + Item string `json:"item"` Description *string `json:"description"` Amount float64 `json:"amount"` CreatedAt time.Time `json:"created_at"` @@ -61,7 +61,6 @@ type ExpenseItemResponse struct { } type CreateExpenseRequest struct { - ExpenseName string `json:"expense_name"` Receiver string `json:"receiver"` TransactionDate string `json:"transaction_date"` CodeNumber string `json:"code_number"` @@ -74,12 +73,12 @@ type CreateExpenseRequest struct { type CreateExpenseItemRequest struct { ChartOfAccountID string `json:"chart_of_account_id"` + Item string `json:"item"` Description *string `json:"description,omitempty"` Amount float64 `json:"amount"` } type UpdateExpenseRequest struct { - ExpenseName *string `json:"expense_name,omitempty"` Receiver *string `json:"receiver,omitempty"` TransactionDate *string `json:"transaction_date,omitempty"` CodeNumber *string `json:"code_number,omitempty"` @@ -93,6 +92,7 @@ type UpdateExpenseRequest struct { type UpdateExpenseItemRequest struct { ChartOfAccountID *string `json:"chart_of_account_id,omitempty"` + Item *string `json:"item,omitempty"` Description *string `json:"description,omitempty"` Amount *float64 `json:"amount,omitempty"` } diff --git a/internal/processor/analytics_processor.go b/internal/processor/analytics_processor.go index c9ae596..41a5b33 100644 --- a/internal/processor/analytics_processor.go +++ b/internal/processor/analytics_processor.go @@ -498,7 +498,7 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req var opsTotal float64 for i, item := range result.OperationalExpenseItems { opsItems[i] = models.OperationalExpenseItem{ - Item: item.Description, + Item: item.Item, Nominal: item.Amount, } opsTotal += item.Amount diff --git a/internal/processor/expense_processor.go b/internal/processor/expense_processor.go index 76b30a8..29eaf55 100644 --- a/internal/processor/expense_processor.go +++ b/internal/processor/expense_processor.go @@ -44,7 +44,6 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID expenseEntity := &entities.Expense{ OrganizationID: organizationID, OutletID: outletID, - ExpenseName: req.ExpenseName, Receiver: req.Receiver, TransactionDate: transactionDate, CodeNumber: req.CodeNumber, @@ -67,6 +66,7 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID itemEntity := &entities.ExpenseItem{ ExpenseID: expenseEntity.ID, ChartOfAccountID: chartOfAccountID, + Item: itemReq.Item, Description: itemReq.Description, Amount: itemReq.Amount, } @@ -91,9 +91,6 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati return nil, fmt.Errorf("expense not found: %w", err) } - if req.ExpenseName != nil { - expenseEntity.ExpenseName = *req.ExpenseName - } if req.Receiver != nil { expenseEntity.Receiver = *req.Receiver } @@ -146,10 +143,15 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati if itemReq.Amount != nil { amount = *itemReq.Amount } + item := "" + if itemReq.Item != nil { + item = *itemReq.Item + } itemEntity := &entities.ExpenseItem{ ExpenseID: expenseEntity.ID, ChartOfAccountID: chartOfAccountID, + Item: item, Description: itemReq.Description, Amount: amount, } diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index b28863d..09d8631 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -531,7 +531,7 @@ func (r *AnalyticsRepositoryImpl) getOperationalExpenseItems(ctx context.Context query := r.db.WithContext(ctx). Table("expense_items ei"). - Select(`COALESCE(ei.description, coa.name) as description, COALESCE(SUM(ei.amount), 0) as amount`). + Select(`COALESCE(NULLIF(ei.item, ''), ei.description, coa.name) as item, COALESCE(SUM(ei.amount), 0) as amount`). Joins("JOIN expenses e ON ei.expense_id = e.id"). Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id"). Where("e.organization_id = ?", organizationID). @@ -542,7 +542,7 @@ func (r *AnalyticsRepositoryImpl) getOperationalExpenseItems(ctx context.Context } err := query. - Group("COALESCE(ei.description, coa.name)"). + Group("COALESCE(NULLIF(ei.item, ''), ei.description, coa.name)"). Order("amount DESC"). Scan(&results).Error diff --git a/internal/repository/expense_repository.go b/internal/repository/expense_repository.go index 7711915..a66a3df 100644 --- a/internal/repository/expense_repository.go +++ b/internal/repository/expense_repository.go @@ -68,8 +68,17 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU case "search": if searchStr, ok := value.(string); ok && searchStr != "" { searchPattern := "%" + strings.ToLower(searchStr) + "%" - query = query.Where("LOWER(expense_name) LIKE ? OR LOWER(receiver) LIKE ? OR LOWER(code_number) LIKE ? OR LOWER(description) LIKE ?", - searchPattern, searchPattern, searchPattern, searchPattern) + query = query.Where(` + LOWER(receiver) LIKE ? + OR LOWER(code_number) LIKE ? + OR LOWER(description) LIKE ? + OR EXISTS ( + SELECT 1 + FROM expense_items ei + WHERE ei.expense_id = expenses.id + AND LOWER(ei.item) LIKE ? + ) + `, searchPattern, searchPattern, searchPattern, searchPattern) } case "outlet_id": if outletID, ok := value.(uuid.UUID); ok { diff --git a/internal/transformer/expense_transformer.go b/internal/transformer/expense_transformer.go index bbf0ce7..d86714b 100644 --- a/internal/transformer/expense_transformer.go +++ b/internal/transformer/expense_transformer.go @@ -12,7 +12,6 @@ func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.Cre } return &models.CreateExpenseRequest{ - ExpenseName: req.ExpenseName, Receiver: req.Receiver, TransactionDate: req.TransactionDate, CodeNumber: req.CodeNumber, @@ -27,6 +26,7 @@ func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.Cre func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest { return models.CreateExpenseItemRequest{ ChartOfAccountID: req.ChartOfAccountID, + Item: req.Item, Description: req.Description, Amount: req.Amount, } @@ -34,7 +34,6 @@ func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) mod func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.UpdateExpenseRequest { modelReq := &models.UpdateExpenseRequest{ - ExpenseName: req.ExpenseName, Receiver: req.Receiver, TransactionDate: req.TransactionDate, CodeNumber: req.CodeNumber, @@ -59,6 +58,7 @@ func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.Upd func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest { return models.UpdateExpenseItemRequest{ ChartOfAccountID: req.ChartOfAccountID, + Item: req.Item, Description: req.Description, Amount: req.Amount, } @@ -89,7 +89,6 @@ func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.E ID: expense.ID, OrganizationID: expense.OrganizationID, OutletID: expense.OutletID, - ExpenseName: expense.ExpenseName, Receiver: expense.Receiver, TransactionDate: expense.TransactionDate, CodeNumber: expense.CodeNumber, @@ -109,6 +108,7 @@ func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contra ExpenseID: item.ExpenseID, ChartOfAccountID: item.ChartOfAccountID, ChartOfAccountName: item.ChartOfAccountName, + Item: item.Item, Description: item.Description, Amount: item.Amount, CreatedAt: item.CreatedAt, diff --git a/internal/validator/expense_validator.go b/internal/validator/expense_validator.go index b0cb81d..7101f45 100644 --- a/internal/validator/expense_validator.go +++ b/internal/validator/expense_validator.go @@ -28,10 +28,6 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create return errors.New("request body is required"), constants.MissingFieldErrorCode } - if strings.TrimSpace(req.ExpenseName) == "" { - return errors.New("expense_name is required"), constants.MissingFieldErrorCode - } - if strings.TrimSpace(req.Receiver) == "" { return errors.New("receiver is required"), constants.MissingFieldErrorCode } @@ -68,6 +64,9 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create if strings.TrimSpace(item.ChartOfAccountID) == "" { return fmt.Errorf("item %d: chart_of_account_id is required", i), constants.MissingFieldErrorCode } + if strings.TrimSpace(item.Item) == "" { + return fmt.Errorf("item %d: item is required", i), constants.MissingFieldErrorCode + } if _, err := uuid.Parse(item.ChartOfAccountID); err != nil { return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode } @@ -84,10 +83,6 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update return errors.New("request body is required"), constants.MissingFieldErrorCode } - if req.ExpenseName != nil && strings.TrimSpace(*req.ExpenseName) == "" { - return errors.New("expense_name cannot be empty"), constants.MalformedFieldErrorCode - } - if req.Receiver != nil && strings.TrimSpace(*req.Receiver) == "" { return errors.New("receiver cannot be empty"), constants.MalformedFieldErrorCode } @@ -123,6 +118,9 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode } } + if item.Item != nil && strings.TrimSpace(*item.Item) == "" { + return fmt.Errorf("item %d: item cannot be empty", i), constants.MalformedFieldErrorCode + } if item.Amount != nil && *item.Amount <= 0 { return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode } diff --git a/migrations/000072_add_expense_name_to_expenses.down.sql b/migrations/000072_add_expense_name_to_expenses.down.sql index 553fdc9..368f8f9 100644 --- a/migrations/000072_add_expense_name_to_expenses.down.sql +++ b/migrations/000072_add_expense_name_to_expenses.down.sql @@ -1,2 +1,16 @@ -DROP INDEX IF EXISTS idx_expenses_expense_name; -ALTER TABLE expenses DROP COLUMN expense_name; +ALTER TABLE expenses ADD COLUMN IF NOT EXISTS expense_name VARCHAR(255) NOT NULL DEFAULT ''; + +UPDATE expenses e +SET expense_name = first_item.item +FROM ( + SELECT DISTINCT ON (expense_id) expense_id, item + FROM expense_items + WHERE COALESCE(item, '') != '' + ORDER BY expense_id, created_at ASC +) first_item +WHERE e.id = first_item.expense_id + AND COALESCE(e.expense_name, '') = ''; + +CREATE INDEX IF NOT EXISTS idx_expenses_expense_name ON expenses(expense_name); +DROP INDEX IF EXISTS idx_expense_items_item; +ALTER TABLE expense_items DROP COLUMN IF EXISTS item; diff --git a/migrations/000072_add_expense_name_to_expenses.up.sql b/migrations/000072_add_expense_name_to_expenses.up.sql index 9cb91e7..2b87356 100644 --- a/migrations/000072_add_expense_name_to_expenses.up.sql +++ b/migrations/000072_add_expense_name_to_expenses.up.sql @@ -1,2 +1,21 @@ -ALTER TABLE expenses ADD COLUMN expense_name VARCHAR(255) NOT NULL DEFAULT ''; -CREATE INDEX idx_expenses_expense_name ON expenses(expense_name); +ALTER TABLE expense_items ADD COLUMN IF NOT EXISTS item VARCHAR(255) NOT NULL DEFAULT ''; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'expenses' + AND column_name = 'expense_name' + ) THEN + UPDATE expense_items ei + SET item = e.expense_name + FROM expenses e + WHERE ei.expense_id = e.id + AND COALESCE(ei.item, '') = ''; + END IF; +END $$; + +DROP INDEX IF EXISTS idx_expenses_expense_name; +ALTER TABLE expenses DROP COLUMN IF EXISTS expense_name; +CREATE INDEX IF NOT EXISTS idx_expense_items_item ON expense_items(item);