Files
zarinpal-analytics-backend/docs/ANALYTICS-SPEC.md

186 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Analytics Engine Capability Spec
- Status: Approved specification — implementation authorized 2026-08-20
- Capability: Analytics engine
- Depends on: Approved Data Foundation specification
- Last updated: 2026-08-20
## Objective
Turn normalized ZarinPal payment facts into trustworthy, merchant-scoped analytics that explain payment performance, verification gaps, retry outcomes, processing time, fees, trends, and same-category position. Deterministic calculations are the product source of truth; later AI assistance may explain or recommend actions from sanitized aggregates but cannot define or alter metrics.
## Actors and consumers
- Authenticated merchant: sees detailed metrics and drill-downs only for its merchant boundary.
- Merchant API: requests versioned analytics sections and exposes typed results/events.
- Dashboard: renders sections independently as they become ready.
- Analytics worker: calculates idempotent section results from PostgreSQL and caches approved aggregates in Redis.
- AI growth analyst: separately approved consumer of minimized, approved aggregate facts only; see ADR 0015 and `GROWTH-ANALYST-SPEC.md`.
## In scope
- Daily, Saturday–Friday weekly, Jalali-month, and Persian-season analytics in `Asia/Tehran`
- Overview, payment performance, fees, and peer-comparison sections
- Payment success and volume with correct `Paid`/`Verified` semantics
- Verification completion and unverified-paid gaps
- Retry recovery and attempt/session diagnostics
- Processing-time p50/p95 with sample and coverage
- Real merchant fee amounts and paid fee rate at session grain
- Current-period trends against an approved equivalent baseline
- Same-category merchant-weighted peer median and performance percentile
- Lazy, bounded parallel calculation with partial SSE delivery
- Versioned 30-day Redis cache keyed by the immutable MVP dataset snapshot
- Traceability from aggregate metrics to permitted merchant-owned evidence
- Merchant-wide or authenticated merchant-terminal scope for deterministic merchant metrics
- Bounded daily payment-history points and filtered read-only transaction evidence
## Out of scope for MVP
The exclusions in this section describe the original four-section analytics MVP. The later, separately approved AI growth analyst adds bounded event analytics and forecasting without changing the canonical MVP period or metric contracts.
- Holiday or named-event reporting inside the original four analytics sections
- Unbounded or free-form LLM analysis
- Causal claims or automated campaign execution
- Arbitrary merchant-created formulas or cross-merchant filters
- Real-time streaming payment ingestion owned by this service
- Persisting calculated analytics as PostgreSQL business facts
- Public or unauthenticated analytics
## Canonical sections
| Section id | Responsibility | Minimum result |
|---|---|---|
| `overview` | Merchant-level outcome and trend summary | bank-paid sessions/amount, success rate, verification completion, selected period, prior-period comparison |
| `payment_performance` | Status, retry, and timing analysis | paid/verified/unverified/failed/reversed counts, retry recovery, init/verify p50/p95 with coverage |
| `fees` | Merchant fee analysis | paid fee amount, paid amount denominator, paid fee rate, period trend |
| `peer_comparison` | Privacy-bounded same-category benchmark | eligible peer count/session count, cohort median, direction-aware merchant percentile or typed suppression |
Every section uses a versioned typed contract and has independent `queued`, `calculating`, `ready`, `failed`, and where applicable `suppressed` states.
## Metric and period contract
- `METRIC-CATALOGUE.md` version `0.5.0` is canonical for formulas, units, grains, missing-data behavior, and peer direction.
- `Paid` and `Verified` both mean bank payment succeeded; only `Verified` is finalized. Never label `Paid` as failure.
- Amount and fee calculations count each eligible payment session once. Attempt rows cannot multiply session values.
- Rates are calculated from summed numerator and denominator components for the selected period, never averaged from daily percentages.
- Timing percentiles exclude null values and expose sample size and non-null coverage; missing time never becomes zero.
- `NoAttempt` markers create zero-attempt sessions and never count as attempts or retries. `InBank` is an observed attempt stage, not proof of rejection.
- Every result states the exact Tehran-local half-open period, Jalali label, unit, sample/denominator, dataset snapshot, metric-definition version, and calculation timestamp.
- Approved interactive reporting-period types remain one Tehran-local day, one Saturday–Friday week, one Jalali month, or one Persian season. The separately versioned growth-event catalog is evidence for growth reports, not a new interactive reporting-period type.
## Peer comparison contract
- Eligible peers are other merchants in the active merchant's category for the same selected period.
- Suppress the peer result unless at least 10 other merchants and 1,000 combined peer sessions contribute.
- Calculate each peer merchant's metric independently and take the median of those merchant values; do not transaction-weight the cohort.
- Return the cohort median and active merchant's performance percentile. Higher displayed percentile always means better, so processing-time and fee-rate ranking is inverted.
- Benchmark payment success, verification completion, retry recovery, init/verify p50/p95, and paid fee rate.
- Never return peer identities, individual values/ranks, rows, or arbitrary filters that enable differencing attacks.
- Peer comparison remains merchant-grain. A terminal-scoped run suppresses this section with `TERMINAL_SCOPE_UNSUPPORTED` instead of presenting merchant-wide peer values as terminal-scoped evidence.
## Terminal scope and payment history
- `terminalId` is optional and is resolved only under the merchant from the authenticated server-side session. Omission means all merchant terminals; unknown and other-merchant identifiers fail identically.
- Overview, payment-performance, fees, and payment-history calculations use the selected terminal predicate when present.
- Payment-history points use Tehran-local buckets and return total sessions, bank-paid sessions, and bank-paid amount in Rial. `DAY` periods are grouped by hour and include `localHour` from 0 through 23; longer periods remain grouped by date.
- The payment-history array is bounded by the maximum approved period length and never contains payer tokens, peer rows, or attempt-grain money.
## Transaction evidence
- The `ALL`, `UNVERIFIED_PAID`, `FAILED`, and `RETRIED` views are scoped by authenticated merchant and approved period.
- Optional terminal, status, failure-stage, and non-negative amount bounds are allowlisted and parameterized. Failure stage applies only to `FAILED`.
- Keyset cursors are authenticated-encrypted, bound to merchant/period/canonical filters, and expire after 24 hours; replay under another binding is rejected.
- One-session detail resolves the opaque session id under the merchant and returns ordered attempts without payer-card keys or response-code fields.
- JSON and CSV reuse the same canonical predicate. CSV omits cursor/page limit, is capped at 100,000 rows, and applies a 60-second deadline.
## Calculation and cache lifecycle
1. An idempotent authenticated start request resolves merchant, selected period, immutable dataset snapshot, and metric-definition version.
2. Equivalent requests map to one current analytics run and do not enqueue duplicate work.
3. Missing section results are enqueued together and calculated independently by the Go worker through Asynq `v0.26.0`, explicitly a pre-1.0 dependency pinned for compatibility.
4. Worker limits default to 16 jobs globally and four per merchant. All four core sections begin in bounded parallel and become visible independently.
5. Each attempt times out after 60 seconds. Transient failures receive at most two retries using exponential backoff of approximately one then two seconds plus jitter; deterministic failures are not retried.
6. Successful section results are published atomically to Redis under keys containing the immutable dataset snapshot, merchant, period, section, and metric-definition version, with a 30-day TTL. PostgreSQL normalized facts remain authoritative.
7. Payment facts do not change during the hackathon MVP, so no insert/update/delete invalidation trigger exists. Any future dataset replacement requires a separately approved recovery and cache-namespace-selection contract.
8. Redis loss causes safe lazy recomputation. It never causes loss of payment facts or metric definitions.
## Partial delivery contract
- A separate authenticated SSE connection streams run/section state and each completed section immediately.
- SSE sends a heartbeat every 15 seconds and retains authorized replay events for one hour.
- Reconnect uses the last event id where retained; otherwise the client fetches the current run snapshot and reopens the stream.
- One failed section never hides ready siblings. Manual retry targets only an exhausted failed section and cannot clear successful results.
- Start limits are 10 requests/minute with burst 3; manual retry limits are 3 requests/10 minutes with burst 1; SSE limits are two concurrent connections/user and four/merchant. Independent user and merchant buckets return `429` with `Retry-After` before work is queued.
## Traceability and privacy
- Merchant-owned aggregate cards link to definitions and, where approved, tenant-scoped session/attempt evidence that reconciles to the aggregate.
- Payer-card tokens remain internal, opaque, merchant-scoped inputs and never appear in APIs, logs, exports, caches, peer results, or AI payloads.
- Redis values contain approved aggregates and trace metadata only, not raw payment rows or credentials.
- Cross-merchant calculations execute server-side and return only approved aggregate/suppressed contracts.
- Future AvalAI prompts may receive minimized aggregates only after a separate AI capability approval.
## Failure and edge behavior
- Zero, undefined denominator, unavailable source, peer suppression, calculation failure, and not-yet-ready are distinct typed states.
- Unknown future payment statuses are excluded from success numerator/denominator and reported through coverage evidence until classified.
- `Reversed` remains outside success and is displayed separately when non-zero.
- Empty approved periods return a valid empty result, not fabricated zero rates or a system failure.
- Section publication is atomic; partial/failed calculations never overwrite a valid ready cache entry.
- Database, Redis, queue, timeout, and authorization errors are distinguishable internally while merchant-facing errors remain safe and actionable.
## Actionable insight threshold
- A deterministic actionable insight is eligible only when its comparison contains at least 100 relevant payment sessions.
- Rate insights require an absolute change of at least 3 percentage points. Amount and count insights require an absolute relative change of at least 20 percent.
- The threshold is a display eligibility rule, not a change to the underlying metric. Exact metrics and comparisons remain available even when no insight card is emitted.
- A zero, missing, or undefined baseline cannot produce a relative-change insight. Return the metric and an explicit unavailable comparison instead of dividing by zero or inventing a trend.
- Meeting a threshold does not prove causation. Insight language describes the observed change and a possible merchant action without claiming why it happened.
## Observability
- Measure section queue delay, calculation duration, query duration, cache hit/miss, retry count, failure class, SSE delivery/reconnect, and suppression rate.
- Correlate start request, analytics run, section job, cache key version, and SSE event without logging sensitive merchant rows or payer tokens.
- Record PostgreSQL query plans during performance verification and add indexes only from measured access paths.
- Define service-level objectives and alert thresholds in the Platform Operations spec, not implicitly in worker code.
## Acceptance criteria
- Every metric reconciles to an independent PostgreSQL query over normalized facts for representative merchants and all approved period types.
- Session-grain amount/fee totals remain unchanged when a session has multiple attempts.
- `Paid` plus `Verified` success, verification completion, retry recovery, and timing coverage match catalogue definitions.
- Saturday–Friday, Jalali-month, and Persian-season boundaries match `calendar_dates` and API half-open ranges.
- First access starts all missing sections once; ready sections stream independently and equivalent concurrent starts deduplicate.
- Cache hits avoid repeating source aggregation; the immutable completed-import snapshot keeps equivalent requests in one stable namespace.
- Redis flush causes correct cold-cache rebuilding without source-data loss.
- Timeouts, transient retry limits/backoff, failed-section isolation, manual retry, rate limits, heartbeat, replay, and snapshot fallback behave as specified.
- Peer results exclude the active merchant, use equal merchant weighting, obey both suppression thresholds, and never expose individual peers.
- Tenant-bound drill-downs reconcile to aggregates and cannot expose another merchant through parameter manipulation or cursor replay.
- Read-only drill-downs cover all, unverified-paid, failed, and retried sessions. Failed-session filters may distinguish zero-attempt and reached-bank stages without exposing payer tokens or claiming undocumented causes.
- No raw payer token, credential, cross-tenant row, or AvalAI secret enters results, logs, cache payloads, or browser data.
## Boundaries
- Always: use catalogue definitions, preserve grain, enforce tenant scope, expose period/unit/coverage/version, calculate idempotently, and keep results reproducible.
- Ask first: add/change a metric, period, peer cohort, threshold, cache key/version, drill-down, AI input, or cross-service interface.
- Never: average rates, multiply session money by attempts, classify `Paid` as failure, expose peer rows, trust client tenant ids, or let AI become the metric source of truth.
## Resolved decisions
- Normalized PostgreSQL facts are authoritative; Redis is a 30-day disposable analytics cache.
- Four sections calculate lazily and in bounded parallel, with partial authenticated SSE delivery.
- Reporting covers every complete day, Iranian week, Jalali month, and Persian season inside the imported dataset's local-date coverage; arbitrary client-computed ranges remain disallowed.
- Peer comparison uses same-category merchant-weighted medians and direction-aware percentiles with the approved suppression threshold.
- Payment lifecycle, retry, timing, and real-fee definitions follow `METRIC-CATALOGUE.md` and ADR 0013.
- Asynq `v0.26.0` is pinned and acknowledged as pre-1.0; jobs have bounded concurrency, timeouts, rate limits, and exponential-backoff retries.
- Default first view is the latest complete Jalali month containing source data, avoiding an empty current month in the static demo.
- Compare with the immediately previous equivalent complete period. If it is empty or unavailable, return an explicit unavailable comparison rather than silently substituting another baseline.
- Overview priority is bank-paid amount, bank-paid session count, payment success rate, then verification completion. Retry, timing, and fee analysis follow below.
- MVP read-only drill-downs cover all, unverified-paid, failed, and retried sessions; payer tokens and peer-level rows are excluded.
- Terminal-scoped merchant metrics are supported, while merchant-grain peer comparison is explicitly suppressed for terminal runs.
- Actionable insight cards require at least 100 relevant sessions and either a 3-percentage-point rate change or a 20-percent amount/count change. Below-threshold comparisons remain visible as metrics but do not generate an action claim.
## Open product decisions before implementation planning
- None. Any new or changed metric, period, cohort, threshold, or cross-service contract requires a specification amendment before implementation.