fix: scope page insights to reporting periods

This commit is contained in:
2026-08-22 17:24:37 +08:00
parent 1b6ad4d095
commit 7f9c16be4b
13 changed files with 295 additions and 24 deletions

View File

@@ -9,6 +9,7 @@ import (
"sort"
"time"
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/analytics"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -83,7 +84,7 @@ func (b *PostgreSQLBuilder) build(ctx context.Context, merchantID int64, termina
timeContext := BuildTimeContext(b.now(), dataThrough)
var latest, previous aggregate
var err error
if period != nil && module == ModuleTransactions {
if period != nil {
latest, err = b.aggregateRange(ctx, merchantID, terminalID, period.StartAt, period.EndAt)
if err == nil {
duration := period.EndAt.Sub(period.StartAt)
@@ -108,16 +109,29 @@ func (b *PostgreSQLBuilder) build(ctx context.Context, merchantID int64, termina
}
evidenceMap := map[string]any{
"schemaVersion": "1.0.0", "module": module, "comparisonAvailable": comparisonAvailable,
"latest30Days": latest, "previousYearEquivalent": previous,
"time": timeContext, "privacy": map[string]any{"aggregateOnly": true, "peerSuppressionApplied": module == ModulePeers},
}
if period != nil && module == ModuleTransactions {
precedingPeriodLabel := ""
if period != nil {
precedingPeriodLabel, _ = analytics.PrecedingPeriodLabel(period.PeriodType, period.StartAt)
evidenceMap["selectedPeriod"] = map[string]any{"periodId": period.PeriodID, "periodType": period.PeriodType, "startAt": period.StartAt, "endAt": period.EndAt, "jalaliLabelFa": period.JalaliLabelFA}
evidenceMap["selectedPeriodAggregate"] = latest
if comparisonAvailable {
evidenceMap["precedingPeriod"] = map[string]any{"jalaliLabelFa": precedingPeriodLabel}
evidenceMap["precedingPeriodAggregate"] = previous
}
} else {
evidenceMap["latest30Days"] = latest
evidenceMap["previousYearEquivalent"] = previous
}
if period != nil && module == ModuleTransactions {
evidenceMap["statusDistribution"] = map[string]any{
"total": latest.Sessions, "paidAwaitingVerification": latest.Unverified, "verified": latest.Verified, "failed": latest.Failed, "reversed": latest.Reversed,
"bankSuccessRate": ratio(latest.Paid, latest.Sessions), "verificationCompletionRate": ratio(latest.Verified, latest.Paid), "failureRate": ratio(latest.Failed, latest.Sessions),
}
evidenceMap["precedingPeriodStatus"] = previous
if comparisonAvailable {
evidenceMap["precedingPeriodStatus"] = previous
}
}
business, err := b.businessContext(ctx, merchantID, terminalID)
if err != nil {
@@ -138,8 +152,8 @@ func (b *PostgreSQLBuilder) build(ctx context.Context, merchantID int64, termina
return Analysis{}, fmt.Errorf("encoding insight evidence: %w", err)
}
copy := deterministicCopy(module, latest, previous, comparisonAvailable, timeContext, confidence)
if period != nil && module == ModuleTransactions {
copy = deterministicTransactionCopy(*period, latest, previous, comparisonAvailable, timeContext, confidence)
if period != nil {
copy = deterministicPeriodCopy(module, *period, precedingPeriodLabel, latest, previous, comparisonAvailable, timeContext, confidence)
}
return Analysis{Time: timeContext, Confidence: confidence, Evidence: encoded, Copy: copy, Calendar: calendar}, nil
}
@@ -311,11 +325,11 @@ func deterministicCopy(module Module, latest, previous aggregate, comparison boo
return copy
}
func deterministicTransactionCopy(period PeriodScope, latest, previous aggregate, comparison bool, _ TimeContext, confidence Confidence) Copy {
latestBody := fmt.Sprintf("در بازهٔ %s، %d تراکنش ثبت شده است: %d پرداخت‌شده و در انتظار تأیید، %d تأیید نهایی، %d ناموفق و %d برگشت‌خورده. نرخ موفقیت بانکی %s، تکمیل تأیید %s و نرخ ناموفق %s است.", period.JalaliLabelFA, latest.Sessions, latest.Unverified, latest.Verified, latest.Failed, latest.Reversed, ratioText(latest.Paid, latest.Sessions), ratioText(latest.Verified, latest.Paid), ratioText(latest.Failed, latest.Sessions))
pastBody := "برای بازهٔ بلافاصله قبل دادهٔ کافی برای مقایسه وجود ندارد."
func deterministicTransactionCopy(period PeriodScope, precedingPeriodLabel string, latest, previous aggregate, comparison bool, _ TimeContext, confidence Confidence) Copy {
latestBody := fmt.Sprintf("در %s، %d تراکنش ثبت شده است: %d پرداخت‌شده و در انتظار تأیید، %d تأیید نهایی، %d ناموفق و %d برگشت‌خورده. نرخ موفقیت بانکی %s، تکمیل تأیید %s و نرخ ناموفق %s است.", period.JalaliLabelFA, latest.Sessions, latest.Unverified, latest.Verified, latest.Failed, latest.Reversed, ratioText(latest.Paid, latest.Sessions), ratioText(latest.Verified, latest.Paid), ratioText(latest.Failed, latest.Sessions))
pastBody := fmt.Sprintf("این خلاصه فقط وضعیت %s را نشان می‌دهد و با داده‌های موجود دربارهٔ افزایش یا کاهش نسبت به دوره‌ای دیگر نتیجه‌گیری نمی‌کند.", period.JalaliLabelFA)
if comparison {
pastBody = fmt.Sprintf("در بازهٔ بلافاصله قبل %d تراکنش و %d پرداخت موفق بانکی ثبت شده بود؛ این مقایسه فقط تغییر مشاهده‌شده را نشان می‌دهد و علت آن را مشخص نمی‌کند.", previous.Sessions, previous.Paid)
pastBody = fmt.Sprintf("در %s، %d تراکنش و %d پرداخت موفق بانکی ثبت شده بود؛ این مقایسه فقط تغییر مشاهده‌شده را نشان می‌دهد و علت آن را مشخص نمی‌کند.", precedingPeriodLabel, previous.Sessions, previous.Paid)
}
uncertainty := "این جمع‌بندی فقط الگوی مشاهده‌شده در نمودارها را توصیف می‌کند و علت تغییر وضعیت‌ها را تعیین نمی‌کند."
return Copy{
@@ -332,6 +346,38 @@ func deterministicTransactionCopy(period PeriodScope, latest, previous aggregate
}
}
func deterministicPeriodCopy(module Module, period PeriodScope, precedingPeriodLabel string, latest, previous aggregate, comparison bool, tc TimeContext, confidence Confidence) Copy {
if module == ModuleTransactions {
return deterministicTransactionCopy(period, precedingPeriodLabel, latest, previous, comparison, tc, confidence)
}
latestBody := fmt.Sprintf("در %s، %d تراکنش ثبت شد و %d پرداخت با موفقیت انجام شد.", period.JalaliLabelFA, latest.Sessions, latest.Paid)
switch module {
case ModulePayments:
latestBody = fmt.Sprintf("در %s، از %d تراکنش ثبت‌شده، %d پرداخت موفق و %d پرداخت ناموفق بود.", period.JalaliLabelFA, latest.Sessions, latest.Paid, latest.Failed)
case ModuleFees:
latestBody = fmt.Sprintf("در %s، مجموع کارمزد ثبت‌شده %d ریال بود و %d پرداخت موفق انجام شد.", period.JalaliLabelFA, latest.Fees, latest.Paid)
case ModulePeers:
latestBody = fmt.Sprintf("در %s، کسب‌وکار شما %d پرداخت موفق از %d تراکنش ثبت‌شده داشت.", period.JalaliLabelFA, latest.Paid, latest.Sessions)
}
pastBody := fmt.Sprintf("این خلاصه فقط وضعیت %s را نشان می‌دهد و با داده‌های موجود دربارهٔ افزایش یا کاهش نسبت به دوره‌ای دیگر نتیجه‌گیری نمی‌کند.", period.JalaliLabelFA)
if comparison {
direction := "تقریباً مشابه"
if latest.Paid > previous.Paid {
direction = "بیشتر"
} else if latest.Paid < previous.Paid {
direction = "کمتر"
}
pastBody = fmt.Sprintf("تعداد پرداخت‌های موفق در %s نسبت به %s %s بوده است؛ بهتر است همین روند را در دورهٔ بعد هم بررسی کنید.", period.JalaliLabelFA, precedingPeriodLabel, direction)
}
copy := deterministicCopy(module, latest, previous, comparison, tc, confidence)
copy.LatestObserved = Narrative{Headline: "خلاصهٔ این بازه", Body: latestBody, EvidenceID: []string{"latest-volume", "payment-health"}}
copy.Past = Narrative{Headline: "مقایسه با بازهٔ قبل", Body: pastBody, EvidenceID: []string{"previous-year-equivalent"}}
return copy
}
func score(impact int64, confidence Confidence) int {
base := int(math.Min(8000, math.Log10(float64(max(int64(1), impact))+1)*1200))
weight := map[Confidence]int{ConfidenceHigh: 10000, ConfidenceMedium: 7500, ConfidenceLow: 4500, ConfidenceInsufficientData: 2000}[confidence]