Compare commits

..

No commits in common. "b90a3cde4a86a83efd8fecf1f74394f462c47b73" and "47fa21d7391f83372a17db9495ae098a743eec65" have entirely different histories.

6 changed files with 39 additions and 173 deletions

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"time" "time"
"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"
) )
@ -452,46 +453,15 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
} }
} }
type categoryAmount struct { todayPromosi := getExpenseAmountByCategory(result.TodayExpenseByCategory, "promosi")
Name string todayLainLain := getExpenseAmountByCategory(result.TodayExpenseByCategory, "lain")
TodayAmt float64 todayTotalOps := todayPromosi + todayLainLain
MtdAmt float64 todayGaji := getExpenseAmountByCategory(result.TodayExpenseByCategory, "gaji")
}
categoryMap := make(map[string]*categoryAmount) mtdPromosi := getExpenseAmountByCategory(result.MtdExpenseByCategory, "promosi")
var categoryOrder []string mtdLainLain := getExpenseAmountByCategory(result.MtdExpenseByCategory, "lain")
mtdTotalOps := mtdPromosi + mtdLainLain
for _, cat := range result.TodayExpenseByCategory { mtdGaji := getExpenseAmountByCategory(result.MtdExpenseByCategory, "gaji")
name := cat.CategoryName
if _, exists := categoryMap[name]; !exists {
categoryMap[name] = &categoryAmount{Name: name}
categoryOrder = append(categoryOrder, name)
}
categoryMap[name].TodayAmt = cat.Amount
}
for _, cat := range result.MtdExpenseByCategory {
name := cat.CategoryName
if _, exists := categoryMap[name]; !exists {
categoryMap[name] = &categoryAmount{Name: name}
categoryOrder = append(categoryOrder, name)
}
categoryMap[name].MtdAmt = cat.Amount
}
var todayTotalOps float64
var mtdTotalOps float64
var todayGaji float64
var mtdGaji float64
for _, cat := range categoryMap {
if isSalaryExpenseCategory(cat.Name) {
todayGaji += cat.TodayAmt
mtdGaji += cat.MtdAmt
continue
}
todayTotalOps += cat.TodayAmt
mtdTotalOps += cat.MtdAmt
}
todayGrossProfit := result.TodayRevenue - result.TodayCost todayGrossProfit := result.TodayRevenue - result.TodayCost
mtdGrossProfit := result.MtdRevenue - result.MtdCost mtdGrossProfit := result.MtdRevenue - result.MtdCost
@ -515,33 +485,6 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
return (nominal / result.MtdRevenue) * 100 return (nominal / result.MtdRevenue) * 100
} }
opsSubItems := make([]models.ProfitLossSummaryRow, 0, len(categoryOrder)+1)
opsCategoryCount := 0
for _, name := range categoryOrder {
cat := categoryMap[name]
if isSalaryExpenseCategory(cat.Name) {
continue
}
opsCategoryCount++
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
ID: fmt.Sprintf("by_%s", slugify(name)),
Label: fmt.Sprintf("%d. %s", opsCategoryCount, cat.Name),
TodayNominal: cat.TodayAmt,
TodayPct: todayPct(cat.TodayAmt),
MtdNominal: cat.MtdAmt,
MtdPct: mtdPct(cat.MtdAmt),
})
}
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
ID: "total_biaya_ops",
Label: fmt.Sprintf("Total Biaya OPS (%d kategori)", opsCategoryCount),
IsBold: true,
TodayNominal: todayTotalOps,
TodayPct: todayPct(todayTotalOps),
MtdNominal: mtdTotalOps,
MtdPct: mtdPct(mtdTotalOps),
})
mainSummary := []models.ProfitLossSummaryRow{ mainSummary := []models.ProfitLossSummaryRow{
{ {
ID: "total_omset", Label: "TOTAL OMSET", ID: "total_omset", Label: "TOTAL OMSET",
@ -562,7 +505,23 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
ID: "biaya_ops", Label: "BIAYA OPS", ID: "biaya_ops", Label: "BIAYA OPS",
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps), TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps), MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
SubItems: opsSubItems, SubItems: []models.ProfitLossSummaryRow{
{
ID: "by_promosi", Label: "1. By Promosi",
TodayNominal: todayPromosi, TodayPct: todayPct(todayPromosi),
MtdNominal: mtdPromosi, MtdPct: mtdPct(mtdPromosi),
},
{
ID: "by_lain_lain", Label: "2. By Lain lain",
TodayNominal: todayLainLain, TodayPct: todayPct(todayLainLain),
MtdNominal: mtdLainLain, MtdPct: mtdPct(mtdLainLain),
},
{
ID: "total_biaya_ops", Label: "Total Biaya OPS (4.1+4.2)", IsBold: true,
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
},
},
}, },
{ {
ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)", ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)",
@ -618,27 +577,11 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
}, nil }, nil
} }
func isSalaryExpenseCategory(name string) bool { func getExpenseAmountByCategory(categories []entities.ExpenseCategoryTotal, keyword string) float64 {
name = strings.ToLower(name) for _, cat := range categories {
return strings.Contains(name, "gaji") || strings.Contains(name, "salary") if strings.Contains(strings.ToLower(cat.CategoryName), keyword) {
} return cat.Amount
func slugify(s string) string {
result := make([]byte, 0, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c >= 'a' && c <= 'z':
result = append(result, c)
case c >= 'A' && c <= 'Z':
result = append(result, c+32)
case c >= '0' && c <= '9':
result = append(result, c)
default:
if len(result) == 0 || result[len(result)-1] != '_' {
result = append(result, '_')
} }
} }
} return 0
return string(result)
} }

