Compare commits

..

3 Commits

Author SHA1 Message Date
8816e4addc Merge branch 'feature/expense-revisi' into feature/exclusive-summary 2026-06-17 18:36:46 +07:00
0db838e2c4 Fix bank 2026-06-17 18:26:40 +07:00
4b6cbb69c1 Add exclusive-summary 2026-06-17 18:17:08 +07:00
7 changed files with 205 additions and 166 deletions

View File

@ -218,9 +218,10 @@ type ExclusiveSummaryDailyTransaction struct {
}
type ExclusiveSummaryBankBalance struct {
Bank string
AccountType string
OpeningBalance float64
ClosingBalance float64
Description *string
Bank string
OpeningBalance *float64
IncomingMutation *float64
OutgoingMutation *float64
ClosingBalance *float64
Notes *string
}

View File

@ -9,8 +9,6 @@ import (
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
"github.com/google/uuid"
)
type AnalyticsProcessor interface {
@ -679,14 +677,8 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
return nil, err
}
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 {
periods := make([]models.ExclusiveSummaryMonthlyPeriod, 0)
for _, bucket := range buildExclusiveSummaryMonthlyBuckets(monthStart) {
period, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
OrganizationID: req.OrganizationID,
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{
OrganizationID: req.OrganizationID,
OutletID: req.OutletID,
@ -726,34 +735,6 @@ 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 = &notes
}
}
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 {

View File

@ -13,11 +13,12 @@ import (
)
type analyticsRepositoryStub struct {
purchasingResult *entities.PurchasingAnalytics
profitLossResult *entities.ProfitLossAnalytics
exclusiveResult *entities.ExclusiveSummaryAnalytics
bankBalances []entities.ExclusiveSummaryBankBalance
profitLossGroup string
purchasingResult *entities.PurchasingAnalytics
profitLossResult *entities.ProfitLossAnalytics
exclusiveSummaryResults []*entities.ExclusiveSummaryAnalytics
bankBalances []entities.ExclusiveSummaryBankBalance
profitLossGroup string
exclusiveSummaryCalls int
}
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
}
func (s analyticsRepositoryStub) GetExclusiveSummaryAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) (*entities.ExclusiveSummaryAnalytics, error) {
return s.exclusiveResult, 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) 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
}
@ -81,7 +88,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{
@ -134,7 +141,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,
@ -206,7 +213,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,
@ -284,22 +291,28 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *tes
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)
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"},
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"},
},
},
},
}, expenseRepositoryStub{})
@ -313,38 +326,45 @@ func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesSummaryAndReimburs
require.NoError(t, err)
require.NotNil(t, result)
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)
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)
}
func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsCalendarBucketsAndBankTemplate(t *testing.T) {
func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsSummaryAndBuckets(t *testing.T) {
location, err := time.LoadLocation("Asia/Jakarta")
require.NoError(t, err)
month := time.Date(2026, 5, 1, 0, 0, 0, 0, location)
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},
},
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}}},
},
bankBalances: []entities.ExclusiveSummaryBankBalance{
{Bank: "Rekening Bank", AccountType: "wallet", OpeningBalance: 1000, ClosingBalance: 2500},
{Bank: "Kas Utama", AccountType: "cash", OpeningBalance: 3000, ClosingBalance: 3500},
{Bank: "Cash and Bank", OpeningBalance: &openingBalance, ClosingBalance: &closingBalance, Notes: &notes},
},
}, expenseRepositoryStub{})
}
processor := NewAnalyticsProcessorImpl(stub, expenseRepositoryStub{})
result, err := processor.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{
OrganizationID: uuid.New(),
@ -355,19 +375,21 @@ func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsCalendarBucketsAndBan
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, 2)
require.Equal(t, "Rekening Bank", result.BankBalance[0].Bank)
require.Len(t, result.BankBalance, 1)
require.Equal(t, "Cash and Bank", result.BankBalance[0].Bank)
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.Equal(t, float64(2500), *result.BankBalance[0].ClosingBalance)
require.NotNil(t, result.BankBalance[0].Notes)
require.Equal(t, "wallet", *result.BankBalance[0].Notes)
require.Equal(t, closingBalance, *result.BankBalance[0].ClosingBalance)
require.Nil(t, result.BankBalance[0].IncomingMutation)
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)
}

View File

@ -760,9 +760,7 @@ 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)
if outletID != nil {
query = query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID)
}
query = r.applyPurchaseOrderItemOutletFilter(query, outletID)
err := query.
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_categories pc ON poi.purchase_category_id = pc.id").
Where("po.organization_id = ?", organizationID).
Where("pc.type = ?", entities.PurchaseCategoryTypeNonInventory).
Where("pc.type = ?", entities.PurchaseCategoryTypeExpense).
Where("po.status = ?", "received").
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
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) {
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(`
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) {
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(`
SELECT date, category_code, category_name, description, amount, source
@ -822,19 +820,18 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailyTransactions(ctx conte
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{}{
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.PurchaseCategoryTypeNonInventory, *outletID, *outletID)
args = append(args, entities.PurchaseCategoryTypeExpense, *outletID, *outletID)
}
query := `
@ -851,7 +848,6 @@ func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemsQuery(organi
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 + `
@ -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) {
var results []entities.ExclusiveSummaryBankBalance
type accountBalance struct {
Name string
OpeningBalance float64
CurrentBalance float64
Description *string
}
var accounts []accountBalance
query := r.db.WithContext(ctx).
Table("accounts").
Select(`
name as bank,
account_type,
opening_balance,
current_balance as closing_balance,
description
`).
Select("name, opening_balance, current_balance, description").
Where("organization_id = ?", organizationID).
Where("is_active = ?", true).
Where("account_type IN ?", []entities.AccountType{entities.AccountTypeBank, entities.AccountTypeWallet, entities.AccountTypeCash})
Where("account_type IN ?", []entities.AccountType{entities.AccountTypeCash, entities.AccountTypeWallet, entities.AccountTypeBank}).
Where("is_active = ?", true)
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("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
Order("number ASC, name ASC").
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
}

View File

@ -198,3 +198,59 @@ 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")
}

View File

@ -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, int(time.Second-time.Nanosecond), location)
result := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 999999999, location)
return &result, nil
}

View File

@ -183,7 +183,7 @@ func TestProfitLossAnalyticsModelToContractCopiesDateRange(t *testing.T) {
require.Equal(t, "total_omset", result.MainSummary[0].ID)
}
func TestExclusiveSummaryPeriodContractToModelParsesISODateRange(t *testing.T) {
func TestExclusiveSummaryPeriodContractToModelParsesFlexibleDates(t *testing.T) {
orgID := uuid.New()
outletID := uuid.New().String()
@ -209,50 +209,16 @@ func TestExclusiveSummaryPeriodContractToModelParsesISODateRange(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)
}