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