Add exclusive-summary
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
)
|
||||
@@ -18,6 +19,8 @@ type AnalyticsProcessor interface {
|
||||
GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error)
|
||||
GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error)
|
||||
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
|
||||
GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error)
|
||||
GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error)
|
||||
}
|
||||
|
||||
type AnalyticsProcessorImpl struct {
|
||||
@@ -651,3 +654,252 @@ func slugify(s string) string {
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
return p.buildExclusiveSummaryPeriod(ctx, req)
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error) {
|
||||
monthStart := time.Date(req.Month.Year(), req.Month.Month(), 1, 0, 0, 0, 0, req.Month.Location())
|
||||
monthEnd := monthStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
||||
|
||||
fullPeriod, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: monthStart,
|
||||
DateTo: monthEnd,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
periods := make([]models.ExclusiveSummaryMonthlyPeriod, 0)
|
||||
for _, bucket := range buildExclusiveSummaryMonthlyBuckets(monthStart) {
|
||||
period, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: bucket.DateFrom,
|
||||
DateTo: bucket.DateTo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
periods = append(periods, models.ExclusiveSummaryMonthlyPeriod{
|
||||
Label: bucket.Label,
|
||||
DateFrom: bucket.DateFrom,
|
||||
DateTo: bucket.DateTo,
|
||||
Sales: period.Summary.Sales,
|
||||
HPP: period.Summary.HPP,
|
||||
GrossProfit: period.Summary.GrossProfit,
|
||||
GrossMargin: percentage(period.Summary.GrossProfit, period.Summary.Sales),
|
||||
})
|
||||
}
|
||||
|
||||
return &models.ExclusiveSummaryMonthlyResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
Month: monthStart.Format("2006-01"),
|
||||
Summary: models.ExclusiveSummaryMonthlySummary{
|
||||
TotalSales: fullPeriod.Summary.Sales,
|
||||
HPP: fullPeriod.Summary.HPP,
|
||||
GrossProfit: fullPeriod.Summary.GrossProfit,
|
||||
OperationalExpensesTotal: fullPeriod.Summary.OperationalExpensesTotal,
|
||||
TotalCost: fullPeriod.Summary.TotalCost,
|
||||
NetProfit: fullPeriod.Summary.NetProfit,
|
||||
NetProfitMargin: percentage(fullPeriod.Summary.NetProfit, fullPeriod.Summary.Sales),
|
||||
},
|
||||
Periods: periods,
|
||||
BankBalance: []models.ExclusiveSummaryBankBalance{
|
||||
{Bank: "BCA"},
|
||||
{Bank: "BRI"},
|
||||
},
|
||||
}, 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 {
|
||||
return nil, fmt.Errorf("failed to get exclusive summary analytics: %w", err)
|
||||
}
|
||||
|
||||
hppBreakdown, hppTotal := exclusiveSummaryCategoryBreakdown(result.HPPBreakdown)
|
||||
operationalBreakdown, operationalTotal := exclusiveSummaryCategoryBreakdown(result.OperationalExpenseBreakdown)
|
||||
salaryDW, salaryStaff, salaryOther := exclusiveSummarySalaryBreakdown(result.DailyTransactions)
|
||||
salaryTotal := salaryDW + salaryStaff + salaryOther
|
||||
otherOperationalExpenses := operationalTotal - salaryTotal
|
||||
if otherOperationalExpenses < 0 {
|
||||
otherOperationalExpenses = 0
|
||||
}
|
||||
|
||||
grossProfit := result.SalesTotal - hppTotal
|
||||
totalCost := hppTotal + operationalTotal
|
||||
netProfit := result.SalesTotal - totalCost
|
||||
excludedSalaryStaff := 0.0
|
||||
if req.ExcludeGajiStaffFromReimburse {
|
||||
excludedSalaryStaff = salaryStaff
|
||||
}
|
||||
|
||||
dailySummary := make([]models.ExclusiveSummaryDailySummary, len(result.DailySummary))
|
||||
for i, item := range result.DailySummary {
|
||||
dailySummary[i] = models.ExclusiveSummaryDailySummary{
|
||||
Date: item.Date,
|
||||
TransactionCount: item.TransactionCount,
|
||||
TotalCost: item.TotalCost,
|
||||
}
|
||||
}
|
||||
|
||||
dailyTransactions := make([]models.ExclusiveSummaryDailyTransaction, len(result.DailyTransactions))
|
||||
for i, item := range result.DailyTransactions {
|
||||
dailyTransactions[i] = models.ExclusiveSummaryDailyTransaction{
|
||||
Date: item.Date,
|
||||
CategoryCode: item.CategoryCode,
|
||||
CategoryName: item.CategoryName,
|
||||
Description: item.Description,
|
||||
Amount: item.Amount,
|
||||
Source: item.Source,
|
||||
}
|
||||
}
|
||||
|
||||
return &models.ExclusiveSummaryPeriodResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
Period: models.ExclusiveSummaryPeriodRange{
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
},
|
||||
Summary: models.ExclusiveSummaryPeriodSummary{
|
||||
Sales: result.SalesTotal,
|
||||
HPP: hppTotal,
|
||||
GrossProfit: grossProfit,
|
||||
SalaryTotal: salaryTotal,
|
||||
SalaryDW: salaryDW,
|
||||
SalaryStaff: salaryStaff,
|
||||
SalaryOther: salaryOther,
|
||||
OtherOperationalExpenses: otherOperationalExpenses,
|
||||
OperationalExpensesTotal: operationalTotal,
|
||||
TotalCost: totalCost,
|
||||
NetProfit: netProfit,
|
||||
},
|
||||
Reimburse: models.ExclusiveSummaryReimburse{
|
||||
TotalCost: totalCost,
|
||||
ExcludedSalaryStaff: excludedSalaryStaff,
|
||||
TotalReimburse: totalCost - excludedSalaryStaff,
|
||||
},
|
||||
HPPBreakdown: hppBreakdown,
|
||||
OperationalExpenseBreakdown: operationalBreakdown,
|
||||
DailySummary: dailySummary,
|
||||
DailyTransactions: dailyTransactions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func exclusiveSummaryCategoryBreakdown(items []entities.ExclusiveSummaryCategoryTotal) ([]models.ExclusiveSummaryCategoryBreakdown, float64) {
|
||||
var total float64
|
||||
for _, item := range items {
|
||||
total += item.Amount
|
||||
}
|
||||
|
||||
breakdown := make([]models.ExclusiveSummaryCategoryBreakdown, len(items))
|
||||
for i, item := range items {
|
||||
breakdown[i] = models.ExclusiveSummaryCategoryBreakdown{
|
||||
CategoryCode: item.CategoryCode,
|
||||
CategoryName: item.CategoryName,
|
||||
Amount: item.Amount,
|
||||
Percentage: percentage(item.Amount, total),
|
||||
}
|
||||
}
|
||||
|
||||
return breakdown, total
|
||||
}
|
||||
|
||||
func exclusiveSummarySalaryBreakdown(transactions []entities.ExclusiveSummaryDailyTransaction) (float64, float64, float64) {
|
||||
var salaryDW float64
|
||||
var salaryStaff float64
|
||||
var salaryOther float64
|
||||
|
||||
for _, transaction := range transactions {
|
||||
if !isExclusiveSummarySalary(transaction.CategoryCode, transaction.CategoryName, transaction.Description) {
|
||||
continue
|
||||
}
|
||||
|
||||
classification := strings.ToLower(transaction.CategoryCode + " " + transaction.CategoryName + " " + transaction.Description)
|
||||
switch {
|
||||
case strings.Contains(classification, "staff") || strings.Contains(classification, "kary") || strings.Contains(classification, "karyawan"):
|
||||
salaryStaff += transaction.Amount
|
||||
case strings.Contains(classification, "dw"):
|
||||
salaryDW += transaction.Amount
|
||||
default:
|
||||
salaryOther += transaction.Amount
|
||||
}
|
||||
}
|
||||
|
||||
return salaryDW, salaryStaff, salaryOther
|
||||
}
|
||||
|
||||
func isExclusiveSummarySalary(parts ...string) bool {
|
||||
text := strings.ToLower(strings.Join(parts, " "))
|
||||
return strings.Contains(text, "gaji") || strings.Contains(text, "salary")
|
||||
}
|
||||
|
||||
func percentage(numerator, denominator float64) float64 {
|
||||
if denominator == 0 {
|
||||
return 0
|
||||
}
|
||||
return (numerator / denominator) * 100
|
||||
}
|
||||
|
||||
type exclusiveSummaryMonthlyBucket struct {
|
||||
Label string
|
||||
DateFrom time.Time
|
||||
DateTo time.Time
|
||||
}
|
||||
|
||||
func buildExclusiveSummaryMonthlyBuckets(monthStart time.Time) []exclusiveSummaryMonthlyBucket {
|
||||
monthEnd := monthStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
||||
buckets := make([]exclusiveSummaryMonthlyBucket, 0, 6)
|
||||
currentStart := monthStart
|
||||
|
||||
for !currentStart.After(monthEnd) {
|
||||
currentEnd := currentStart
|
||||
for currentEnd.Weekday() != time.Sunday && currentEnd.Day() < monthEnd.Day() {
|
||||
currentEnd = currentEnd.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
bucketEnd := time.Date(currentEnd.Year(), currentEnd.Month(), currentEnd.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), currentEnd.Location())
|
||||
if bucketEnd.After(monthEnd) {
|
||||
bucketEnd = monthEnd
|
||||
}
|
||||
|
||||
buckets = append(buckets, exclusiveSummaryMonthlyBucket{
|
||||
Label: fmt.Sprintf("%d - %d %s", currentStart.Day(), bucketEnd.Day(), indonesianMonthName(currentStart.Month())),
|
||||
DateFrom: currentStart,
|
||||
DateTo: bucketEnd,
|
||||
})
|
||||
|
||||
currentStart = time.Date(bucketEnd.Year(), bucketEnd.Month(), bucketEnd.Day(), 0, 0, 0, 0, bucketEnd.Location()).AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
return buckets
|
||||
}
|
||||
|
||||
func indonesianMonthName(month time.Month) string {
|
||||
names := map[time.Month]string{
|
||||
time.January: "Januari",
|
||||
time.February: "Februari",
|
||||
time.March: "Maret",
|
||||
time.April: "April",
|
||||
time.May: "Mei",
|
||||
time.June: "Juni",
|
||||
time.July: "Juli",
|
||||
time.August: "Agustus",
|
||||
time.September: "September",
|
||||
time.October: "Oktober",
|
||||
time.November: "November",
|
||||
time.December: "Desember",
|
||||
}
|
||||
return names[month]
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
)
|
||||
|
||||
type analyticsRepositoryStub struct {
|
||||
purchasingResult *entities.PurchasingAnalytics
|
||||
profitLossResult *entities.ProfitLossAnalytics
|
||||
profitLossGroup string
|
||||
purchasingResult *entities.PurchasingAnalytics
|
||||
profitLossResult *entities.ProfitLossAnalytics
|
||||
exclusiveSummaryResults []*entities.ExclusiveSummaryAnalytics
|
||||
profitLossGroup string
|
||||
exclusiveSummaryCalls int
|
||||
}
|
||||
|
||||
func (analyticsRepositoryStub) GetPaymentMethodAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) ([]*entities.PaymentMethodAnalytics, error) {
|
||||
@@ -47,6 +49,16 @@ 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
|
||||
}
|
||||
|
||||
type expenseRepositoryStub struct{}
|
||||
|
||||
func (expenseRepositoryStub) Create(context.Context, *entities.Expense) error { return nil }
|
||||
@@ -71,7 +83,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{
|
||||
@@ -124,7 +136,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,
|
||||
@@ -196,7 +208,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,
|
||||
@@ -273,3 +285,92 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *tes
|
||||
require.Equal(t, float64(7400), result.MainSummary[6].MtdNominal)
|
||||
require.True(t, result.MainSummary[6].IsBold)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesTotalsAndReimburse(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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetExclusiveSummaryPeriod(context.Background(), &models.ExclusiveSummaryPeriodRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
DateFrom: now,
|
||||
DateTo: now,
|
||||
ExcludeGajiStaffFromReimburse: true,
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
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(stub, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
Month: month,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
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, "BCA", result.BankBalance[0].Bank)
|
||||
require.Equal(t, 6, stub.exclusiveSummaryCalls)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user