Add item_expense

This commit is contained in:
ryan 2026-05-25 16:19:36 +07:00
parent da87d659df
commit b8be29e110
12 changed files with 439 additions and 181 deletions

View File

@ -10,23 +10,35 @@ type CreateExpenseRequest struct {
Receiver string `json:"receiver" validate:"required"`
TransactionDate string `json:"transaction_date" validate:"required"`
CodeNumber string `json:"code_number" validate:"required"`
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
OutletID string `json:"outlet_id" validate:"required"`
Description *string `json:"description,omitempty"`
Tax float64 `json:"tax"`
Total float64 `json:"total" validate:"required"`
Items []CreateExpenseItemRequest `json:"items" validate:"required"`
}
type CreateExpenseItemRequest struct {
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
Description *string `json:"description,omitempty"`
Amount float64 `json:"amount" validate:"required"`
}
type UpdateExpenseRequest struct {
Receiver *string `json:"receiver,omitempty"`
TransactionDate *string `json:"transaction_date,omitempty"`
CodeNumber *string `json:"code_number,omitempty"`
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
OutletID *string `json:"outlet_id,omitempty"`
Description *string `json:"description,omitempty"`
Tax *float64 `json:"tax,omitempty"`
Total *float64 `json:"total,omitempty"`
Reserved1 *string `json:"reserved1,omitempty"`
Items []UpdateExpenseItemRequest `json:"items,omitempty"`
}
type UpdateExpenseItemRequest struct {
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
Description *string `json:"description,omitempty"`
Amount *float64 `json:"amount,omitempty"`
}
type ExpenseResponse struct {
@ -36,14 +48,24 @@ type ExpenseResponse struct {
Receiver string `json:"receiver"`
TransactionDate time.Time `json:"transaction_date"`
CodeNumber string `json:"code_number"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
Description *string `json:"description"`
Tax float64 `json:"tax"`
Total float64 `json:"total"`
Reserved1 *string `json:"reserved1,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Items []ExpenseItemResponse `json:"items,omitempty"`
}
type ExpenseItemResponse struct {
ID uuid.UUID `json:"id"`
ExpenseID uuid.UUID `json:"expense_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
Description *string `json:"description"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListExpenseRequest struct {

View File

@ -15,7 +15,6 @@ type Expense struct {
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"`
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
Description *string `gorm:"type:text" json:"description"`
Tax float64 `gorm:"type:decimal(15,2);not null;default:0" json:"tax"`
Total float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total"`
@ -25,7 +24,7 @@ type Expense struct {
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
Items []ExpenseItem `gorm:"foreignKey:ExpenseID" json:"items,omitempty"`
}
func (e *Expense) BeforeCreate(tx *gorm.DB) error {

View File

@ -0,0 +1,33 @@
package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
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"`
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"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
}
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
if e.ID == uuid.Nil {
e.ID = uuid.New()
}
return nil
}
func (ExpenseItem) TableName() string {
return "expense_items"
}

View File

@ -17,7 +17,6 @@ func ExpenseEntityToModel(entity *entities.Expense) *models.Expense {
Receiver: entity.Receiver,
TransactionDate: entity.TransactionDate,
CodeNumber: entity.CodeNumber,
ChartOfAccountID: entity.ChartOfAccountID,
Description: entity.Description,
Tax: entity.Tax,
Total: entity.Total,
@ -39,7 +38,6 @@ func ExpenseModelToEntity(model *models.Expense) *entities.Expense {
Receiver: model.Receiver,
TransactionDate: model.TransactionDate,
CodeNumber: model.CodeNumber,
ChartOfAccountID: model.ChartOfAccountID,
Description: model.Description,
Tax: model.Tax,
Total: model.Total,
@ -61,7 +59,6 @@ func ExpenseEntityToResponse(entity *entities.Expense) *models.ExpenseResponse {
Receiver: entity.Receiver,
TransactionDate: entity.TransactionDate,
CodeNumber: entity.CodeNumber,
ChartOfAccountID: entity.ChartOfAccountID,
Description: entity.Description,
Tax: entity.Tax,
Total: entity.Total,
@ -70,8 +67,8 @@ func ExpenseEntityToResponse(entity *entities.Expense) *models.ExpenseResponse {
UpdatedAt: entity.UpdatedAt,
}
if entity.ChartOfAccount != nil {
resp.ChartOfAccountName = entity.ChartOfAccount.Name
if entity.Items != nil {
resp.Items = ExpenseItemEntitiesToResponses(entity.Items)
}
return resp
@ -88,3 +85,40 @@ func ExpenseEntitiesToResponses(entities []*entities.Expense) []*models.ExpenseR
}
return responses
}
func ExpenseItemEntityToResponse(entity *entities.ExpenseItem) *models.ExpenseItemResponse {
if entity == nil {
return nil
}
response := &models.ExpenseItemResponse{
ID: entity.ID,
ExpenseID: entity.ExpenseID,
ChartOfAccountID: entity.ChartOfAccountID,
Description: entity.Description,
Amount: entity.Amount,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
if entity.ChartOfAccount != nil {
response.ChartOfAccountName = entity.ChartOfAccount.Name
}
return response
}
func ExpenseItemEntitiesToResponses(entities []entities.ExpenseItem) []models.ExpenseItemResponse {
if entities == nil {
return nil
}
responses := make([]models.ExpenseItemResponse, len(entities))
for i, entity := range entities {
response := ExpenseItemEntityToResponse(&entity)
if response != nil {
responses[i] = *response
}
}
return responses
}

View File

@ -13,7 +13,6 @@ type Expense struct {
Receiver string `json:"receiver"`
TransactionDate time.Time `json:"transaction_date"`
CodeNumber string `json:"code_number"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
Description *string `json:"description"`
Tax float64 `json:"tax"`
Total float64 `json:"total"`
@ -22,6 +21,16 @@ type Expense struct {
UpdatedAt time.Time `json:"updated_at"`
}
type ExpenseItem struct {
ID uuid.UUID `json:"id"`
ExpenseID uuid.UUID `json:"expense_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
Description *string `json:"description"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ExpenseResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
@ -29,37 +38,59 @@ type ExpenseResponse struct {
Receiver string `json:"receiver"`
TransactionDate time.Time `json:"transaction_date"`
CodeNumber string `json:"code_number"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
Description *string `json:"description"`
Tax float64 `json:"tax"`
Total float64 `json:"total"`
Reserved1 *string `json:"reserved1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Items []ExpenseItemResponse `json:"items,omitempty"`
}
type ExpenseItemResponse struct {
ID uuid.UUID `json:"id"`
ExpenseID uuid.UUID `json:"expense_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
Description *string `json:"description"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateExpenseRequest struct {
Receiver string `json:"receiver"`
TransactionDate string `json:"transaction_date"`
CodeNumber string `json:"code_number"`
ChartOfAccountID string `json:"chart_of_account_id"`
OutletID string `json:"outlet_id"`
Description *string `json:"description"`
Tax float64 `json:"tax"`
Total float64 `json:"total"`
Items []CreateExpenseItemRequest `json:"items"`
}
type CreateExpenseItemRequest struct {
ChartOfAccountID string `json:"chart_of_account_id"`
Description *string `json:"description,omitempty"`
Amount float64 `json:"amount"`
}
type UpdateExpenseRequest struct {
Receiver *string `json:"receiver"`
TransactionDate *string `json:"transaction_date"`
CodeNumber *string `json:"code_number"`
ChartOfAccountID *string `json:"chart_of_account_id"`
OutletID *string `json:"outlet_id"`
Description *string `json:"description"`
Tax *float64 `json:"tax"`
Total *float64 `json:"total"`
Reserved1 *string `json:"reserved1"`
Receiver *string `json:"receiver,omitempty"`
TransactionDate *string `json:"transaction_date,omitempty"`
CodeNumber *string `json:"code_number,omitempty"`
OutletID *string `json:"outlet_id,omitempty"`
Description *string `json:"description,omitempty"`
Tax *float64 `json:"tax,omitempty"`
Total *float64 `json:"total,omitempty"`
Reserved1 *string `json:"reserved1,omitempty"`
Items []UpdateExpenseItemRequest `json:"items,omitempty"`
}
type UpdateExpenseItemRequest struct {
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
Description *string `json:"description,omitempty"`
Amount *float64 `json:"amount,omitempty"`
}
type ListExpenseRequest struct {

View File

@ -31,11 +31,6 @@ func NewExpenseProcessorImpl(expenseRepo ExpenseRepository) *ExpenseProcessorImp
}
func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID uuid.UUID, req *models.CreateExpenseRequest) (*models.ExpenseResponse, error) {
chartOfAccountID, err := uuid.Parse(req.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("invalid chart_of_account_id: %w", err)
}
outletID, err := uuid.Parse(req.OutletID)
if err != nil {
return nil, fmt.Errorf("invalid outlet_id: %w", err)
@ -52,7 +47,6 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
Receiver: req.Receiver,
TransactionDate: transactionDate,
CodeNumber: req.CodeNumber,
ChartOfAccountID: chartOfAccountID,
Description: req.Description,
Tax: req.Tax,
Total: req.Total,
@ -63,6 +57,25 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
return nil, fmt.Errorf("failed to create expense: %w", err)
}
for _, itemReq := range req.Items {
chartOfAccountID, err := uuid.Parse(itemReq.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
}
itemEntity := &entities.ExpenseItem{
ExpenseID: expenseEntity.ID,
ChartOfAccountID: chartOfAccountID,
Description: itemReq.Description,
Amount: itemReq.Amount,
}
err = p.expenseRepo.CreateItem(ctx, itemEntity)
if err != nil {
return nil, fmt.Errorf("failed to create expense item: %w", err)
}
}
created, err := p.expenseRepo.GetByID(ctx, expenseEntity.ID)
if err != nil {
return mappers.ExpenseEntityToResponse(expenseEntity), nil
@ -90,13 +103,6 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
if req.CodeNumber != nil {
expenseEntity.CodeNumber = *req.CodeNumber
}
if req.ChartOfAccountID != nil {
chartOfAccountID, err := uuid.Parse(*req.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("invalid chart_of_account_id: %w", err)
}
expenseEntity.ChartOfAccountID = chartOfAccountID
}
if req.OutletID != nil {
outletID, err := uuid.Parse(*req.OutletID)
if err != nil {
@ -117,6 +123,40 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
expenseEntity.Reserved1 = req.Reserved1
}
if req.Items != nil {
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
if err != nil {
return nil, fmt.Errorf("failed to delete existing items: %w", err)
}
for _, itemReq := range req.Items {
chartOfAccountID := uuid.Nil
if itemReq.ChartOfAccountID != nil {
chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
}
}
amount := 0.0
if itemReq.Amount != nil {
amount = *itemReq.Amount
}
itemEntity := &entities.ExpenseItem{
ExpenseID: expenseEntity.ID,
ChartOfAccountID: chartOfAccountID,
Description: itemReq.Description,
Amount: amount,
}
err = p.expenseRepo.CreateItem(ctx, itemEntity)
if err != nil {
return nil, fmt.Errorf("failed to create expense item: %w", err)
}
}
}
err = p.expenseRepo.Update(ctx, expenseEntity)
if err != nil {
return nil, fmt.Errorf("failed to update expense: %w", err)

View File

@ -14,4 +14,6 @@ type ExpenseRepository interface {
Update(ctx context.Context, expense *entities.Expense) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.Expense, int64, error)
CreateItem(ctx context.Context, item *entities.ExpenseItem) error
DeleteItemsByExpenseID(ctx context.Context, expenseID uuid.UUID) error
}

View File

@ -27,7 +27,9 @@ func (r *ExpenseRepositoryImpl) Create(ctx context.Context, expense *entities.Ex
func (r *ExpenseRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Expense, error) {
var expense entities.Expense
err := r.db.WithContext(ctx).Preload("ChartOfAccount").First(&expense, "id = ?", id).Error
err := r.db.WithContext(ctx).
Preload("Items.ChartOfAccount").
First(&expense, "id = ?", id).Error
if err != nil {
return nil, err
}
@ -36,7 +38,10 @@ func (r *ExpenseRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*ent
func (r *ExpenseRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Expense, error) {
var expense entities.Expense
err := r.db.WithContext(ctx).Preload("ChartOfAccount").Where("id = ? AND organization_id = ?", id, organizationID).First(&expense).Error
err := r.db.WithContext(ctx).
Preload("Items.ChartOfAccount").
Where("id = ? AND organization_id = ?", id, organizationID).
First(&expense).Error
if err != nil {
return nil, err
}
@ -78,6 +83,19 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU
return nil, 0, err
}
err := query.Preload("ChartOfAccount").Order("created_at DESC").Limit(limit).Offset(offset).Find(&expenses).Error
err := query.
Preload("Items.ChartOfAccount").
Order("created_at DESC").
Limit(limit).
Offset(offset).
Find(&expenses).Error
return expenses, total, err
}
func (r *ExpenseRepositoryImpl) CreateItem(ctx context.Context, item *entities.ExpenseItem) error {
return r.db.WithContext(ctx).Create(item).Error
}
func (r *ExpenseRepositoryImpl) DeleteItemsByExpenseID(ctx context.Context, expenseID uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.ExpenseItem{}, "expense_id = ?", expenseID).Error
}

View File

@ -6,30 +6,60 @@ import (
)
func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.CreateExpenseRequest {
items := make([]models.CreateExpenseItemRequest, len(req.Items))
for i, item := range req.Items {
items[i] = CreateExpenseItemRequestToModel(&item)
}
return &models.CreateExpenseRequest{
Receiver: req.Receiver,
TransactionDate: req.TransactionDate,
CodeNumber: req.CodeNumber,
ChartOfAccountID: req.ChartOfAccountID,
OutletID: req.OutletID,
Description: req.Description,
Tax: req.Tax,
Total: req.Total,
Items: items,
}
}
func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest {
return models.CreateExpenseItemRequest{
ChartOfAccountID: req.ChartOfAccountID,
Description: req.Description,
Amount: req.Amount,
}
}
func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.UpdateExpenseRequest {
return &models.UpdateExpenseRequest{
modelReq := &models.UpdateExpenseRequest{
Receiver: req.Receiver,
TransactionDate: req.TransactionDate,
CodeNumber: req.CodeNumber,
ChartOfAccountID: req.ChartOfAccountID,
OutletID: req.OutletID,
Description: req.Description,
Tax: req.Tax,
Total: req.Total,
Reserved1: req.Reserved1,
}
if req.Items != nil {
items := make([]models.UpdateExpenseItemRequest, len(req.Items))
for i, item := range req.Items {
items[i] = UpdateExpenseItemRequestToModel(&item)
}
modelReq.Items = items
}
return modelReq
}
func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest {
return models.UpdateExpenseItemRequest{
ChartOfAccountID: req.ChartOfAccountID,
Description: req.Description,
Amount: req.Amount,
}
}
func ListExpenseRequestToModel(req *contract.ListExpenseRequest) *models.ListExpenseRequest {
@ -41,6 +71,15 @@ func ListExpenseRequestToModel(req *contract.ListExpenseRequest) *models.ListExp
}
func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.ExpenseResponse {
if expense == nil {
return nil
}
items := make([]contract.ExpenseItemResponse, len(expense.Items))
for i, item := range expense.Items {
items[i] = ExpenseItemModelResponseToResponse(&item)
}
return &contract.ExpenseResponse{
ID: expense.ID,
OrganizationID: expense.OrganizationID,
@ -48,14 +87,26 @@ func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.E
Receiver: expense.Receiver,
TransactionDate: expense.TransactionDate,
CodeNumber: expense.CodeNumber,
ChartOfAccountID: expense.ChartOfAccountID,
ChartOfAccountName: expense.ChartOfAccountName,
Description: expense.Description,
Tax: expense.Tax,
Total: expense.Total,
Reserved1: expense.Reserved1,
CreatedAt: expense.CreatedAt,
UpdatedAt: expense.UpdatedAt,
Items: items,
}
}
func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contract.ExpenseItemResponse {
return contract.ExpenseItemResponse{
ID: item.ID,
ExpenseID: item.ExpenseID,
ChartOfAccountID: item.ChartOfAccountID,
ChartOfAccountName: item.ChartOfAccountName,
Description: item.Description,
Amount: item.Amount,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
}
}

View File

@ -2,6 +2,7 @@ package validator
import (
"errors"
"fmt"
"strings"
"apskel-pos-be/internal/constants"
@ -39,14 +40,6 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create
return errors.New("code_number is required"), constants.MissingFieldErrorCode
}
if strings.TrimSpace(req.ChartOfAccountID) == "" {
return errors.New("chart_of_account_id is required"), constants.MissingFieldErrorCode
}
if _, err := uuid.Parse(req.ChartOfAccountID); err != nil {
return errors.New("chart_of_account_id must be a valid UUID"), constants.MalformedFieldErrorCode
}
if strings.TrimSpace(req.OutletID) == "" {
return errors.New("outlet_id is required"), constants.MissingFieldErrorCode
}
@ -63,6 +56,22 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create
return errors.New("tax cannot be negative"), constants.MalformedFieldErrorCode
}
if len(req.Items) == 0 {
return errors.New("at least one item is required"), constants.MissingFieldErrorCode
}
for i, item := range req.Items {
if strings.TrimSpace(item.ChartOfAccountID) == "" {
return fmt.Errorf("item %d: chart_of_account_id 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
}
if item.Amount <= 0 {
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
}
}
return nil, ""
}
@ -79,15 +88,6 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update
return errors.New("code_number cannot be empty"), constants.MalformedFieldErrorCode
}
if req.ChartOfAccountID != nil {
if strings.TrimSpace(*req.ChartOfAccountID) == "" {
return errors.New("chart_of_account_id cannot be empty"), constants.MalformedFieldErrorCode
}
if _, err := uuid.Parse(*req.ChartOfAccountID); err != nil {
return errors.New("chart_of_account_id must be a valid UUID"), constants.MalformedFieldErrorCode
}
}
if req.OutletID != nil {
if strings.TrimSpace(*req.OutletID) == "" {
return errors.New("outlet_id cannot be empty"), constants.MalformedFieldErrorCode
@ -105,6 +105,22 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update
return errors.New("tax cannot be negative"), constants.MalformedFieldErrorCode
}
if req.Items != nil {
for i, item := range req.Items {
if item.ChartOfAccountID != nil {
if strings.TrimSpace(*item.ChartOfAccountID) == "" {
return fmt.Errorf("item %d: chart_of_account_id cannot be empty", i), constants.MalformedFieldErrorCode
}
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
}
}
if item.Amount != nil && *item.Amount <= 0 {
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
}
}
}
return nil, ""
}

View File

@ -1 +1,2 @@
DROP TABLE IF EXISTS expense_items;
DROP TABLE IF EXISTS expenses;

View File

@ -5,7 +5,6 @@ CREATE TABLE expenses (
receiver VARCHAR(255) NOT NULL,
transaction_date DATE NOT NULL,
code_number VARCHAR(50) NOT NULL,
chart_of_account_id UUID NOT NULL REFERENCES chart_of_accounts(id) ON DELETE RESTRICT,
description TEXT,
tax DECIMAL(15,2) NOT NULL DEFAULT 0,
total DECIMAL(15,2) NOT NULL DEFAULT 0,
@ -16,7 +15,19 @@ CREATE TABLE expenses (
CREATE INDEX idx_expenses_organization_id ON expenses(organization_id);
CREATE INDEX idx_expenses_outlet_id ON expenses(outlet_id);
CREATE INDEX idx_expenses_chart_of_account_id ON expenses(chart_of_account_id);
CREATE INDEX idx_expenses_transaction_date ON expenses(transaction_date);
CREATE INDEX idx_expenses_code_number ON expenses(code_number);
CREATE INDEX idx_expenses_created_at ON expenses(created_at);
CREATE TABLE expense_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
expense_id UUID NOT NULL REFERENCES expenses(id) ON DELETE CASCADE,
chart_of_account_id UUID NOT NULL REFERENCES chart_of_accounts(id) ON DELETE RESTRICT,
description TEXT,
amount DECIMAL(15,2) NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_expense_items_expense_id ON expense_items(expense_id);
CREATE INDEX idx_expense_items_chart_of_account_id ON expense_items(chart_of_account_id);