Compare commits
3 Commits
8816e4addc
...
5fa9fc5070
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fa9fc5070 | |||
| 07e8be0521 | |||
| 4b7b225f58 |
@ -218,10 +218,9 @@ type ExclusiveSummaryDailyTransaction struct {
|
||||
}
|
||||
|
||||
type ExclusiveSummaryBankBalance struct {
|
||||
Bank string
|
||||
OpeningBalance *float64
|
||||
IncomingMutation *float64
|
||||
OutgoingMutation *float64
|
||||
ClosingBalance *float64
|
||||
Notes *string
|
||||
Bank string
|
||||
AccountType string
|
||||
OpeningBalance float64
|
||||
ClosingBalance float64
|
||||
Description *string
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AnalyticsProcessor interface {
|
||||
@ -677,8 +679,14 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
periods := make([]models.ExclusiveSummaryMonthlyPeriod, 0)
|
||||
for _, bucket := range buildExclusiveSummaryMonthlyBuckets(monthStart) {
|
||||
bankBalance, err := p.buildExclusiveSummaryBankBalances(ctx, req.OrganizationID, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buckets := buildExclusiveSummaryMonthlyBuckets(monthStart)
|
||||
periods := make([]models.ExclusiveSummaryMonthlyPeriod, 0, len(buckets))
|
||||
for _, bucket := range buckets {
|
||||
period, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
@ -700,23 +708,6 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
|
||||
})
|
||||
}
|
||||
|
||||
bankBalances, err := p.analyticsRepo.GetExclusiveSummaryBankBalances(ctx, req.OrganizationID, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get exclusive summary bank balances: %w", err)
|
||||
}
|
||||
|
||||
bankBalance := make([]models.ExclusiveSummaryBankBalance, len(bankBalances))
|
||||
for i, item := range bankBalances {
|
||||
bankBalance[i] = models.ExclusiveSummaryBankBalance{
|
||||
Bank: item.Bank,
|
||||
OpeningBalance: item.OpeningBalance,
|
||||
IncomingMutation: item.IncomingMutation,
|
||||
OutgoingMutation: item.OutgoingMutation,
|
||||
ClosingBalance: item.ClosingBalance,
|
||||
Notes: item.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
return &models.ExclusiveSummaryMonthlyResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
@ -735,6 +726,34 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) buildExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.ExclusiveSummaryBankBalance, error) {
|
||||
balances, err := p.analyticsRepo.GetExclusiveSummaryBankBalances(ctx, organizationID, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get exclusive summary bank balances: %w", err)
|
||||
}
|
||||
|
||||
result := make([]models.ExclusiveSummaryBankBalance, len(balances))
|
||||
for i, balance := range balances {
|
||||
openingBalance := balance.OpeningBalance
|
||||
closingBalance := balance.ClosingBalance
|
||||
notes := strings.TrimSpace(balance.AccountType)
|
||||
if balance.Description != nil && strings.TrimSpace(*balance.Description) != "" {
|
||||
notes = strings.TrimSpace(*balance.Description)
|
||||
}
|
||||
|
||||
result[i] = models.ExclusiveSummaryBankBalance{
|
||||
Bank: balance.Bank,
|
||||
OpeningBalance: &openingBalance,
|
||||
ClosingBalance: &closingBalance,
|
||||
}
|
||||
if notes != "" {
|
||||
result[i].Notes = ¬es
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) buildExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
||||
result, err := p.analyticsRepo.GetExclusiveSummaryAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
||||
if err != nil {
|
||||
|
||||
@ -13,12 +13,11 @@ import (
|
||||
)
|
||||
|
||||
type analyticsRepositoryStub struct {
|
||||
purchasingResult *entities.PurchasingAnalytics
|
||||
profitLossResult *entities.ProfitLossAnalytics
|
||||
exclusiveSummaryResults []*entities.ExclusiveSummaryAnalytics
|
||||
bankBalances []entities.ExclusiveSummaryBankBalance
|
||||
profitLossGroup string
|
||||
exclusiveSummaryCalls int
|
||||
purchasingResult *entities.PurchasingAnalytics
|
||||
profitLossResult *entities.ProfitLossAnalytics
|
||||
exclusiveResult *entities.ExclusiveSummaryAnalytics
|
||||
bankBalances []entities.ExclusiveSummaryBankBalance
|
||||
profitLossGroup string
|
||||
}
|
||||
|
||||
func (analyticsRepositoryStub) GetPaymentMethodAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) ([]*entities.PaymentMethodAnalytics, error) {
|
||||
@ -50,17 +49,11 @@ func (s analyticsRepositoryStub) GetProfitLossAnalytics(_ context.Context, _ uui
|
||||
return s.profitLossResult, nil
|
||||
}
|
||||
|
||||
func (s *analyticsRepositoryStub) GetExclusiveSummaryAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) (*entities.ExclusiveSummaryAnalytics, error) {
|
||||
if s.exclusiveSummaryCalls < len(s.exclusiveSummaryResults) {
|
||||
result := s.exclusiveSummaryResults[s.exclusiveSummaryCalls]
|
||||
s.exclusiveSummaryCalls++
|
||||
return result, nil
|
||||
}
|
||||
s.exclusiveSummaryCalls++
|
||||
return &entities.ExclusiveSummaryAnalytics{}, nil
|
||||
func (s analyticsRepositoryStub) GetExclusiveSummaryAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) (*entities.ExclusiveSummaryAnalytics, error) {
|
||||
return s.exclusiveResult, nil
|
||||
}
|
||||
|
||||
func (s *analyticsRepositoryStub) GetExclusiveSummaryBankBalances(context.Context, uuid.UUID, *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) {
|
||||
func (s analyticsRepositoryStub) GetExclusiveSummaryBankBalances(context.Context, uuid.UUID, *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) {
|
||||
return s.bankBalances, nil
|
||||
}
|
||||
|
||||
@ -88,7 +81,7 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
||||
outletID := uuid.New()
|
||||
outletName := "Main Outlet"
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
||||
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||
purchasingResult: &entities.PurchasingAnalytics{
|
||||
OutletName: &outletName,
|
||||
Summary: entities.PurchasingSummary{
|
||||
@ -141,7 +134,7 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *
|
||||
productID := uuid.New()
|
||||
categoryID := uuid.New()
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
||||
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||
profitLossResult: &entities.ProfitLossAnalytics{
|
||||
Summary: entities.ProfitLossSummary{
|
||||
TotalRevenue: 1000,
|
||||
@ -213,7 +206,7 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *
|
||||
|
||||
func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *testing.T) {
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
||||
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||
profitLossResult: &entities.ProfitLossAnalytics{
|
||||
Summary: entities.ProfitLossSummary{
|
||||
TotalRevenue: 10000,
|
||||
@ -291,28 +284,22 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *tes
|
||||
require.True(t, result.MainSummary[6].IsBold)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesTotalsAndReimburse(t *testing.T) {
|
||||
func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesSummaryAndReimburse(t *testing.T) {
|
||||
now := time.Date(2026, 5, 26, 0, 0, 0, 0, time.UTC)
|
||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
||||
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{
|
||||
{
|
||||
SalesTotal: 1000,
|
||||
HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "RAW", CategoryName: "Raw", Amount: 400},
|
||||
},
|
||||
OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "GAJI", CategoryName: "Gaji", Amount: 250},
|
||||
{CategoryCode: "OPS", CategoryName: "Operasional", Amount: 100},
|
||||
},
|
||||
DailySummary: []entities.ExclusiveSummaryDailySummary{
|
||||
{Date: now, TransactionCount: 3, TotalCost: 750},
|
||||
},
|
||||
DailyTransactions: []entities.ExclusiveSummaryDailyTransaction{
|
||||
{Date: now, CategoryCode: "RAW", CategoryName: "Raw", Description: "beras", Amount: 400, Source: "purchase_order"},
|
||||
{Date: now, CategoryCode: "GAJI", CategoryName: "Gaji", Description: "gaji karyawan", Amount: 200, Source: "purchase_order"},
|
||||
{Date: now, CategoryCode: "GAJI", CategoryName: "Gaji", Description: "DW", Amount: 50, Source: "purchase_order"},
|
||||
{Date: now, CategoryCode: "OPS", CategoryName: "Operasional", Description: "atk", Amount: 100, Source: "purchase_order"},
|
||||
},
|
||||
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||
exclusiveResult: &entities.ExclusiveSummaryAnalytics{
|
||||
SalesTotal: 35619000,
|
||||
HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "hpp_nusantara", CategoryName: "Nusantara", Amount: 19010552},
|
||||
},
|
||||
OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "biaya_gaji_staff", CategoryName: "Gaji Staff", Amount: 48203333},
|
||||
{CategoryCode: "biaya_gaji_dw", CategoryName: "Gaji DW", Amount: 3555000},
|
||||
{CategoryCode: "biaya_lain", CategoryName: "Biaya Lain-lain", Amount: 1608605},
|
||||
},
|
||||
DailyTransactions: []entities.ExclusiveSummaryDailyTransaction{
|
||||
{Date: now, CategoryCode: "biaya_gaji_staff", CategoryName: "Gaji Staff", Description: "gaji kary", Amount: 48203333, Source: "purchase_order"},
|
||||
{Date: now, CategoryCode: "biaya_gaji_dw", CategoryName: "Gaji DW", Description: "DW", Amount: 3555000, Source: "purchase_order"},
|
||||
},
|
||||
},
|
||||
}, expenseRepositoryStub{})
|
||||
@ -326,45 +313,38 @@ func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesTotalsAndReimburse
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, float64(1000), result.Summary.Sales)
|
||||
require.Equal(t, float64(400), result.Summary.HPP)
|
||||
require.Equal(t, float64(600), result.Summary.GrossProfit)
|
||||
require.Equal(t, float64(350), result.Summary.OperationalExpensesTotal)
|
||||
require.Equal(t, float64(750), result.Summary.TotalCost)
|
||||
require.Equal(t, float64(250), result.Summary.NetProfit)
|
||||
require.Equal(t, float64(250), result.Summary.SalaryTotal)
|
||||
require.Equal(t, float64(50), result.Summary.SalaryDW)
|
||||
require.Equal(t, float64(200), result.Summary.SalaryStaff)
|
||||
require.Equal(t, float64(100), result.Summary.OtherOperationalExpenses)
|
||||
require.Equal(t, float64(200), result.Reimburse.ExcludedSalaryStaff)
|
||||
require.Equal(t, float64(550), result.Reimburse.TotalReimburse)
|
||||
require.Len(t, result.HPPBreakdown, 1)
|
||||
require.Equal(t, float64(100), result.HPPBreakdown[0].Percentage)
|
||||
require.Len(t, result.DailySummary, 1)
|
||||
require.Len(t, result.DailyTransactions, 4)
|
||||
require.Equal(t, float64(35619000), result.Summary.Sales)
|
||||
require.Equal(t, float64(19010552), result.Summary.HPP)
|
||||
require.Equal(t, float64(16608448), result.Summary.GrossProfit)
|
||||
require.Equal(t, float64(51758333), result.Summary.SalaryTotal)
|
||||
require.Equal(t, float64(3555000), result.Summary.SalaryDW)
|
||||
require.Equal(t, float64(48203333), result.Summary.SalaryStaff)
|
||||
require.Equal(t, float64(53366938), result.Summary.OperationalExpensesTotal)
|
||||
require.Equal(t, float64(72377490), result.Summary.TotalCost)
|
||||
require.Equal(t, float64(-36758490), result.Summary.NetProfit)
|
||||
require.Equal(t, float64(48203333), result.Reimburse.ExcludedSalaryStaff)
|
||||
require.Equal(t, float64(24174157), result.Reimburse.TotalReimburse)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsSummaryAndBuckets(t *testing.T) {
|
||||
func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsCalendarBucketsAndBankTemplate(t *testing.T) {
|
||||
location, err := time.LoadLocation("Asia/Jakarta")
|
||||
require.NoError(t, err)
|
||||
month := time.Date(2026, 5, 1, 0, 0, 0, 0, location)
|
||||
openingBalance := 5000000.0
|
||||
closingBalance := 5000000.0
|
||||
notes := "Main cash account for daily transactions"
|
||||
stub := &analyticsRepositoryStub{
|
||||
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{
|
||||
{SalesTotal: 1000, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 400}}, OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 100}}},
|
||||
{SalesTotal: 100, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 40}}},
|
||||
{SalesTotal: 200, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 80}}},
|
||||
{SalesTotal: 300, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 120}}},
|
||||
{SalesTotal: 400, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 160}}},
|
||||
{SalesTotal: 500, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 200}}},
|
||||
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||
exclusiveResult: &entities.ExclusiveSummaryAnalytics{
|
||||
SalesTotal: 1000,
|
||||
HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "hpp", CategoryName: "HPP", Amount: 400},
|
||||
},
|
||||
OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
||||
{CategoryCode: "ops", CategoryName: "OPS", Amount: 100},
|
||||
},
|
||||
},
|
||||
bankBalances: []entities.ExclusiveSummaryBankBalance{
|
||||
{Bank: "Cash and Bank", OpeningBalance: &openingBalance, ClosingBalance: &closingBalance, Notes: ¬es},
|
||||
{Bank: "Rekening Bank", AccountType: "wallet", OpeningBalance: 1000, ClosingBalance: 2500},
|
||||
{Bank: "Kas Utama", AccountType: "cash", OpeningBalance: 3000, ClosingBalance: 3500},
|
||||
},
|
||||
}
|
||||
processor := NewAnalyticsProcessorImpl(stub, expenseRepositoryStub{})
|
||||
}, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
@ -375,21 +355,19 @@ func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsSummaryAndBuckets(t *
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, "2026-05", result.Month)
|
||||
require.Equal(t, float64(1000), result.Summary.TotalSales)
|
||||
require.Equal(t, float64(400), result.Summary.HPP)
|
||||
require.Equal(t, float64(500), result.Summary.NetProfit)
|
||||
require.InDelta(t, float64(50), result.Summary.NetProfitMargin, 0.0001)
|
||||
require.Len(t, result.Periods, 5)
|
||||
require.Equal(t, "1 - 3 Mei", result.Periods[0].Label)
|
||||
require.Equal(t, "25 - 31 Mei", result.Periods[4].Label)
|
||||
require.Len(t, result.BankBalance, 1)
|
||||
require.Equal(t, "Cash and Bank", result.BankBalance[0].Bank)
|
||||
require.Len(t, result.BankBalance, 2)
|
||||
require.Equal(t, "Rekening Bank", result.BankBalance[0].Bank)
|
||||
require.NotNil(t, result.BankBalance[0].OpeningBalance)
|
||||
require.Equal(t, openingBalance, *result.BankBalance[0].OpeningBalance)
|
||||
require.Equal(t, float64(1000), *result.BankBalance[0].OpeningBalance)
|
||||
require.NotNil(t, result.BankBalance[0].ClosingBalance)
|
||||
require.Equal(t, closingBalance, *result.BankBalance[0].ClosingBalance)
|
||||
require.Equal(t, float64(2500), *result.BankBalance[0].ClosingBalance)
|
||||
require.NotNil(t, result.BankBalance[0].Notes)
|
||||
require.Equal(t, "wallet", *result.BankBalance[0].Notes)
|
||||
require.Nil(t, result.BankBalance[0].IncomingMutation)
|
||||
require.Nil(t, result.BankBalance[0].OutgoingMutation)
|
||||
require.NotNil(t, result.BankBalance[0].Notes)
|
||||
require.Equal(t, notes, *result.BankBalance[0].Notes)
|
||||
require.Equal(t, 6, stub.exclusiveSummaryCalls)
|
||||
require.Equal(t, "Kas Utama", result.BankBalance[1].Bank)
|
||||
}
|
||||
|
||||
@ -760,7 +760,9 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryHPPBreakdown(ctx context.Co
|
||||
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
|
||||
Where("po.status = ?", "received").
|
||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo)
|
||||
query = r.applyPurchaseOrderItemOutletFilter(query, outletID)
|
||||
if outletID != nil {
|
||||
query = query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID)
|
||||
}
|
||||
|
||||
err := query.
|
||||
Group("pc.id, pc.code, pc.name, pc.sort_order").
|
||||
@ -783,7 +785,7 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryOperationalExpenseBreakdown
|
||||
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
|
||||
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
|
||||
Where("po.organization_id = ?", organizationID).
|
||||
Where("pc.type = ?", entities.PurchaseCategoryTypeExpense).
|
||||
Where("pc.type = ?", entities.PurchaseCategoryTypeNonInventory).
|
||||
Where("po.status = ?", "received").
|
||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
||||
Group("pc.id, pc.code, pc.name, pc.sort_order").
|
||||
@ -795,7 +797,7 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryOperationalExpenseBreakdown
|
||||
|
||||
func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailySummary(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryDailySummary, error) {
|
||||
var results []entities.ExclusiveSummaryDailySummary
|
||||
rawQuery, args := r.exclusiveSummaryPurchaseOrderItemQuery(organizationID, outletID, dateFrom, dateTo)
|
||||
rawQuery, args := r.exclusiveSummaryPurchaseOrderItemsQuery(organizationID, outletID, dateFrom, dateTo)
|
||||
|
||||
err := r.db.WithContext(ctx).Raw(`
|
||||
SELECT date, COUNT(*) as transaction_count, COALESCE(SUM(amount), 0) as total_cost
|
||||
@ -809,7 +811,7 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailySummary(ctx context.Co
|
||||
|
||||
func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailyTransactions(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryDailyTransaction, error) {
|
||||
var results []entities.ExclusiveSummaryDailyTransaction
|
||||
rawQuery, args := r.exclusiveSummaryPurchaseOrderItemQuery(organizationID, outletID, dateFrom, dateTo)
|
||||
rawQuery, args := r.exclusiveSummaryPurchaseOrderItemsQuery(organizationID, outletID, dateFrom, dateTo)
|
||||
|
||||
err := r.db.WithContext(ctx).Raw(`
|
||||
SELECT date, category_code, category_name, description, amount, source
|
||||
@ -820,18 +822,19 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailyTransactions(ctx conte
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemQuery(organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (string, []interface{}) {
|
||||
outletFilter := ""
|
||||
func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemsQuery(organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (string, []interface{}) {
|
||||
args := []interface{}{
|
||||
organizationID,
|
||||
entities.PurchaseCategoryTypeRawMaterial,
|
||||
entities.PurchaseCategoryTypeNonInventory,
|
||||
"received",
|
||||
dateFrom,
|
||||
dateTo,
|
||||
}
|
||||
|
||||
outletFilter := ""
|
||||
if outletID != nil {
|
||||
outletFilter = "AND (pc.type = ? OR i.outlet_id = ? OR u.outlet_id = ?)"
|
||||
args = append(args, entities.PurchaseCategoryTypeExpense, *outletID, *outletID)
|
||||
args = append(args, entities.PurchaseCategoryTypeNonInventory, *outletID, *outletID)
|
||||
}
|
||||
|
||||
query := `
|
||||
@ -848,6 +851,7 @@ func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemQuery(organiz
|
||||
LEFT JOIN ingredients i ON poi.ingredient_id = i.id
|
||||
LEFT JOIN units u ON poi.unit_id = u.id
|
||||
WHERE po.organization_id = ?
|
||||
AND pc.type IN (?, ?)
|
||||
AND po.status = ?
|
||||
AND po.transaction_date >= ? AND po.transaction_date <= ?
|
||||
` + outletFilter + `
|
||||
@ -857,45 +861,28 @@ func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemQuery(organiz
|
||||
}
|
||||
|
||||
func (r *AnalyticsRepositoryImpl) GetExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) {
|
||||
type accountBalance struct {
|
||||
Name string
|
||||
OpeningBalance float64
|
||||
CurrentBalance float64
|
||||
Description *string
|
||||
}
|
||||
var results []entities.ExclusiveSummaryBankBalance
|
||||
|
||||
var accounts []accountBalance
|
||||
query := r.db.WithContext(ctx).
|
||||
Table("accounts").
|
||||
Select("name, opening_balance, current_balance, description").
|
||||
Select(`
|
||||
name as bank,
|
||||
account_type,
|
||||
opening_balance,
|
||||
current_balance as closing_balance,
|
||||
description
|
||||
`).
|
||||
Where("organization_id = ?", organizationID).
|
||||
Where("account_type IN ?", []entities.AccountType{entities.AccountTypeCash, entities.AccountTypeWallet, entities.AccountTypeBank}).
|
||||
Where("is_active = ?", true)
|
||||
Where("is_active = ?", true).
|
||||
Where("account_type IN ?", []entities.AccountType{entities.AccountTypeBank, entities.AccountTypeWallet, entities.AccountTypeCash})
|
||||
|
||||
if outletID != nil {
|
||||
query = query.Where("outlet_id = ? OR outlet_id IS NULL", *outletID)
|
||||
} else {
|
||||
query = query.Where("outlet_id IS NULL")
|
||||
}
|
||||
|
||||
err := query.
|
||||
Order("number ASC, name ASC").
|
||||
Scan(&accounts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Order("CASE account_type WHEN 'bank' THEN 1 WHEN 'wallet' THEN 2 WHEN 'cash' THEN 3 ELSE 4 END, number ASC, name ASC").
|
||||
Scan(&results).Error
|
||||
|
||||
balances := make([]entities.ExclusiveSummaryBankBalance, len(accounts))
|
||||
for i, account := range accounts {
|
||||
openingBalance := account.OpeningBalance
|
||||
closingBalance := account.CurrentBalance
|
||||
balances[i] = entities.ExclusiveSummaryBankBalance{
|
||||
Bank: account.Name,
|
||||
OpeningBalance: &openingBalance,
|
||||
ClosingBalance: &closingBalance,
|
||||
Notes: account.Description,
|
||||
}
|
||||
}
|
||||
|
||||
return balances, nil
|
||||
return results, err
|
||||
}
|
||||
|
||||
@ -198,59 +198,3 @@ func TestAnalyticsServiceGetProfitLossAnalyticsAllowsEmptyGroupBy(t *testing.T)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp)
|
||||
}
|
||||
|
||||
func TestAnalyticsServiceGetExclusiveSummaryPeriodValidation(t *testing.T) {
|
||||
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *models.ExclusiveSummaryPeriodRequest
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
req: nil,
|
||||
wantErr: "request cannot be nil",
|
||||
},
|
||||
{
|
||||
name: "missing organization",
|
||||
req: &models.ExclusiveSummaryPeriodRequest{
|
||||
DateFrom: now,
|
||||
DateTo: now,
|
||||
},
|
||||
wantErr: "organization_id is required",
|
||||
},
|
||||
{
|
||||
name: "reversed dates",
|
||||
req: &models.ExclusiveSummaryPeriodRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
DateFrom: now.AddDate(0, 0, 1),
|
||||
DateTo: now,
|
||||
},
|
||||
wantErr: "date_from cannot be after date_to",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp, err := service.GetExclusiveSummaryPeriod(context.Background(), tt.req)
|
||||
|
||||
require.Nil(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyticsServiceGetExclusiveSummaryMonthlyValidation(t *testing.T) {
|
||||
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
|
||||
|
||||
resp, err := service.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
})
|
||||
|
||||
require.Nil(t, resp)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "month is required")
|
||||
}
|
||||
|
||||
@ -753,18 +753,18 @@ func parseISODateToJakartaTime(dateStr string, endOfDay bool) (*time.Time, error
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
date, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
location, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
date, err := time.ParseInLocation("2006-01-02", dateStr, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if endOfDay {
|
||||
result := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 999999999, location)
|
||||
result := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), location)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
|
||||
@ -183,7 +183,7 @@ func TestProfitLossAnalyticsModelToContractCopiesDateRange(t *testing.T) {
|
||||
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||
}
|
||||
|
||||
func TestExclusiveSummaryPeriodContractToModelParsesFlexibleDates(t *testing.T) {
|
||||
func TestExclusiveSummaryPeriodContractToModelParsesISODateRange(t *testing.T) {
|
||||
orgID := uuid.New()
|
||||
outletID := uuid.New().String()
|
||||
|
||||
@ -209,16 +209,50 @@ func TestExclusiveSummaryPeriodContractToModelParsesFlexibleDates(t *testing.T)
|
||||
|
||||
func TestExclusiveSummaryMonthlyContractToModelParsesMonth(t *testing.T) {
|
||||
orgID := uuid.New()
|
||||
outletID := uuid.New().String()
|
||||
|
||||
result, err := ExclusiveSummaryMonthlyContractToModel(&contract.ExclusiveSummaryMonthlyRequest{
|
||||
OrganizationID: orgID,
|
||||
OutletID: &outletID,
|
||||
Month: "2026-05",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, orgID, result.OrganizationID)
|
||||
require.NotNil(t, result.OutletID)
|
||||
require.Equal(t, outletID, result.OutletID.String())
|
||||
|
||||
location, err := time.LoadLocation("Asia/Jakarta")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Date(2026, 5, 1, 0, 0, 0, 0, location), result.Month)
|
||||
}
|
||||
|
||||
func TestExclusiveSummaryPeriodModelToContractCopiesBreakdowns(t *testing.T) {
|
||||
dateFrom := time.Date(2026, 5, 26, 0, 0, 0, 0, time.UTC)
|
||||
dateTo := time.Date(2026, 5, 31, 23, 59, 59, int(time.Second-time.Nanosecond), time.UTC)
|
||||
|
||||
result := ExclusiveSummaryPeriodModelToContract(&models.ExclusiveSummaryPeriodResponse{
|
||||
OrganizationID: uuid.New(),
|
||||
Period: models.ExclusiveSummaryPeriodRange{
|
||||
DateFrom: dateFrom,
|
||||
DateTo: dateTo,
|
||||
},
|
||||
Summary: models.ExclusiveSummaryPeriodSummary{
|
||||
Sales: 1000,
|
||||
HPP: 400,
|
||||
TotalCost: 550,
|
||||
NetProfit: 450,
|
||||
SalaryStaff: 100,
|
||||
},
|
||||
HPPBreakdown: []models.ExclusiveSummaryCategoryBreakdown{
|
||||
{CategoryCode: "hpp", CategoryName: "HPP", Amount: 400, Percentage: 100},
|
||||
},
|
||||
})
|
||||
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, dateFrom, result.Period.DateFrom)
|
||||
require.Equal(t, dateTo, result.Period.DateTo)
|
||||
require.Equal(t, float64(1000), result.Summary.Sales)
|
||||
require.Len(t, result.HPPBreakdown, 1)
|
||||
require.Equal(t, "hpp", result.HPPBreakdown[0].CategoryCode)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user