View File

@ -165,83 +165,3 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *
require.NotEmpty(t, result.MainSummary) require.NotEmpty(t, result.MainSummary)
require.Equal(t, "total_omset", result.MainSummary[0].ID) require.Equal(t, "total_omset", result.MainSummary[0].ID)
} }
func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *testing.T) {
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
profitLossResult: &entities.ProfitLossAnalytics{
Summary: entities.ProfitLossSummary{
TotalRevenue: 10000,
TotalCost: 4000,
},
TodayRevenue: 10000,
TodayCost: 4000,
MtdRevenue: 20000,
MtdCost: 8000,
TodayExpenseByCategory: []entities.ExpenseCategoryTotal{
{CategoryName: "Gaji", Amount: 1500},
{CategoryName: "Promosi", Amount: 300},
{CategoryName: "Sewa", Amount: 500},
},
MtdExpenseByCategory: []entities.ExpenseCategoryTotal{
{CategoryName: "Gaji", Amount: 3000},
{CategoryName: "Promosi", Amount: 600},
{CategoryName: "Sewa", Amount: 1000},
},
},
}, expenseRepositoryStub{})
result, err := processor.GetProfitLossAnalytics(context.Background(), &models.ProfitLossAnalyticsRequest{
OrganizationID: uuid.New(),
DateFrom: now,
DateTo: now,
})
require.NoError(t, err)
require.NotNil(t, result)
require.Len(t, result.MainSummary, 7)
require.Equal(t, "total_omset", result.MainSummary[0].ID)
require.Equal(t, float64(10000), result.MainSummary[0].TodayNominal)
require.Equal(t, float64(20000), result.MainSummary[0].MtdNominal)
require.Equal(t, "hpp", result.MainSummary[1].ID)
require.Equal(t, float64(4000), result.MainSummary[1].TodayNominal)
require.Equal(t, float64(8000), result.MainSummary[1].MtdNominal)
require.Equal(t, "laba_kotor", result.MainSummary[2].ID)
require.Equal(t, float64(6000), result.MainSummary[2].TodayNominal)
require.Equal(t, float64(12000), result.MainSummary[2].MtdNominal)
require.Equal(t, "biaya_ops", result.MainSummary[3].ID)
require.Equal(t, float64(800), result.MainSummary[3].TodayNominal)
require.Equal(t, float64(1600), result.MainSummary[3].MtdNominal)
require.Len(t, result.MainSummary[3].SubItems, 3) // 2 operational categories + 1 total
require.Equal(t, "by_promosi", result.MainSummary[3].SubItems[0].ID)
require.Equal(t, float64(300), result.MainSummary[3].SubItems[0].TodayNominal)
require.Equal(t, float64(600), result.MainSummary[3].SubItems[0].MtdNominal)
require.Equal(t, "by_sewa", result.MainSummary[3].SubItems[1].ID)
require.Equal(t, float64(500), result.MainSummary[3].SubItems[1].TodayNominal)
require.Equal(t, float64(1000), result.MainSummary[3].SubItems[1].MtdNominal)
require.Equal(t, "total_biaya_ops", result.MainSummary[3].SubItems[2].ID)
require.True(t, result.MainSummary[3].SubItems[2].IsBold)
require.Equal(t, float64(800), result.MainSummary[3].SubItems[2].TodayNominal)
require.Equal(t, float64(1600), result.MainSummary[3].SubItems[2].MtdNominal)
require.Equal(t, "laba_rugi_sblm_gaji", result.MainSummary[4].ID)
require.Equal(t, float64(5200), result.MainSummary[4].TodayNominal)
require.Equal(t, float64(10400), result.MainSummary[4].MtdNominal)
require.Equal(t, "biaya_gaji", result.MainSummary[5].ID)
require.Equal(t, float64(1500), result.MainSummary[5].TodayNominal)
require.Equal(t, float64(3000), result.MainSummary[5].MtdNominal)
require.Equal(t, "laba_rugi", result.MainSummary[6].ID)
require.Equal(t, float64(3700), result.MainSummary[6].TodayNominal)
require.Equal(t, float64(7400), result.MainSummary[6].MtdNominal)
require.True(t, result.MainSummary[6].IsBold)
}

