Update expense for product category (non-inventory type)
This commit is contained in:
parent
e09feff36d
commit
c3db919531
@ -396,7 +396,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
||||||
notificationProcessor: buildNotificationProcessor(cfg, repos),
|
notificationProcessor: buildNotificationProcessor(cfg, repos),
|
||||||
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
|
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
|
||||||
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo),
|
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo, repos.purchaseCategoryRepo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,10 +19,11 @@ type CreateExpenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateExpenseItemRequest struct {
|
type CreateExpenseItemRequest struct {
|
||||||
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
|
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
|
||||||
Item string `json:"item" validate:"required"`
|
PurchaseCategoryID string `json:"purchase_category_id" validate:"required"`
|
||||||
Description *string `json:"description,omitempty"`
|
Item string `json:"item" validate:"required"`
|
||||||
Amount float64 `json:"amount" validate:"required"`
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount float64 `json:"amount" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseRequest struct {
|
type UpdateExpenseRequest struct {
|
||||||
@ -39,10 +40,11 @@ type UpdateExpenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseItemRequest struct {
|
type UpdateExpenseItemRequest struct {
|
||||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||||
Item *string `json:"item,omitempty"`
|
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Item *string `json:"item,omitempty"`
|
||||||
Amount *float64 `json:"amount,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseResponse struct {
|
type ExpenseResponse struct {
|
||||||
@ -63,15 +65,19 @@ type ExpenseResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseItemResponse struct {
|
type ExpenseItemResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ExpenseID uuid.UUID `json:"expense_id"`
|
ExpenseID uuid.UUID `json:"expense_id"`
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||||
Item string `json:"item"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||||
Amount float64 `json:"amount"`
|
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Item string `json:"item"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListExpenseRequest struct {
|
type ListExpenseRequest struct {
|
||||||
@ -100,15 +106,16 @@ type ExpenseAnalyticsRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsResponse struct {
|
type ExpenseAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
GroupBy string `json:"group_by"`
|
GroupBy string `json:"group_by"`
|
||||||
Summary ExpenseAnalyticsSummary `json:"summary"`
|
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||||
Data []ExpenseAnalyticsData `json:"data"`
|
Data []ExpenseAnalyticsData `json:"data"`
|
||||||
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||||
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
||||||
|
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsSummary struct {
|
type ExpenseAnalyticsSummary struct {
|
||||||
@ -130,6 +137,15 @@ type ExpenseAnalyticsData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsCategoryData struct {
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
|
PurchaseCategoryName string `json:"purchase_category_name"`
|
||||||
|
PurchaseCategoryType string `json:"purchase_category_type"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsChartOfAccountData struct {
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
ChartOfAccountName string `json:"chart_of_account_name"`
|
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
|||||||
@ -29,10 +29,11 @@ type Expense struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalytics struct {
|
type ExpenseAnalytics struct {
|
||||||
Summary ExpenseAnalyticsSummary
|
Summary ExpenseAnalyticsSummary
|
||||||
Data []ExpenseAnalyticsData
|
Data []ExpenseAnalyticsData
|
||||||
CategoryData []ExpenseAnalyticsCategoryData
|
CategoryData []ExpenseAnalyticsCategoryData
|
||||||
ItemData []ExpenseAnalyticsItemData
|
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData
|
||||||
|
ItemData []ExpenseAnalyticsItemData
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsSummary struct {
|
type ExpenseAnalyticsSummary struct {
|
||||||
@ -54,6 +55,15 @@ type ExpenseAnalyticsData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsCategoryData struct {
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
|
PurchaseCategoryID uuid.UUID
|
||||||
|
PurchaseCategoryName string
|
||||||
|
PurchaseCategoryType string
|
||||||
|
TotalAmount float64
|
||||||
|
ExpenseCount int64
|
||||||
|
ItemCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsChartOfAccountData struct {
|
||||||
ChartOfAccountID uuid.UUID
|
ChartOfAccountID uuid.UUID
|
||||||
ChartOfAccountName string
|
ChartOfAccountName string
|
||||||
TotalAmount float64
|
TotalAmount float64
|
||||||
|
|||||||
@ -9,17 +9,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ExpenseItem struct {
|
type ExpenseItem struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
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"`
|
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"`
|
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
|
||||||
Item string `gorm:"not null;size:255" json:"item"`
|
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id"`
|
||||||
Description *string `gorm:"type:text" json:"description"`
|
Item string `gorm:"not null;size:255" json:"item"`
|
||||||
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
Description *string `gorm:"type:text" json:"description"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
|
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
|
||||||
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
|
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
|
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@ -95,20 +95,27 @@ func ExpenseItemEntityToResponse(entity *entities.ExpenseItem) *models.ExpenseIt
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := &models.ExpenseItemResponse{
|
response := &models.ExpenseItemResponse{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
ExpenseID: entity.ExpenseID,
|
ExpenseID: entity.ExpenseID,
|
||||||
ChartOfAccountID: entity.ChartOfAccountID,
|
ChartOfAccountID: entity.ChartOfAccountID,
|
||||||
Item: entity.Item,
|
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||||
Description: entity.Description,
|
Item: entity.Item,
|
||||||
Amount: entity.Amount,
|
Description: entity.Description,
|
||||||
CreatedAt: entity.CreatedAt,
|
Amount: entity.Amount,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
if entity.ChartOfAccount != nil {
|
if entity.ChartOfAccount != nil {
|
||||||
response.ChartOfAccountName = entity.ChartOfAccount.Name
|
response.ChartOfAccountName = entity.ChartOfAccount.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if entity.PurchaseCategory != nil {
|
||||||
|
response.PurchaseCategoryName = entity.PurchaseCategory.Name
|
||||||
|
response.PurchaseCategoryType = string(entity.PurchaseCategory.Type)
|
||||||
|
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
|
||||||
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,14 +23,15 @@ type Expense struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseItem struct {
|
type ExpenseItem struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ExpenseID uuid.UUID `json:"expense_id"`
|
ExpenseID uuid.UUID `json:"expense_id"`
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
Item string `json:"item"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description"`
|
Item string `json:"item"`
|
||||||
Amount float64 `json:"amount"`
|
Description *string `json:"description"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Amount float64 `json:"amount"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseResponse struct {
|
type ExpenseResponse struct {
|
||||||
@ -51,15 +52,19 @@ type ExpenseResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseItemResponse struct {
|
type ExpenseItemResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ExpenseID uuid.UUID `json:"expense_id"`
|
ExpenseID uuid.UUID `json:"expense_id"`
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||||
Item string `json:"item"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||||
Amount float64 `json:"amount"`
|
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Item string `json:"item"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateExpenseRequest struct {
|
type CreateExpenseRequest struct {
|
||||||
@ -75,10 +80,11 @@ type CreateExpenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateExpenseItemRequest struct {
|
type CreateExpenseItemRequest struct {
|
||||||
ChartOfAccountID string `json:"chart_of_account_id"`
|
ChartOfAccountID string `json:"chart_of_account_id"`
|
||||||
Item string `json:"item"`
|
PurchaseCategoryID string `json:"purchase_category_id"`
|
||||||
Description *string `json:"description,omitempty"`
|
Item string `json:"item"`
|
||||||
Amount float64 `json:"amount"`
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseRequest struct {
|
type UpdateExpenseRequest struct {
|
||||||
@ -95,10 +101,11 @@ type UpdateExpenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseItemRequest struct {
|
type UpdateExpenseItemRequest struct {
|
||||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||||
Item *string `json:"item,omitempty"`
|
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Item *string `json:"item,omitempty"`
|
||||||
Amount *float64 `json:"amount,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListExpenseRequest struct {
|
type ListExpenseRequest struct {
|
||||||
@ -128,15 +135,16 @@ type ExpenseAnalyticsRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsResponse struct {
|
type ExpenseAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
GroupBy string `json:"group_by"`
|
GroupBy string `json:"group_by"`
|
||||||
Summary ExpenseAnalyticsSummary `json:"summary"`
|
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||||
Data []ExpenseAnalyticsData `json:"data"`
|
Data []ExpenseAnalyticsData `json:"data"`
|
||||||
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||||
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
||||||
|
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsSummary struct {
|
type ExpenseAnalyticsSummary struct {
|
||||||
@ -158,6 +166,15 @@ type ExpenseAnalyticsData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsCategoryData struct {
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
|
PurchaseCategoryName string `json:"purchase_category_name"`
|
||||||
|
PurchaseCategoryType string `json:"purchase_category_type"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsChartOfAccountData struct {
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
ChartOfAccountName string `json:"chart_of_account_name"`
|
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
|||||||
@ -23,12 +23,14 @@ type ExpenseProcessor interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseProcessorImpl struct {
|
type ExpenseProcessorImpl struct {
|
||||||
expenseRepo ExpenseRepository
|
expenseRepo ExpenseRepository
|
||||||
|
purchaseCategoryRepo PurchaseCategoryRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository) *ExpenseProcessorImpl {
|
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository, purchaseCategoryRepo PurchaseCategoryRepository) *ExpenseProcessorImpl {
|
||||||
return &ExpenseProcessorImpl{
|
return &ExpenseProcessorImpl{
|
||||||
expenseRepo: expenseRepo,
|
expenseRepo: expenseRepo,
|
||||||
|
purchaseCategoryRepo: purchaseCategoryRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +50,30 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
|
|||||||
status = *req.Status
|
status = *req.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items := make([]entities.ExpenseItem, len(req.Items))
|
||||||
|
for i, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
purchaseCategoryID, err := uuid.Parse(itemReq.PurchaseCategoryID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
||||||
|
}
|
||||||
|
if err := p.validateNonInventoryPurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items[i] = entities.ExpenseItem{
|
||||||
|
ChartOfAccountID: chartOfAccountID,
|
||||||
|
PurchaseCategoryID: purchaseCategoryID,
|
||||||
|
Item: itemReq.Item,
|
||||||
|
Description: itemReq.Description,
|
||||||
|
Amount: itemReq.Amount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expenseEntity := &entities.Expense{
|
expenseEntity := &entities.Expense{
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
OutletID: outletID,
|
OutletID: outletID,
|
||||||
@ -65,21 +91,10 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
|
|||||||
return nil, fmt.Errorf("failed to create expense: %w", err)
|
return nil, fmt.Errorf("failed to create expense: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, itemReq := range req.Items {
|
for i := range items {
|
||||||
chartOfAccountID, err := uuid.Parse(itemReq.ChartOfAccountID)
|
items[i].ExpenseID = expenseEntity.ID
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemEntity := &entities.ExpenseItem{
|
err = p.expenseRepo.CreateItem(ctx, &items[i])
|
||||||
ExpenseID: expenseEntity.ID,
|
|
||||||
ChartOfAccountID: chartOfAccountID,
|
|
||||||
Item: itemReq.Item,
|
|
||||||
Description: itemReq.Description,
|
|
||||||
Amount: itemReq.Amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.expenseRepo.CreateItem(ctx, itemEntity)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
||||||
}
|
}
|
||||||
@ -135,13 +150,10 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
expenseEntity.Reserved1 = req.Reserved1
|
expenseEntity.Reserved1 = req.Reserved1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var items []entities.ExpenseItem
|
||||||
if req.Items != nil {
|
if req.Items != nil {
|
||||||
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
|
items = make([]entities.ExpenseItem, len(req.Items))
|
||||||
if err != nil {
|
for i, itemReq := range req.Items {
|
||||||
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, itemReq := range req.Items {
|
|
||||||
chartOfAccountID := uuid.Nil
|
chartOfAccountID := uuid.Nil
|
||||||
if itemReq.ChartOfAccountID != nil {
|
if itemReq.ChartOfAccountID != nil {
|
||||||
chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID)
|
chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID)
|
||||||
@ -150,6 +162,17 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if itemReq.PurchaseCategoryID == nil {
|
||||||
|
return nil, fmt.Errorf("purchase_category_id is required for item")
|
||||||
|
}
|
||||||
|
purchaseCategoryID, err := uuid.Parse(*itemReq.PurchaseCategoryID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
||||||
|
}
|
||||||
|
if err := p.validateNonInventoryPurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
amount := 0.0
|
amount := 0.0
|
||||||
if itemReq.Amount != nil {
|
if itemReq.Amount != nil {
|
||||||
amount = *itemReq.Amount
|
amount = *itemReq.Amount
|
||||||
@ -159,15 +182,23 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
item = *itemReq.Item
|
item = *itemReq.Item
|
||||||
}
|
}
|
||||||
|
|
||||||
itemEntity := &entities.ExpenseItem{
|
items[i] = entities.ExpenseItem{
|
||||||
ExpenseID: expenseEntity.ID,
|
ExpenseID: expenseEntity.ID,
|
||||||
ChartOfAccountID: chartOfAccountID,
|
ChartOfAccountID: chartOfAccountID,
|
||||||
Item: item,
|
PurchaseCategoryID: purchaseCategoryID,
|
||||||
Description: itemReq.Description,
|
Item: item,
|
||||||
Amount: amount,
|
Description: itemReq.Description,
|
||||||
|
Amount: amount,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = p.expenseRepo.CreateItem(ctx, itemEntity)
|
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range items {
|
||||||
|
err = p.expenseRepo.CreateItem(ctx, &items[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
||||||
}
|
}
|
||||||
@ -252,6 +283,18 @@ func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *mod
|
|||||||
categoryData := make([]models.ExpenseAnalyticsCategoryData, len(result.CategoryData))
|
categoryData := make([]models.ExpenseAnalyticsCategoryData, len(result.CategoryData))
|
||||||
for i, item := range result.CategoryData {
|
for i, item := range result.CategoryData {
|
||||||
categoryData[i] = models.ExpenseAnalyticsCategoryData{
|
categoryData[i] = models.ExpenseAnalyticsCategoryData{
|
||||||
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
|
PurchaseCategoryName: item.PurchaseCategoryName,
|
||||||
|
PurchaseCategoryType: item.PurchaseCategoryType,
|
||||||
|
TotalAmount: item.TotalAmount,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
ItemCount: item.ItemCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chartOfAccountData := make([]models.ExpenseAnalyticsChartOfAccountData, len(result.ChartOfAccountData))
|
||||||
|
for i, item := range result.ChartOfAccountData {
|
||||||
|
chartOfAccountData[i] = models.ExpenseAnalyticsChartOfAccountData{
|
||||||
ChartOfAccountID: item.ChartOfAccountID,
|
ChartOfAccountID: item.ChartOfAccountID,
|
||||||
ChartOfAccountName: item.ChartOfAccountName,
|
ChartOfAccountName: item.ChartOfAccountName,
|
||||||
TotalAmount: item.TotalAmount,
|
TotalAmount: item.TotalAmount,
|
||||||
@ -284,8 +327,26 @@ func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *mod
|
|||||||
TotalCategories: result.Summary.TotalCategories,
|
TotalCategories: result.Summary.TotalCategories,
|
||||||
TotalItems: result.Summary.TotalItems,
|
TotalItems: result.Summary.TotalItems,
|
||||||
},
|
},
|
||||||
Data: data,
|
Data: data,
|
||||||
CategoryData: categoryData,
|
CategoryData: categoryData,
|
||||||
ItemData: itemData,
|
ChartOfAccountData: chartOfAccountData,
|
||||||
|
ItemData: itemData,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ExpenseProcessorImpl) validateNonInventoryPurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID) error {
|
||||||
|
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !category.IsActive {
|
||||||
|
return fmt.Errorf("purchase category is inactive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if category.Type != entities.PurchaseCategoryTypeNonInventory {
|
||||||
|
return fmt.Errorf("purchase category must be non_inventory")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,45 @@ type expenseRepositoryCaptureStub struct {
|
|||||||
analytics *entities.ExpenseAnalytics
|
analytics *entities.ExpenseAnalytics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type expensePurchaseCategoryRepositoryStub struct {
|
||||||
|
category *entities.PurchaseCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*expensePurchaseCategoryRepositoryStub) Create(context.Context, *entities.PurchaseCategory) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *expensePurchaseCategoryRepositoryStub) GetByIDAndOrganizationID(context.Context, uuid.UUID, uuid.UUID) (*entities.PurchaseCategory, error) {
|
||||||
|
return s.category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*expensePurchaseCategoryRepositoryStub) Update(context.Context, *entities.PurchaseCategory) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*expensePurchaseCategoryRepositoryStub) SoftDelete(context.Context, uuid.UUID, uuid.UUID) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*expensePurchaseCategoryRepositoryStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.PurchaseCategory, int64, error) {
|
||||||
|
return nil, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*expensePurchaseCategoryRepositoryStub) ExistsByCode(context.Context, uuid.UUID, string, *uuid.UUID) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExpensePurchaseCategoryRepo(categoryID uuid.UUID, categoryType entities.PurchaseCategoryType) *expensePurchaseCategoryRepositoryStub {
|
||||||
|
return &expensePurchaseCategoryRepositoryStub{
|
||||||
|
category: &entities.PurchaseCategory{
|
||||||
|
ID: categoryID,
|
||||||
|
Name: "Operational",
|
||||||
|
Type: categoryType,
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *expenseRepositoryCaptureStub) Create(_ context.Context, expense *entities.Expense) error {
|
func (s *expenseRepositoryCaptureStub) Create(_ context.Context, expense *entities.Expense) error {
|
||||||
if expense.ID == uuid.Nil {
|
if expense.ID == uuid.Nil {
|
||||||
expense.ID = uuid.New()
|
expense.ID = uuid.New()
|
||||||
@ -62,7 +101,8 @@ func (*expenseRepositoryCaptureStub) DeleteItemsByExpenseID(context.Context, uui
|
|||||||
|
|
||||||
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
p := NewExpenseProcessorImpl(repo)
|
purchaseCategoryID := uuid.New()
|
||||||
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
||||||
chartOfAccountID := uuid.New()
|
chartOfAccountID := uuid.New()
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
@ -73,9 +113,10 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []models.CreateExpenseItemRequest{
|
Items: []models.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: chartOfAccountID.String(),
|
ChartOfAccountID: chartOfAccountID.String(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -84,13 +125,15 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
|||||||
require.NotNil(t, resp)
|
require.NotNil(t, resp)
|
||||||
require.Len(t, repo.createdItems, 1)
|
require.Len(t, repo.createdItems, 1)
|
||||||
require.Equal(t, "Cleaning supplies", repo.createdItems[0].Item)
|
require.Equal(t, "Cleaning supplies", repo.createdItems[0].Item)
|
||||||
|
require.Equal(t, purchaseCategoryID, repo.createdItems[0].PurchaseCategoryID)
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
require.Equal(t, "Cleaning supplies", resp.Items[0].Item)
|
require.Equal(t, "Cleaning supplies", resp.Items[0].Item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
p := NewExpenseProcessorImpl(repo)
|
purchaseCategoryID := uuid.New()
|
||||||
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
Receiver: "Cashier",
|
Receiver: "Cashier",
|
||||||
@ -100,9 +143,10 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []models.CreateExpenseItemRequest{
|
Items: []models.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -115,7 +159,8 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
|||||||
|
|
||||||
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
p := NewExpenseProcessorImpl(repo)
|
purchaseCategoryID := uuid.New()
|
||||||
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
||||||
status := "approved"
|
status := "approved"
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
@ -127,9 +172,10 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []models.CreateExpenseItemRequest{
|
Items: []models.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -140,8 +186,35 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
|||||||
require.Equal(t, "approved", resp.Status)
|
require.Equal(t, "approved", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExpenseProcessorCreateRejectsRawMaterialPurchaseCategory(t *testing.T) {
|
||||||
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
|
purchaseCategoryID := uuid.New()
|
||||||
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeRawMaterial))
|
||||||
|
|
||||||
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
|
Receiver: "Cashier",
|
||||||
|
TransactionDate: "2026-05-29",
|
||||||
|
CodeNumber: "EXP-001",
|
||||||
|
OutletID: uuid.NewString(),
|
||||||
|
Total: 10000,
|
||||||
|
Items: []models.CreateExpenseItemRequest{
|
||||||
|
{
|
||||||
|
ChartOfAccountID: uuid.NewString(),
|
||||||
|
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||||
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, resp)
|
||||||
|
require.Contains(t, err.Error(), "non_inventory")
|
||||||
|
}
|
||||||
|
|
||||||
func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) {
|
func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) {
|
||||||
coaID := uuid.New()
|
coaID := uuid.New()
|
||||||
|
purchaseCategoryID := uuid.New()
|
||||||
outletID := uuid.New()
|
outletID := uuid.New()
|
||||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
repo := &expenseRepositoryCaptureStub{
|
repo := &expenseRepositoryCaptureStub{
|
||||||
@ -165,6 +238,16 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
CategoryData: []entities.ExpenseAnalyticsCategoryData{
|
CategoryData: []entities.ExpenseAnalyticsCategoryData{
|
||||||
|
{
|
||||||
|
PurchaseCategoryID: purchaseCategoryID,
|
||||||
|
PurchaseCategoryName: "Operational Supplies",
|
||||||
|
PurchaseCategoryType: "non_inventory",
|
||||||
|
TotalAmount: 100000,
|
||||||
|
ExpenseCount: 2,
|
||||||
|
ItemCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ChartOfAccountData: []entities.ExpenseAnalyticsChartOfAccountData{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: coaID,
|
ChartOfAccountID: coaID,
|
||||||
ChartOfAccountName: "Operational",
|
ChartOfAccountName: "Operational",
|
||||||
@ -183,7 +266,7 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
p := NewExpenseProcessorImpl(repo)
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
||||||
|
|
||||||
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
|
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
|
||||||
OrganizationID: uuid.New(),
|
OrganizationID: uuid.New(),
|
||||||
@ -200,7 +283,9 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
|||||||
require.Len(t, resp.Data, 1)
|
require.Len(t, resp.Data, 1)
|
||||||
require.Equal(t, int64(2), resp.Data[0].ExpenseCount)
|
require.Equal(t, int64(2), resp.Data[0].ExpenseCount)
|
||||||
require.Len(t, resp.CategoryData, 1)
|
require.Len(t, resp.CategoryData, 1)
|
||||||
require.Equal(t, coaID, resp.CategoryData[0].ChartOfAccountID)
|
require.Equal(t, purchaseCategoryID, resp.CategoryData[0].PurchaseCategoryID)
|
||||||
|
require.Len(t, resp.ChartOfAccountData, 1)
|
||||||
|
require.Equal(t, coaID, resp.ChartOfAccountData[0].ChartOfAccountID)
|
||||||
require.Len(t, resp.ItemData, 1)
|
require.Len(t, resp.ItemData, 1)
|
||||||
require.Equal(t, "Cleaning supplies", resp.ItemData[0].Item)
|
require.Equal(t, "Cleaning supplies", resp.ItemData[0].Item)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ func (r *ExpenseRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*ent
|
|||||||
var expense entities.Expense
|
var expense entities.Expense
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Items.ChartOfAccount").
|
Preload("Items.ChartOfAccount").
|
||||||
|
Preload("Items.PurchaseCategory").
|
||||||
First(&expense, "id = ?", id).Error
|
First(&expense, "id = ?", id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -41,6 +42,7 @@ func (r *ExpenseRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id
|
|||||||
var expense entities.Expense
|
var expense entities.Expense
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Items.ChartOfAccount").
|
Preload("Items.ChartOfAccount").
|
||||||
|
Preload("Items.PurchaseCategory").
|
||||||
Where("id = ? AND organization_id = ?", id, organizationID).
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
First(&expense).Error
|
First(&expense).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -107,6 +109,7 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU
|
|||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Preload("Items.ChartOfAccount").
|
Preload("Items.ChartOfAccount").
|
||||||
|
Preload("Items.PurchaseCategory").
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
Offset(offset).
|
Offset(offset).
|
||||||
@ -139,7 +142,7 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
Table("expense_items ei").
|
Table("expense_items ei").
|
||||||
Select(`
|
Select(`
|
||||||
COUNT(ei.id) as total_items,
|
COUNT(ei.id) as total_items,
|
||||||
COUNT(DISTINCT ei.chart_of_account_id) as total_categories
|
COUNT(DISTINCT ei.purchase_category_id) as total_categories
|
||||||
`).
|
`).
|
||||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||||
Where("e.organization_id = ?", organizationID).
|
Where("e.organization_id = ?", organizationID).
|
||||||
@ -174,7 +177,7 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
COALESCE(SUM(item_counts.categories), 0) as categories
|
COALESCE(SUM(item_counts.categories), 0) as categories
|
||||||
`).
|
`).
|
||||||
Joins(`LEFT JOIN (
|
Joins(`LEFT JOIN (
|
||||||
SELECT expense_id, COUNT(id) as items, COUNT(DISTINCT chart_of_account_id) as categories
|
SELECT expense_id, COUNT(id) as items, COUNT(DISTINCT purchase_category_id) as categories
|
||||||
FROM expense_items
|
FROM expense_items
|
||||||
GROUP BY expense_id
|
GROUP BY expense_id
|
||||||
) item_counts ON item_counts.expense_id = e.id`).
|
) item_counts ON item_counts.expense_id = e.id`).
|
||||||
@ -192,6 +195,32 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
|
|
||||||
var categoryData []entities.ExpenseAnalyticsCategoryData
|
var categoryData []entities.ExpenseAnalyticsCategoryData
|
||||||
categoryQuery := r.db.WithContext(ctx).
|
categoryQuery := r.db.WithContext(ctx).
|
||||||
|
Table("expense_items ei").
|
||||||
|
Select(`
|
||||||
|
pc.id as purchase_category_id,
|
||||||
|
pc.name as purchase_category_name,
|
||||||
|
pc.type as purchase_category_type,
|
||||||
|
COALESCE(SUM(ei.amount), 0) as total_amount,
|
||||||
|
COUNT(DISTINCT e.id) as expense_count,
|
||||||
|
COUNT(ei.id) as item_count
|
||||||
|
`).
|
||||||
|
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||||
|
Joins("JOIN purchase_categories pc ON ei.purchase_category_id = pc.id").
|
||||||
|
Where("e.organization_id = ?", organizationID).
|
||||||
|
Where("pc.type = ?", entities.PurchaseCategoryTypeNonInventory).
|
||||||
|
Where("e.status = ?", "approved").
|
||||||
|
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo).
|
||||||
|
Group("pc.id, pc.name, pc.type").
|
||||||
|
Order("total_amount DESC")
|
||||||
|
if outletID != nil {
|
||||||
|
categoryQuery = categoryQuery.Where("e.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
if err := categoryQuery.Scan(&categoryData).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var chartOfAccountData []entities.ExpenseAnalyticsChartOfAccountData
|
||||||
|
chartOfAccountQuery := r.db.WithContext(ctx).
|
||||||
Table("expense_items ei").
|
Table("expense_items ei").
|
||||||
Select(`
|
Select(`
|
||||||
COALESCE(parent_coa.id, coa.id) as chart_of_account_id,
|
COALESCE(parent_coa.id, coa.id) as chart_of_account_id,
|
||||||
@ -209,9 +238,9 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
Group("COALESCE(parent_coa.id, coa.id), COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
Group("COALESCE(parent_coa.id, coa.id), COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||||
Order("total_amount DESC")
|
Order("total_amount DESC")
|
||||||
if outletID != nil {
|
if outletID != nil {
|
||||||
categoryQuery = categoryQuery.Where("e.outlet_id = ?", *outletID)
|
chartOfAccountQuery = chartOfAccountQuery.Where("e.outlet_id = ?", *outletID)
|
||||||
}
|
}
|
||||||
if err := categoryQuery.Scan(&categoryData).Error; err != nil {
|
if err := chartOfAccountQuery.Scan(&chartOfAccountData).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,10 +268,11 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.ExpenseAnalytics{
|
return &entities.ExpenseAnalytics{
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
Data: data,
|
Data: data,
|
||||||
CategoryData: categoryData,
|
CategoryData: categoryData,
|
||||||
ItemData: itemData,
|
ChartOfAccountData: chartOfAccountData,
|
||||||
|
ItemData: itemData,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,10 +27,11 @@ func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.Cre
|
|||||||
|
|
||||||
func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest {
|
func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest {
|
||||||
return models.CreateExpenseItemRequest{
|
return models.CreateExpenseItemRequest{
|
||||||
ChartOfAccountID: req.ChartOfAccountID,
|
ChartOfAccountID: req.ChartOfAccountID,
|
||||||
Item: req.Item,
|
PurchaseCategoryID: req.PurchaseCategoryID,
|
||||||
Description: req.Description,
|
Item: req.Item,
|
||||||
Amount: req.Amount,
|
Description: req.Description,
|
||||||
|
Amount: req.Amount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,10 +61,11 @@ func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.Upd
|
|||||||
|
|
||||||
func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest {
|
func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest {
|
||||||
return models.UpdateExpenseItemRequest{
|
return models.UpdateExpenseItemRequest{
|
||||||
ChartOfAccountID: req.ChartOfAccountID,
|
ChartOfAccountID: req.ChartOfAccountID,
|
||||||
Item: req.Item,
|
PurchaseCategoryID: req.PurchaseCategoryID,
|
||||||
Description: req.Description,
|
Item: req.Item,
|
||||||
Amount: req.Amount,
|
Description: req.Description,
|
||||||
|
Amount: req.Amount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,15 +111,19 @@ func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.E
|
|||||||
|
|
||||||
func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contract.ExpenseItemResponse {
|
func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contract.ExpenseItemResponse {
|
||||||
return contract.ExpenseItemResponse{
|
return contract.ExpenseItemResponse{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
ExpenseID: item.ExpenseID,
|
ExpenseID: item.ExpenseID,
|
||||||
ChartOfAccountID: item.ChartOfAccountID,
|
ChartOfAccountID: item.ChartOfAccountID,
|
||||||
ChartOfAccountName: item.ChartOfAccountName,
|
ChartOfAccountName: item.ChartOfAccountName,
|
||||||
Item: item.Item,
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
Description: item.Description,
|
PurchaseCategoryName: item.PurchaseCategoryName,
|
||||||
Amount: item.Amount,
|
PurchaseCategoryType: item.PurchaseCategoryType,
|
||||||
CreatedAt: item.CreatedAt,
|
PurchaseCategory: PurchaseCategoryModelResponseToResponse(item.PurchaseCategory),
|
||||||
UpdatedAt: item.UpdatedAt,
|
Item: item.Item,
|
||||||
|
Description: item.Description,
|
||||||
|
Amount: item.Amount,
|
||||||
|
CreatedAt: item.CreatedAt,
|
||||||
|
UpdatedAt: item.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +182,18 @@ func ExpenseAnalyticsModelToContract(resp *models.ExpenseAnalyticsResponse) *con
|
|||||||
categoryData := make([]contract.ExpenseAnalyticsCategoryData, len(resp.CategoryData))
|
categoryData := make([]contract.ExpenseAnalyticsCategoryData, len(resp.CategoryData))
|
||||||
for i, item := range resp.CategoryData {
|
for i, item := range resp.CategoryData {
|
||||||
categoryData[i] = contract.ExpenseAnalyticsCategoryData{
|
categoryData[i] = contract.ExpenseAnalyticsCategoryData{
|
||||||
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
|
PurchaseCategoryName: item.PurchaseCategoryName,
|
||||||
|
PurchaseCategoryType: item.PurchaseCategoryType,
|
||||||
|
TotalAmount: item.TotalAmount,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
ItemCount: item.ItemCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chartOfAccountData := make([]contract.ExpenseAnalyticsChartOfAccountData, len(resp.ChartOfAccountData))
|
||||||
|
for i, item := range resp.ChartOfAccountData {
|
||||||
|
chartOfAccountData[i] = contract.ExpenseAnalyticsChartOfAccountData{
|
||||||
ChartOfAccountID: item.ChartOfAccountID,
|
ChartOfAccountID: item.ChartOfAccountID,
|
||||||
ChartOfAccountName: item.ChartOfAccountName,
|
ChartOfAccountName: item.ChartOfAccountName,
|
||||||
TotalAmount: item.TotalAmount,
|
TotalAmount: item.TotalAmount,
|
||||||
@ -208,8 +226,9 @@ func ExpenseAnalyticsModelToContract(resp *models.ExpenseAnalyticsResponse) *con
|
|||||||
TotalCategories: resp.Summary.TotalCategories,
|
TotalCategories: resp.Summary.TotalCategories,
|
||||||
TotalItems: resp.Summary.TotalItems,
|
TotalItems: resp.Summary.TotalItems,
|
||||||
},
|
},
|
||||||
Data: data,
|
Data: data,
|
||||||
CategoryData: categoryData,
|
CategoryData: categoryData,
|
||||||
ItemData: itemData,
|
ChartOfAccountData: chartOfAccountData,
|
||||||
|
ItemData: itemData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,12 +68,18 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create
|
|||||||
if strings.TrimSpace(item.ChartOfAccountID) == "" {
|
if strings.TrimSpace(item.ChartOfAccountID) == "" {
|
||||||
return fmt.Errorf("item %d: chart_of_account_id is required", i), constants.MissingFieldErrorCode
|
return fmt.Errorf("item %d: chart_of_account_id is required", i), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(item.PurchaseCategoryID) == "" {
|
||||||
|
return fmt.Errorf("item %d: purchase_category_id is required", i), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
if strings.TrimSpace(item.Item) == "" {
|
if strings.TrimSpace(item.Item) == "" {
|
||||||
return fmt.Errorf("item %d: item is required", i), constants.MissingFieldErrorCode
|
return fmt.Errorf("item %d: item is required", i), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
if _, err := uuid.Parse(item.ChartOfAccountID); err != nil {
|
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
|
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
if _, err := uuid.Parse(item.PurchaseCategoryID); err != nil {
|
||||||
|
return fmt.Errorf("item %d: purchase_category_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
if item.Amount <= 0 {
|
if item.Amount <= 0 {
|
||||||
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
|
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
@ -126,6 +132,15 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update
|
|||||||
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if item.PurchaseCategoryID == nil {
|
||||||
|
return fmt.Errorf("item %d: purchase_category_id is required", i), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(*item.PurchaseCategoryID) == "" {
|
||||||
|
return fmt.Errorf("item %d: purchase_category_id cannot be empty", i), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
if _, err := uuid.Parse(*item.PurchaseCategoryID); err != nil {
|
||||||
|
return fmt.Errorf("item %d: purchase_category_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
if item.Item != nil && strings.TrimSpace(*item.Item) == "" {
|
if item.Item != nil && strings.TrimSpace(*item.Item) == "" {
|
||||||
return fmt.Errorf("item %d: item cannot be empty", i), constants.MalformedFieldErrorCode
|
return fmt.Errorf("item %d: item cannot be empty", i), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,8 +21,9 @@ func TestExpenseValidatorCreateRequiresItemName(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Amount: 10000,
|
PurchaseCategoryID: uuid.NewString(),
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -45,9 +46,10 @@ func TestExpenseValidatorCreateDoesNotRequireHeaderExpenseName(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: uuid.NewString(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -71,9 +73,10 @@ func TestExpenseValidatorCreateAllowsValidOptionalStatus(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: uuid.NewString(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -97,9 +100,10 @@ func TestExpenseValidatorCreateRejectsInvalidStatus(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: uuid.NewString(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -114,10 +118,11 @@ func TestExpenseValidatorCreateRejectsInvalidStatus(t *testing.T) {
|
|||||||
func TestExpenseValidatorUpdateRejectsEmptyItemNameWhenProvided(t *testing.T) {
|
func TestExpenseValidatorUpdateRejectsEmptyItemNameWhenProvided(t *testing.T) {
|
||||||
v := NewExpenseValidator()
|
v := NewExpenseValidator()
|
||||||
empty := " "
|
empty := " "
|
||||||
|
purchaseCategoryID := uuid.NewString()
|
||||||
|
|
||||||
req := &contract.UpdateExpenseRequest{
|
req := &contract.UpdateExpenseRequest{
|
||||||
Items: []contract.UpdateExpenseItemRequest{
|
Items: []contract.UpdateExpenseItemRequest{
|
||||||
{Item: &empty},
|
{PurchaseCategoryID: &purchaseCategoryID, Item: &empty},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_expense_items_purchase_category_id;
|
||||||
|
ALTER TABLE expense_items DROP COLUMN IF EXISTS purchase_category_id;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE expense_items
|
||||||
|
ADD COLUMN IF NOT EXISTS purchase_category_id UUID REFERENCES purchase_categories(id) ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_expense_items_purchase_category_id
|
||||||
|
ON expense_items(purchase_category_id);
|
||||||
Loading…
x
Reference in New Issue
Block a user