package processor import ( "context" "testing" "time" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/models" "github.com/google/uuid" "github.com/stretchr/testify/require" ) type expenseRepositoryCaptureStub struct { createdExpense *entities.Expense createdItems []*entities.ExpenseItem 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 { if expense.ID == uuid.Nil { expense.ID = uuid.New() } s.createdExpense = expense return nil } func (s *expenseRepositoryCaptureStub) GetByID(context.Context, uuid.UUID) (*entities.Expense, error) { if s.createdExpense == nil { return nil, nil } items := make([]entities.ExpenseItem, len(s.createdItems)) for i, item := range s.createdItems { items[i] = *item } s.createdExpense.Items = items return s.createdExpense, nil } func (*expenseRepositoryCaptureStub) GetByIDAndOrganizationID(context.Context, uuid.UUID, uuid.UUID) (*entities.Expense, error) { return nil, nil } func (*expenseRepositoryCaptureStub) Update(context.Context, *entities.Expense) error { return nil } func (*expenseRepositoryCaptureStub) Delete(context.Context, uuid.UUID) error { return nil } func (*expenseRepositoryCaptureStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) { return nil, 0, nil } func (s *expenseRepositoryCaptureStub) GetAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time, string) (*entities.ExpenseAnalytics, error) { return s.analytics, nil } func (s *expenseRepositoryCaptureStub) CreateItem(_ context.Context, item *entities.ExpenseItem) error { if item.ID == uuid.Nil { item.ID = uuid.New() } s.createdItems = append(s.createdItems, item) return nil } func (*expenseRepositoryCaptureStub) DeleteItemsByExpenseID(context.Context, uuid.UUID) error { return nil } func TestExpenseProcessorCreatePersistsItemName(t *testing.T) { repo := &expenseRepositoryCaptureStub{} purchaseCategoryID := uuid.New() p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense)) chartOfAccountID := uuid.New() 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: chartOfAccountID.String(), PurchaseCategoryID: purchaseCategoryID.String(), Item: "Cleaning supplies", Amount: 10000, }, }, }) require.NoError(t, err) require.NotNil(t, resp) require.Len(t, repo.createdItems, 1) require.Equal(t, "Cleaning supplies", repo.createdItems[0].Item) require.Equal(t, purchaseCategoryID, repo.createdItems[0].PurchaseCategoryID) require.Len(t, resp.Items, 1) require.Equal(t, "Cleaning supplies", resp.Items[0].Item) } func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) { repo := &expenseRepositoryCaptureStub{} purchaseCategoryID := uuid.New() p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense)) 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.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "draft", repo.createdExpense.Status) require.Equal(t, "draft", resp.Status) } func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) { repo := &expenseRepositoryCaptureStub{} purchaseCategoryID := uuid.New() p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense)) status := "approved" resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{ Receiver: "Cashier", TransactionDate: "2026-05-29", CodeNumber: "EXP-001", OutletID: uuid.NewString(), Status: &status, Total: 10000, Items: []models.CreateExpenseItemRequest{ { ChartOfAccountID: uuid.NewString(), PurchaseCategoryID: purchaseCategoryID.String(), Item: "Cleaning supplies", Amount: 10000, }, }, }) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "approved", repo.createdExpense.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(), "expense") } func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) { coaID := uuid.New() purchaseCategoryID := uuid.New() outletID := uuid.New() now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) repo := &expenseRepositoryCaptureStub{ analytics: &entities.ExpenseAnalytics{ Summary: entities.ExpenseAnalyticsSummary{ TotalExpenses: 100000, TotalExpenseCount: 2, TotalTax: 10000, AverageExpenseValue: 50000, TotalCategories: 1, TotalItems: 2, }, Data: []entities.ExpenseAnalyticsData{ { Date: now, Expenses: 100000, ExpenseCount: 2, Tax: 10000, Items: 2, Categories: 1, }, }, CategoryData: []entities.ExpenseAnalyticsCategoryData{ { PurchaseCategoryID: purchaseCategoryID, PurchaseCategoryName: "Operational Supplies", PurchaseCategoryType: "expense", TotalAmount: 100000, ExpenseCount: 2, ItemCount: 2, }, }, ChartOfAccountData: []entities.ExpenseAnalyticsChartOfAccountData{ { ChartOfAccountID: coaID, ChartOfAccountName: "Operational", TotalAmount: 100000, ExpenseCount: 2, ItemCount: 2, }, }, ItemData: []entities.ExpenseAnalyticsItemData{ { Item: "Cleaning supplies", TotalAmount: 100000, ExpenseCount: 2, ItemCount: 2, }, }, }, } p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense)) resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{ OrganizationID: uuid.New(), OutletID: &outletID, DateFrom: now, DateTo: now, }) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "day", resp.GroupBy) require.Equal(t, &outletID, resp.OutletID) require.Equal(t, float64(100000), resp.Summary.TotalExpenses) require.Len(t, resp.Data, 1) require.Equal(t, int64(2), resp.Data[0].ExpenseCount) require.Len(t, resp.CategoryData, 1) 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.Equal(t, "Cleaning supplies", resp.ItemData[0].Item) }