View File

@ -22,6 +22,8 @@ const (
MetadataKeyLastSplitQuantities = "last_split_quantities" MetadataKeyLastSplitQuantities = "last_split_quantities"
) )
type SplitBillValidation struct { type SplitBillValidation struct {
OrderItems map[uuid.UUID]*entities.OrderItem OrderItems map[uuid.UUID]*entities.OrderItem
PaidQuantities map[uuid.UUID]int PaidQuantities map[uuid.UUID]int

View File

@ -637,7 +637,7 @@ func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, orga
query := r.db.WithContext(ctx). query := r.db.WithContext(ctx).
Table("expense_items ei"). Table("expense_items ei").
Select(`COALESCE(parent_coa.name, coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount`). Select(`COALESCE(parent_coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount`).
Joins("JOIN expenses e ON ei.expense_id = e.id"). Joins("JOIN expenses e ON ei.expense_id = e.id").
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id"). Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id"). Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
@ -650,8 +650,8 @@ func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, orga
} }
err := query. err := query.
Group("COALESCE(parent_coa.name, coa.name, 'Lain-lain')"). Group("parent_coa.name").
Order("COALESCE(parent_coa.name, coa.name, 'Lain-lain')"). Order("parent_coa.name").
Scan(&results).Error Scan(&results).Error
return results, err return results, err

View File

@ -173,3 +173,4 @@ func (r *IngredientUnitConverterRepositoryImpl) ConvertQuantity(ctx context.Cont
// If no converter found, return error // If no converter found, return error
return 0, fmt.Errorf("no conversion found between units %s and %s for ingredient %s", fromUnitID, toUnitID, ingredientID) return 0, fmt.Errorf("no conversion found between units %s and %s for ingredient %s", fromUnitID, toUnitID, ingredientID)
} }