From c79a1219bfdc54cd1b7d01bdeb0cfec133fc8c32 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Fri, 21 Aug 2026 01:16:57 +0330 Subject: [PATCH] feat: integrate AI growth analyst experience --- DESIGN.md | 1 + src/lib/api/client.ts | 18 + src/lib/api/growth-client.test.ts | 56 + src/lib/api/schema.d.ts | 3236 ++++++++++------- src/lib/components/GrowthForecastChart.svelte | 8 +- .../components/GrowthForecastChart.test.ts | 4 + src/lib/components/GrowthInsights.svelte | 207 ++ src/lib/components/GrowthPage.svelte | 284 ++ src/lib/growth-report-state.test.ts | 21 + src/lib/growth-report-state.ts | 11 + src/routes/(app)/growth/+page.svelte | 5 + tasks/ai-growth-analyst.md | 11 +- 12 files changed, 2441 insertions(+), 1421 deletions(-) create mode 100644 src/lib/api/growth-client.test.ts create mode 100644 src/lib/components/GrowthInsights.svelte create mode 100644 src/lib/components/GrowthPage.svelte create mode 100644 src/lib/growth-report-state.test.ts create mode 100644 src/lib/growth-report-state.ts create mode 100644 src/routes/(app)/growth/+page.svelte diff --git a/DESIGN.md b/DESIGN.md index 604c9c4..4c1a170 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -132,6 +132,7 @@ The approved Persian-first baseline remains authoritative where the shipped arti | Version | Date | Decision | | ------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0.11.0 | 2026-08-21 | Added the feature-gated AI growth analyst: a cache-first 90-day forecast view with count/amount modes, P10–P90 uncertainty band, keyboard and tabular alternatives, evidence-linked Persian campaign guidance, preserved-content refresh states, and restrained semantic opportunity/risk styling. | | 0.10.0 | 2026-08-20 | Reframed retry guidance around the share of failed payment paths retried, including an explicit zero state; made payment-flow bars proportional to counts; and removed single-day hover styling from whole-week selection. | | 0.9.3 | 2026-08-20 | Replaced stacked field focus outlines with one narrow semantic-blue border across text fields, textareas, native selects, and custom select triggers; other keyboard targets retain a subtle 1px outline. | | 0.9.2 | 2026-08-20 | Pinned the initialization skeleton sidebar to the physical right and reserved matching right-side content space so loading and loaded shells share the same geometry. | diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index a2399a2..11aa0b5 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -11,6 +11,7 @@ export type SessionStatus = components['schemas']['SessionStatus']; export type DetailView = components['schemas']['DetailView']; export type FailureStage = components['schemas']['FailureStage']; export type Problem = components['schemas']['Problem']; +export type GrowthReport = components['schemas']['GrowthReport']; const apiBase = (import.meta.env.PUBLIC_API_BASE_URL || '/api/v1').replace(/\/$/, ''); @@ -119,6 +120,23 @@ export const api = { sessionDetail(sessionId: string) { return request(`/analytics/sessions/${encodeURIComponent(sessionId)}`); }, + latestGrowthReport(terminalId?: string) { + const query = terminalId ? `?terminalId=${encodeURIComponent(terminalId)}` : ''; + return request(`/analytics/growth-reports/latest${query}`); + }, + createGrowthReport(csrfToken: string, options: { terminalId?: string; refresh?: boolean } = {}) { + return request('/analytics/growth-reports', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken, + 'Idempotency-Key': crypto.randomUUID(), + }, + body: JSON.stringify(options), + }); + }, + growthReport(reportId: string) { + return request(`/analytics/growth-reports/${encodeURIComponent(reportId)}`); + }, exportUrl( periodId: string, view: DetailView, diff --git a/src/lib/api/growth-client.test.ts b/src/lib/api/growth-client.test.ts new file mode 100644 index 0000000..06a33e6 --- /dev/null +++ b/src/lib/api/growth-client.test.ts @@ -0,0 +1,56 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { api } from './client'; + +const report = { + reportId: 'report-1', + status: 'QUEUED', + scope: { terminalId: null }, + horizonDays: 90, + deterministicEvidence: {}, + cached: false, + stale: false, + nextRefreshAt: '2026-08-22T00:00:00Z', + createdAt: '2026-08-21T00:00:00Z', + updatedAt: '2026-08-21T00:00:00Z', + eventsUrl: '/api/v1/analytics/growth-reports/report-1/events', +}; + +afterEach(() => vi.unstubAllGlobals()); + +describe('growth report API client', () => { + it('reads the cached terminal report without creating a new report', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify(report), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchMock); + + await api.latestGrowthReport('terminal-1'); + + expect(fetchMock).toHaveBeenCalledOnce(); + expect(fetchMock.mock.calls[0][0]).toBe('/api/v1/analytics/growth-reports/latest?terminalId=terminal-1'); + expect(fetchMock.mock.calls[0][1].method).toBeUndefined(); + }); + + it('marks manual refresh explicitly and sends the authenticated write headers', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify(report), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchMock); + vi.stubGlobal('crypto', { randomUUID: () => 'idempotency-1' }); + + await api.createGrowthReport('csrf-1', { terminalId: 'terminal-1', refresh: true }); + + const init = fetchMock.mock.calls[0][1]; + expect(init.method).toBe('POST'); + expect(init.headers['X-CSRF-Token']).toBe('csrf-1'); + expect(init.headers['Idempotency-Key']).toBe('idempotency-1'); + expect(JSON.parse(init.body)).toEqual({ terminalId: 'terminal-1', refresh: true }); + }); +}); diff --git a/src/lib/api/schema.d.ts b/src/lib/api/schema.d.ts index bc24835..0713585 100644 --- a/src/lib/api/schema.d.ts +++ b/src/lib/api/schema.d.ts @@ -4,1447 +4,1865 @@ */ export interface paths { - "/auth/sessions": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Authenticate a demo merchant user - * @description Creates a revocable server-side session. Expected credential failures are - * externally indistinguishable. The response sets the host-only - * `__Host-zpa_session` cookie; the raw token is never present in JSON. - */ - post: operations["createAuthSession"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + '/auth/sessions': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - "/auth/session": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get the current session summary */ - get: operations["getAuthSession"]; - put?: never; - post?: never; - /** - * Revoke the current session - * @description Revokes server-side state before expiring the browser cookie. - */ - delete: operations["deleteAuthSession"]; - options?: never; - head?: never; - patch?: never; - trace?: never; + get?: never; + put?: never; + /** + * Authenticate a demo merchant user + * @description Creates a revocable server-side session. Expected credential failures are + * externally indistinguishable. The response sets the host-only + * `__Host-zpa_session` cookie; the raw token is never present in JSON. + */ + post: operations['createAuthSession']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/auth/session': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - "/analytics/periods": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List approved reporting periods - * @description Returns allowlisted periods with server-calculated UTC and Tehran-local - * half-open boundaries. Clients start runs with a returned `periodId` and - * never submit arbitrary timestamps. - */ - get: operations["listAnalyticsPeriods"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + /** Get the current session summary */ + get: operations['getAuthSession']; + put?: never; + post?: never; + /** + * Revoke the current session + * @description Revokes server-side state before expiring the browser cookie. + */ + delete: operations['deleteAuthSession']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/periods': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - "/analytics/terminals": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List terminals owned by the authenticated merchant - * @description Returns display-safe terminal choices scoped exclusively from the authenticated server session. - */ - get: operations["listMerchantTerminals"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + /** + * List approved reporting periods + * @description Returns every complete allowlisted day, Iranian week, Jalali month, and + * Persian season inside the imported dataset's local-date coverage, with + * server-calculated UTC and Tehran-local half-open boundaries. Periods are + * grouped by type and ordered newest-first inside each group. Clients start + * runs with a returned `periodId` and never submit arbitrary timestamps. + */ + get: operations['listAnalyticsPeriods']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/terminals': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - "/analytics/runs": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Create or return the current analytics run - * @description Idempotently resolves the current merchant/dataset/metric version - * and starts only missing sections. Equivalent concurrent starts deduplicate. - */ - post: operations["createAnalyticsRun"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + /** + * List terminals owned by the authenticated merchant + * @description Returns display-safe terminal choices scoped exclusively from the authenticated server session. + */ + get: operations['listMerchantTerminals']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/runs': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - "/analytics/runs/{runId}": { - parameters: { - query?: never; - header?: never; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components["parameters"]["RunIdPath"]; - }; - cookie?: never; - }; - /** Get the current run snapshot */ - get: operations["getAnalyticsRun"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + get?: never; + put?: never; + /** + * Create or return the current analytics run + * @description Idempotently resolves the current merchant/dataset/metric version + * and starts only missing sections. Equivalent concurrent starts deduplicate. + */ + post: operations['createAnalyticsRun']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/runs/{runId}': { + parameters: { + query?: never; + header?: never; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components['parameters']['RunIdPath']; + }; + cookie?: never; }; - "/analytics/runs/{runId}/events": { - parameters: { - query?: never; - header?: never; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components["parameters"]["RunIdPath"]; - }; - cookie?: never; - }; - /** - * Stream partial analytics results - * @description Authenticated SSE stream. Events are retained for one hour. A retained - * `Last-Event-ID` replays only later events. Missing, invalid, unauthorized, - * or expired ids reset safely by emitting `run.snapshot` with - * `recovery=RESET_TO_SNAPSHOT`, then continuing from new events. - * - * Event types are `run.snapshot`, `section.started`, `section.ready`, - * `section.failed`, `section.suppressed`, `run.completed`, and `heartbeat`. - * Each event has an opaque `id` and one-line JSON `data` matching one of the - * schemas named by `AnalyticsSseEvent`. - */ - get: operations["streamAnalyticsRunEvents"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + /** Get the current run snapshot */ + get: operations['getAnalyticsRun']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/runs/{runId}/events': { + parameters: { + query?: never; + header?: never; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components['parameters']['RunIdPath']; + }; + cookie?: never; }; - "/analytics/runs/{runId}/sections/{sectionId}/retry-attempts": { - parameters: { - query?: never; - header?: never; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components["parameters"]["RunIdPath"]; - sectionId: components["parameters"]["SectionIdPath"]; - }; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Retry one exhausted failed section - * @description Does not clear or restart ready sibling sections. - */ - post: operations["createAnalyticsSectionRetryAttempt"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + /** + * Stream partial analytics results + * @description Authenticated SSE stream. Events are retained for one hour. A retained + * `Last-Event-ID` replays only later events. Missing, invalid, unauthorized, + * or expired ids reset safely by emitting `run.snapshot` with + * `recovery=RESET_TO_SNAPSHOT`, then continuing from new events. + * + * Event types are `run.snapshot`, `section.started`, `section.ready`, + * `section.failed`, `section.suppressed`, `run.completed`, and `heartbeat`. + * Each event has an opaque `id` and one-line JSON `data` matching one of the + * schemas named by `AnalyticsSseEvent`. + */ + get: operations['streamAnalyticsRunEvents']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/runs/{runId}/sections/{sectionId}/retry-attempts': { + parameters: { + query?: never; + header?: never; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components['parameters']['RunIdPath']; + sectionId: components['parameters']['SectionIdPath']; + }; + cookie?: never; }; - "/analytics/session-details": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List authorized merchant-owned session evidence - * @description Cursor-paginated detail rows for one approved period and view. Stable - * ordering is `createdAt DESC, sessionId DESC`. The server binds cursors to - * merchant, filters, and ordering. Cross-tenant and absent ids are not - * distinguishable. - */ - get: operations["listAnalyticsSessionDetails"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + get?: never; + put?: never; + /** + * Retry one exhausted failed section + * @description Does not clear or restart ready sibling sections. + */ + post: operations['createAnalyticsSectionRetryAttempt']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/session-details': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - "/analytics/sessions/{sessionId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get one merchant-owned payment session and attempt history */ - get: operations["getAnalyticsSession"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + /** + * List authorized merchant-owned session evidence + * @description Cursor-paginated detail rows for one approved period and view. Stable + * ordering is `createdAt DESC, sessionId DESC`. The server binds cursors to + * merchant, filters, and ordering. Cross-tenant and absent ids are not + * distinguishable. + */ + get: operations['listAnalyticsSessionDetails']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/sessions/{sessionId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - "/analytics/session-details.csv": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Export the authorized detail view as CSV - * @description Uses the same tenant scope, approved period, filters, and allowlisted - * projection as `listAnalyticsSessionDetails`. Cursor and page limit do not - * apply. Output is UTF-8 with a BOM, a Persian header row whose labels use - * normal spaces, comma separators, CRLF records, and RFC 4180 quoting. - * - * Stable columns are: - * `شناسه نشست پرداخت`, `شناسه پایانه`, `وضعیت پرداخت`, `مبلغ ریال`, - * `کارمزد ریال`, `زمان ایجاد`, `زمان انقضا`, `تعداد تلاش`, - * `وضعیت آخرین تلاش`, `زمان آخرین تلاش`, `زمان تأیید`, `زمان تسویه`, - * `مرحله مشاهده شده شکست`. - * - * CSV status labels are Persian: `Paid` is `پرداخت شده و تأیید نشده`, - * `Verified` is `تأیید شده`, `Failed` is `ناموفق`, `Reversed` is - * `برگشت خورده`, and `InBank` is `در بانک`. Numeric cells remain plain - * calculation-safe numbers and timestamps remain UTC ISO values. - * - * Columns unavailable to the chosen authorized view are empty. Payer tokens, - * credentials, peer data, internal ids, and any field unavailable in the JSON - * detail view are forbidden. Potential spreadsheet formulas in textual cells - * are prefixed with an apostrophe before CSV quoting. - */ - get: operations["exportAnalyticsSessionDetailsCsv"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + /** Get one merchant-owned payment session and attempt history */ + get: operations['getAnalyticsSession']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/session-details.csv': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + /** + * Export the authorized detail view as CSV + * @description Uses the same tenant scope, approved period, filters, and allowlisted + * projection as `listAnalyticsSessionDetails`. Cursor and page limit do not + * apply. Output is UTF-8 with a BOM, a Persian header row whose labels use + * normal spaces, comma separators, CRLF records, and RFC 4180 quoting. + * + * Stable columns are: + * `شناسه نشست پرداخت`, `شناسه پایانه`, `وضعیت پرداخت`, `مبلغ ریال`, + * `کارمزد ریال`, `زمان ایجاد`, `زمان انقضا`, `تعداد تلاش`, + * `وضعیت آخرین تلاش`, `زمان آخرین تلاش`, `زمان تأیید`, `زمان تسویه`, + * `مرحله مشاهده شده شکست`. + * + * CSV status labels are Persian: `Paid` is `پرداخت شده و تأیید نشده`, + * `Verified` is `تأیید شده`, `Failed` is `ناموفق`, `Reversed` is + * `برگشت خورده`, and `InBank` is `در بانک`. Numeric cells remain plain + * calculation-safe numbers and timestamps remain UTC ISO values. + * + * Columns unavailable to the chosen authorized view are empty. Payer tokens, + * credentials, peer data, internal ids, and any field unavailable in the JSON + * detail view are forbidden. Potential spreadsheet formulas in textual cells + * are prefixed with an apostrophe before CSV quoting. + */ + get: operations['exportAnalyticsSessionDetailsCsv']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/growth-reports': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or reuse a growth report */ + post: operations['createGrowthReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/growth-reports/latest': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getLatestGrowthReport']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/growth-reports/{reportId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getGrowthReport']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/analytics/growth-reports/{reportId}/events': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['streamGrowthReportEvents']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { - schemas: { - LoginRequest: { - /** @example merchant-0001 */ - username: string; - password: string; - }; - AuthSession: { - user: components["schemas"]["MerchantUserSummary"]; - merchant: components["schemas"]["MerchantSummary"]; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - idleExpiresAt: string; - /** Format: date-time */ - absoluteExpiresAt: string; - /** @description Session-bound CSRF token. It is not an authentication token and must not be logged. */ - csrfToken: string; - }; - MerchantUserSummary: { - userId: string; - /** @example merchant-0001 */ - username: string; - }; - MerchantSummary: { - /** @description Opaque API identifier, never accepted as authorization input. */ - merchantId: string; - displayName: string; - profileIsSynthetic: boolean; - }; - /** @enum {string} */ - PeriodType: "DAY" | "WEEK" | "JALALI_MONTH" | "PERSIAN_SEASON"; - /** @example jm-1405-05 */ - PeriodId: string; - ReportingPeriod: { - periodId: components["schemas"]["PeriodId"]; - periodType: components["schemas"]["PeriodType"]; - /** - * Format: date-time - * @description Inclusive UTC boundary. - * @example 2026-07-23T20:30:00Z - */ - startAt: string; - /** - * Format: date-time - * @description Exclusive UTC boundary. - * @example 2026-08-22T20:30:00Z - */ - endAt: string; - /** - * Format: date-time - * @description Inclusive Asia/Tehran civil boundary with explicit offset. - * @example 2026-07-24T00:00:00+03:30 - */ - localStartAt: string; - /** - * Format: date-time - * @description Exclusive Asia/Tehran civil boundary with explicit offset. - * @example 2026-08-23T00:00:00+03:30 - */ - localEndAt: string; - /** @constant */ - timezone: "Asia/Tehran"; - /** @example مرداد ۱۴۰۵ */ - jalaliLabelFa: string; - isComplete: boolean; - isDefault: boolean; - }; - PeriodList: { - defaultPeriodId: components["schemas"]["PeriodId"]; - periods: components["schemas"]["ReportingPeriod"][]; - }; - CreateAnalyticsRunRequest: { - periodId: components["schemas"]["PeriodId"]; - terminalId?: string; - }; - Terminal: { - terminalId: string; - displayName: string; - /** Format: int64 */ - sessionCount: number; - isDefault: boolean; - }; - TerminalList: { - terminals: components["schemas"]["Terminal"][]; - }; - AnalyticsScope: { - terminalId: string | null; - }; - /** @example run_01K2M7YYYYYYYYYYYYYYYYYYYY */ - RunId: string; - /** @enum {string} */ - SectionId: "overview" | "payment_performance" | "fees" | "peer_comparison"; - /** @enum {string} */ - AnalyticsRunState: "IN_PROGRESS" | "COMPLETE" | "COMPLETE_WITH_FAILURES"; - /** @enum {string} */ - SectionState: "QUEUED" | "CALCULATING" | "READY" | "FAILED" | "SUPPRESSED"; - AnalyticsRun: { - runId: components["schemas"]["RunId"]; - state: components["schemas"]["AnalyticsRunState"]; - period: components["schemas"]["ReportingPeriod"]; - /** @description Opaque identifier for the immutable completed-import snapshot. */ - datasetSnapshotId: string; - /** @constant */ - metricDefinitionVersion: "0.4.0"; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string; - /** Format: uri-reference */ - eventsUrl: string; - scope: components["schemas"]["AnalyticsScope"]; - sections: { - overview: components["schemas"]["OverviewSection"]; - paymentPerformance: components["schemas"]["PaymentPerformanceSection"]; - fees: components["schemas"]["FeesSection"]; - peerComparison: components["schemas"]["PeerComparisonSection"]; - }; - }; - QueuedSection: { - /** @constant */ - state: "QUEUED"; - /** Format: date-time */ - queuedAt?: string | null; - }; - CalculatingSection: { - /** @constant */ - state: "CALCULATING"; - /** Format: date-time */ - startedAt: string; - }; - FailedSection: { - /** @constant */ - state: "FAILED"; - /** Format: date-time */ - failedAt: string; - /** @enum {string} */ - errorCode: "CALCULATION_FAILED" | "CALCULATION_TIMEOUT" | "SOURCE_UNAVAILABLE" | "UNSUPPORTED_METRIC_VERSION"; - canRetry: boolean; - /** Format: uri-reference */ - retryAttemptsUrl?: string | null; - }; - ReadySectionMetadata: { - /** Format: date-time */ - calculatedAt: string; - datasetSnapshotId: string; - /** @constant */ - metricDefinitionVersion: "0.4.0"; - }; - IntegerMetric: { - metricId: string; - value: number; - /** @enum {string} */ - unit: "SESSIONS" | "RIAL"; - }; - DecimalMetric: { - metricId: string; - /** @description Null only when the denominator is zero or source is unavailable. */ - value: string | null; - /** @enum {string} */ - unit: "PERCENT" | "RATIO"; - numerator: number; - denominator: number; - }; - TimingDistribution: { - /** @enum {string} */ - metricId: "init_time_ms" | "verify_time_ms"; - p50Ms: number | null; - p95Ms: number | null; - sampleSize: number; - eligibleSampleSize: number; - coveragePercent: string | null; - }; - PreviousPeriodComparison: { - /** @enum {string} */ - state: "AVAILABLE" | "UNAVAILABLE"; - baselinePeriod: components["schemas"]["ReportingPeriod"]; - absoluteChange?: string | null; - relativeChangePercent?: string | null; - /** @enum {string|null} */ - unavailableReason?: "EMPTY_BASELINE" | "ZERO_BASELINE" | "SOURCE_UNAVAILABLE" | null; - isActionableInsightEligible?: boolean; - }; - OverviewData: { - /** - * @example { - * "metricId": "bank_paid_sessions", - * "value": 420, - * "unit": "SESSIONS" - * } - */ - bankPaidSessions: components["schemas"]["IntegerMetric"]; - /** - * @example { - * "metricId": "bank_paid_amount_rial", - * "value": 680000000, - * "unit": "RIAL" - * } - */ - bankPaidAmountRial: components["schemas"]["IntegerMetric"]; - paymentSuccessRate: components["schemas"]["DecimalMetric"]; - verificationCompletionRate: components["schemas"]["DecimalMetric"]; - comparisons: { - [key: string]: components["schemas"]["PreviousPeriodComparison"]; - }; - paymentHistory: components["schemas"]["PaymentHistoryPoint"][]; - }; - PaymentHistoryPoint: { - /** Format: date */ - localDate: string; - /** Format: int64 */ - totalSessions: number; - /** Format: int64 */ - bankPaidSessions: number; - /** Format: int64 */ - bankPaidAmountRial: number; - }; - PaymentPerformanceData: { - bankPaidSessions: components["schemas"]["IntegerMetric"]; - verifiedSessions: components["schemas"]["IntegerMetric"]; - unverifiedPaidSessions: components["schemas"]["IntegerMetric"]; - failedSessions: components["schemas"]["IntegerMetric"]; - reversedSessions: components["schemas"]["IntegerMetric"]; - noAttemptSessions: components["schemas"]["IntegerMetric"]; - reachedBankSessions: components["schemas"]["IntegerMetric"]; - retriedSessions: components["schemas"]["IntegerMetric"]; - retryRecoveryRate: components["schemas"]["DecimalMetric"]; - initTiming: components["schemas"]["TimingDistribution"]; - verifyTiming: components["schemas"]["TimingDistribution"]; - }; - FeesData: { - paidFeeAmountRial: components["schemas"]["IntegerMetric"]; - bankPaidAmountRial: components["schemas"]["IntegerMetric"]; - paidFeeRate: components["schemas"]["DecimalMetric"]; - comparison: components["schemas"]["PreviousPeriodComparison"]; - }; - PeerMetric: { - /** @enum {string} */ - metricId: "payment_success_rate" | "verification_completion_rate" | "retry_recovery_rate" | "init_time_ms_p50" | "init_time_ms_p95" | "verify_time_ms_p50" | "verify_time_ms_p95" | "paid_fee_rate"; - cohortMedian: string; - /** @description Higher always means better; lower-is-better metrics are direction-adjusted. */ - merchantPerformancePercentile: string; - /** @enum {string} */ - direction: "HIGHER_IS_BETTER" | "LOWER_IS_BETTER"; - }; - PeerComparisonData: { - eligiblePeerCount: number; - combinedPeerSessionCount: number; - metrics: components["schemas"]["PeerMetric"][]; - }; - OverviewReadySection: { - /** @constant */ - sectionId: "overview"; - /** @constant */ - state: "READY"; - metadata: components["schemas"]["ReadySectionMetadata"]; - data: components["schemas"]["OverviewData"]; - }; - PaymentPerformanceReadySection: { - /** @constant */ - sectionId: "payment_performance"; - /** @constant */ - state: "READY"; - metadata: components["schemas"]["ReadySectionMetadata"]; - data: components["schemas"]["PaymentPerformanceData"]; - }; - FeesReadySection: { - /** @constant */ - sectionId: "fees"; - /** @constant */ - state: "READY"; - metadata: components["schemas"]["ReadySectionMetadata"]; - data: components["schemas"]["FeesData"]; - }; - PeerComparisonReadySection: { - /** @constant */ - sectionId: "peer_comparison"; - /** @constant */ - state: "READY"; - metadata: components["schemas"]["ReadySectionMetadata"]; - data: components["schemas"]["PeerComparisonData"]; - }; - PeerComparisonSuppressedSection: { - /** @constant */ - sectionId: "peer_comparison"; - /** @constant */ - state: "SUPPRESSED"; - metadata: components["schemas"]["ReadySectionMetadata"]; - /** - * @description `TERMINAL_SCOPE_UNSUPPORTED` is returned for terminal-scoped runs because - * the approved peer cohort is merchant-grain and must not be presented as - * if it benchmarked one terminal. - * @enum {string} - */ - reason: "INSUFFICIENT_COHORT" | "TERMINAL_SCOPE_UNSUPPORTED"; - eligiblePeerCount: number; - combinedPeerSessionCount: number; - /** @constant */ - requiredPeerCount: 10; - /** @constant */ - requiredPeerSessionCount: 1000; - }; - OverviewSection: components["schemas"]["QueuedSection"] | components["schemas"]["CalculatingSection"] | components["schemas"]["FailedSection"] | components["schemas"]["OverviewReadySection"]; - PaymentPerformanceSection: components["schemas"]["QueuedSection"] | components["schemas"]["CalculatingSection"] | components["schemas"]["FailedSection"] | components["schemas"]["PaymentPerformanceReadySection"]; - FeesSection: components["schemas"]["QueuedSection"] | components["schemas"]["CalculatingSection"] | components["schemas"]["FailedSection"] | components["schemas"]["FeesReadySection"]; - PeerComparisonSection: components["schemas"]["QueuedSection"] | components["schemas"]["CalculatingSection"] | components["schemas"]["FailedSection"] | components["schemas"]["PeerComparisonReadySection"] | components["schemas"]["PeerComparisonSuppressedSection"]; - /** @enum {string} */ - DetailView: "ALL" | "UNVERIFIED_PAID" | "FAILED" | "RETRIED"; - /** @enum {string} */ - FailureStage: "NO_ATTEMPT" | "REACHED_BANK" | "OTHER_OBSERVED"; - /** @enum {string} */ - SessionStatus: "Paid" | "Verified" | "Failed" | "Reversed"; - /** @enum {string} */ - AttemptStatus: "InBank" | "Paid" | "Verified" | "Failed" | "Reversed"; - AttemptSummary: { - sequence: number; - status: components["schemas"]["AttemptStatus"]; - /** Format: date-time */ - createdAt?: string | null; - initTimeMs?: number | null; - verifyTimeMs?: number | null; - }; - SessionDetail: { - /** @description Opaque merchant-local API id, not the internal database key. */ - sessionId: string; - /** @description Existing opaque source terminal reference shown as `شناسه پایانه`; tenant-scoped display/trace data only. */ - terminalId: string; - sessionStatus: components["schemas"]["SessionStatus"]; - amountRial: number; - feeAmountRial: number; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - expiresAt?: string | null; - /** Format: date-time */ - verifiedAt?: string | null; - /** Format: date-time */ - settledAt?: string | null; - attemptCount: number; - failureStage?: components["schemas"]["FailureStage"] | null; - /** @description Bounded ordered attempt summary; payer tokens and response codes are absent. */ - attempts: components["schemas"]["AttemptSummary"][]; - }; - PageInfo: { - limit: number; - hasNextPage: boolean; - nextCursor: string | null; - }; - SessionDetailPage: { - period: components["schemas"]["ReportingPeriod"]; - view: components["schemas"]["DetailView"]; - appliedFilters: { - terminalId?: string | null; - failureStage?: components["schemas"]["FailureStage"] | null; - status?: components["schemas"]["SessionStatus"] | null; - /** Format: int64 */ - minAmountRial?: number | null; - /** Format: int64 */ - maxAmountRial?: number | null; - }; - items: components["schemas"]["SessionDetail"][]; - pageInfo: components["schemas"]["PageInfo"]; - }; - EventId: string; - SseEventBase: { - eventId: components["schemas"]["EventId"]; - runId: components["schemas"]["RunId"]; - /** Format: date-time */ - occurredAt: string; - }; - RunSnapshotEvent: components["schemas"]["SseEventBase"] & { - /** @enum {string} */ - recovery: "INITIAL" | "REPLAYED" | "RESET_TO_SNAPSHOT"; - run: components["schemas"]["AnalyticsRun"]; - }; - SectionStartedEvent: components["schemas"]["SseEventBase"] & { - sectionId: components["schemas"]["SectionId"]; - /** Format: date-time */ - startedAt: string; - }; - SectionReadyEvent: components["schemas"]["SseEventBase"] & { - sectionId: components["schemas"]["SectionId"]; - section: components["schemas"]["OverviewReadySection"] | components["schemas"]["PaymentPerformanceReadySection"] | components["schemas"]["FeesReadySection"] | components["schemas"]["PeerComparisonReadySection"]; - }; - SectionFailedEvent: components["schemas"]["SseEventBase"] & { - sectionId: components["schemas"]["SectionId"]; - section: components["schemas"]["FailedSection"]; - }; - SectionSuppressedEvent: components["schemas"]["SseEventBase"] & { - /** @constant */ - sectionId: "peer_comparison"; - section: components["schemas"]["PeerComparisonSuppressedSection"]; - }; - RunCompletedEvent: components["schemas"]["SseEventBase"] & { - run: components["schemas"]["AnalyticsRun"]; - }; - HeartbeatEvent: components["schemas"]["SseEventBase"] & { - /** Format: date-time */ - serverTime: string; - }; - /** @description JSON payload carried in the SSE `data` field; event name selects the schema. */ - AnalyticsSseEvent: components["schemas"]["RunSnapshotEvent"] | components["schemas"]["SectionStartedEvent"] | components["schemas"]["SectionReadyEvent"] | components["schemas"]["SectionFailedEvent"] | components["schemas"]["SectionSuppressedEvent"] | components["schemas"]["RunCompletedEvent"] | components["schemas"]["HeartbeatEvent"]; - FieldError: { - /** @description Allowlisted public field name; never a database path. */ - field: string; - reason: string; - }; - /** @description RFC 9457 Problem Details with stable application extensions. */ - Problem: { - /** - * Format: uri - * @description Stable problem type URI under `https://zarinpal.amiirkhl.ir/docs/errors/{error-slug}`. - */ - type: string; - title: string; - /** @description Must equal the actual HTTP response status. */ - status: number; - /** @description Safe occurrence-specific guidance; clients must not parse it. */ - detail: string; - /** Format: uri-reference */ - instance: string; - /** @description Stable application error code used by clients. */ - code: string; - requestId: string; - errors?: components["schemas"]["FieldError"][]; - /** - * Format: uri-reference - * @description Optional safe recovery link for run-related problems. - */ - snapshotUrl?: string; - }; + schemas: { + CreateGrowthReportRequest: { + terminalId?: string; + /** @default false */ + refresh: boolean; + }; + GrowthRange: { + /** Format: int64 */ + p10: number; + /** Format: int64 */ + p50: number; + /** Format: int64 */ + p90: number; + }; + GrowthForecastPoint: { + /** Format: date-time */ + date: string; + /** Format: int64 */ + baselineSessions: number; + /** Format: int64 */ + baselineAmountRial: number; + paidSessions: components['schemas']['GrowthRange']; + paidAmountRial: components['schemas']['GrowthRange']; + eventIds?: string[]; + }; + GrowthForecast: { + /** Format: date-time */ + startDate: string; + /** @enum {integer} */ + horizonDays: 90; + /** @enum {string} */ + confidence: 'HIGH' | 'MEDIUM' | 'INSUFFICIENT_DATA'; + suppressionReason?: string; + points: components['schemas']['GrowthForecastPoint'][]; + backtest: { + origins: number; + /** Format: int64 */ + sessionMapeBps: number; + /** Format: int64 */ + amountMapeBps: number; + /** Format: int64 */ + intervalCoverageBps: number; + }; + }; + GrowthHistoricalWin: { + evidenceId: string; + eventId: string; + eventTitleFa: string; + /** Format: date-time */ + startDate: string; + /** Format: date-time */ + endDate: string; + eligible?: boolean; + suppressionReason?: string; + observationCount?: number; + /** Format: int64 */ + baselineSessionCount?: number; + /** Format: int64 */ + baselinePaidSessions?: number; + /** Format: int64 */ + baselineAmountRial?: number; + /** Format: int64 */ + actualSessionCount?: number; + /** Format: int64 */ + actualPaidSessions?: number; + /** Format: int64 */ + actualAmountRial?: number; + /** Format: int64 */ + sessionUpliftBps?: number; + /** Format: int64 */ + paidSessionUpliftBps?: number; + /** Format: int64 */ + amountUpliftBps?: number; + materialWin: boolean; + materialDecline?: boolean; + }; + GrowthOpportunity: { + evidenceId: string; + eventId: string; + eventTitleFa: string; + /** Format: date-time */ + date: string; + /** Format: int64 */ + similarityBps: number; + expectedSessions: components['schemas']['GrowthRange']; + expectedAmountRial: components['schemas']['GrowthRange']; + historicalEvidenceId: string; + }; + GrowthRisk: { + evidenceId: string; + /** Format: date-time */ + date: string; + /** Format: date-time */ + endDate?: string; + /** + * Format: int64 + * @description At least one decline dimension is 2000 BPS or greater. + */ + sessionDeclineBps: number; + /** + * Format: int64 + * @description At least one decline dimension is 2000 BPS or greater. + */ + amountDeclineBps: number; + expectedSessions: components['schemas']['GrowthRange']; + expectedAmountRial: components['schemas']['GrowthRange']; + }; + GrowthPeer: { + /** @enum {string} */ + state: 'READY' | 'SUPPRESSED'; + reason?: string; + eligiblePeerCount: number; + /** Format: int64 */ + combinedPeerSessionCount: number; + /** Format: int64 */ + medianPaidSessions: number; + /** Format: int64 */ + percentileBps: number; + }; + GrowthCampaignBrief: { + headline: string; + interpretation: string; + offerSuggestion: string; + messageSuggestion: string; + preparationAction: string; + limitation: string; + evidenceIds: string[]; + }; + GrowthFallbackBrief: { + headlineFa: string; + guidanceFa: string; + offerFa: string; + messageFa: string; + preparationFa: string; + limitationFa: string; + evidenceIds: string[]; + }; + GrowthEvidence: { + forecast: components['schemas']['GrowthForecast']; + historicalWins: components['schemas']['GrowthHistoricalWin'][]; + opportunities: components['schemas']['GrowthOpportunity'][]; + risks: components['schemas']['GrowthRisk'][]; + peer?: components['schemas']['GrowthPeer']; + fallbackBriefs: components['schemas']['GrowthFallbackBrief'][]; + }; + GrowthReport: { + reportId: string; + revision: number; + /** @enum {string} */ + status: 'QUEUED' | 'ANALYZING' | 'GENERATING' | 'READY' | 'PARTIAL' | 'FAILED'; + scope: { + terminalId?: string | null; + }; + /** @enum {integer} */ + horizonDays: 90; + /** Format: date-time */ + forecastStartDate?: string; + deterministicEvidence?: components['schemas']['GrowthEvidence']; + aiCopy?: { + briefs: components['schemas']['GrowthCampaignBrief'][]; + }; + failureCode?: string; + cached: boolean; + stale: boolean; + /** Format: date-time */ + nextRefreshAt: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + /** Format: uri-reference */ + eventsUrl: string; + }; + LoginRequest: { + /** @example merchant-0001 */ + username: string; + password: string; + }; + AuthSession: { + user: components['schemas']['MerchantUserSummary']; + merchant: components['schemas']['MerchantSummary']; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + idleExpiresAt: string; + /** Format: date-time */ + absoluteExpiresAt: string; + /** @description Session-bound CSRF token. It is not an authentication token and must not be logged. */ + csrfToken: string; + }; + MerchantUserSummary: { + userId: string; + /** @example merchant-0001 */ + username: string; + }; + MerchantSummary: { + /** @description Opaque API identifier, never accepted as authorization input. */ + merchantId: string; + displayName: string; + profileIsSynthetic: boolean; + }; + /** @enum {string} */ + PeriodType: 'DAY' | 'WEEK' | 'JALALI_MONTH' | 'PERSIAN_SEASON'; + /** @example jm-1405-05 */ + PeriodId: string; + ReportingPeriod: { + periodId: components['schemas']['PeriodId']; + periodType: components['schemas']['PeriodType']; + /** + * Format: date-time + * @description Inclusive UTC boundary. + * @example 2026-07-23T20:30:00Z + */ + startAt: string; + /** + * Format: date-time + * @description Exclusive UTC boundary. + * @example 2026-08-22T20:30:00Z + */ + endAt: string; + /** + * Format: date-time + * @description Inclusive Asia/Tehran civil boundary with explicit offset. + * @example 2026-07-24T00:00:00+03:30 + */ + localStartAt: string; + /** + * Format: date-time + * @description Exclusive Asia/Tehran civil boundary with explicit offset. + * @example 2026-08-23T00:00:00+03:30 + */ + localEndAt: string; + /** @constant */ + timezone: 'Asia/Tehran'; + /** @example مرداد ۱۴۰۵ */ + jalaliLabelFa: string; + isComplete: boolean; + isDefault: boolean; + }; + PeriodList: { + defaultPeriodId: components['schemas']['PeriodId']; + /** @description Dataset-bounded complete periods, grouped by type and newest-first within each group. */ + periods: components['schemas']['ReportingPeriod'][]; + }; + CreateAnalyticsRunRequest: { + periodId: components['schemas']['PeriodId']; + terminalId?: string; + }; + Terminal: { + terminalId: string; + displayName: string; + /** Format: int64 */ + sessionCount: number; + isDefault: boolean; + }; + TerminalList: { + terminals: components['schemas']['Terminal'][]; + }; + AnalyticsScope: { + terminalId: string | null; + }; + /** @example run_01K2M7YYYYYYYYYYYYYYYYYYYY */ + RunId: string; + /** @enum {string} */ + SectionId: 'overview' | 'payment_performance' | 'fees' | 'peer_comparison'; + /** @enum {string} */ + AnalyticsRunState: 'IN_PROGRESS' | 'COMPLETE' | 'COMPLETE_WITH_FAILURES'; + /** @enum {string} */ + SectionState: 'QUEUED' | 'CALCULATING' | 'READY' | 'FAILED' | 'SUPPRESSED'; + AnalyticsRun: { + runId: components['schemas']['RunId']; + state: components['schemas']['AnalyticsRunState']; + period: components['schemas']['ReportingPeriod']; + /** @description Opaque identifier for the immutable completed-import snapshot. */ + datasetSnapshotId: string; + /** @constant */ + metricDefinitionVersion: '0.4.0'; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + /** Format: uri-reference */ + eventsUrl: string; + scope: components['schemas']['AnalyticsScope']; + sections: { + overview: components['schemas']['OverviewSection']; + paymentPerformance: components['schemas']['PaymentPerformanceSection']; + fees: components['schemas']['FeesSection']; + peerComparison: components['schemas']['PeerComparisonSection']; + }; + }; + QueuedSection: { + /** @constant */ + state: 'QUEUED'; + /** Format: date-time */ + queuedAt?: string | null; + }; + CalculatingSection: { + /** @constant */ + state: 'CALCULATING'; + /** Format: date-time */ + startedAt: string; + }; + FailedSection: { + /** @constant */ + state: 'FAILED'; + /** Format: date-time */ + failedAt: string; + /** @enum {string} */ + errorCode: + | 'CALCULATION_FAILED' + | 'CALCULATION_TIMEOUT' + | 'SOURCE_UNAVAILABLE' + | 'UNSUPPORTED_METRIC_VERSION'; + canRetry: boolean; + /** Format: uri-reference */ + retryAttemptsUrl?: string | null; + }; + ReadySectionMetadata: { + /** Format: date-time */ + calculatedAt: string; + datasetSnapshotId: string; + /** @constant */ + metricDefinitionVersion: '0.4.0'; + }; + IntegerMetric: { + metricId: string; + value: number; + /** @enum {string} */ + unit: 'SESSIONS' | 'RIAL'; + }; + DecimalMetric: { + metricId: string; + /** @description Null only when the denominator is zero or source is unavailable. */ + value: string | null; + /** @enum {string} */ + unit: 'PERCENT' | 'RATIO'; + numerator: number; + denominator: number; + }; + TimingDistribution: { + /** @enum {string} */ + metricId: 'init_time_ms' | 'verify_time_ms'; + p50Ms: number | null; + p95Ms: number | null; + sampleSize: number; + eligibleSampleSize: number; + coveragePercent: string | null; + }; + PreviousPeriodComparison: { + /** @enum {string} */ + state: 'AVAILABLE' | 'UNAVAILABLE'; + baselinePeriod: components['schemas']['ReportingPeriod']; + absoluteChange?: string | null; + relativeChangePercent?: string | null; + /** @enum {string|null} */ + unavailableReason?: 'EMPTY_BASELINE' | 'ZERO_BASELINE' | 'SOURCE_UNAVAILABLE' | null; + isActionableInsightEligible?: boolean; + }; + OverviewData: { + /** + * @example { + * "metricId": "bank_paid_sessions", + * "value": 420, + * "unit": "SESSIONS" + * } + */ + bankPaidSessions: components['schemas']['IntegerMetric']; + /** + * @example { + * "metricId": "bank_paid_amount_rial", + * "value": 680000000, + * "unit": "RIAL" + * } + */ + bankPaidAmountRial: components['schemas']['IntegerMetric']; + paymentSuccessRate: components['schemas']['DecimalMetric']; + verificationCompletionRate: components['schemas']['DecimalMetric']; + comparisons: { + [key: string]: components['schemas']['PreviousPeriodComparison']; + }; + paymentHistory: components['schemas']['PaymentHistoryPoint'][]; + }; + PaymentHistoryPoint: { + /** Format: date */ + localDate: string; + /** Format: int64 */ + totalSessions: number; + /** Format: int64 */ + bankPaidSessions: number; + /** Format: int64 */ + bankPaidAmountRial: number; + }; + PaymentPerformanceData: { + bankPaidSessions: components['schemas']['IntegerMetric']; + verifiedSessions: components['schemas']['IntegerMetric']; + unverifiedPaidSessions: components['schemas']['IntegerMetric']; + failedSessions: components['schemas']['IntegerMetric']; + reversedSessions: components['schemas']['IntegerMetric']; + noAttemptSessions: components['schemas']['IntegerMetric']; + reachedBankSessions: components['schemas']['IntegerMetric']; + retriedSessions: components['schemas']['IntegerMetric']; + retryRecoveryRate: components['schemas']['DecimalMetric']; + initTiming: components['schemas']['TimingDistribution']; + verifyTiming: components['schemas']['TimingDistribution']; + }; + FeesData: { + paidFeeAmountRial: components['schemas']['IntegerMetric']; + bankPaidAmountRial: components['schemas']['IntegerMetric']; + paidFeeRate: components['schemas']['DecimalMetric']; + comparison: components['schemas']['PreviousPeriodComparison']; + }; + PeerMetric: { + /** @enum {string} */ + metricId: + | 'payment_success_rate' + | 'verification_completion_rate' + | 'retry_recovery_rate' + | 'init_time_ms_p50' + | 'init_time_ms_p95' + | 'verify_time_ms_p50' + | 'verify_time_ms_p95' + | 'paid_fee_rate'; + cohortMedian: string; + /** @description Higher always means better; lower-is-better metrics are direction-adjusted. */ + merchantPerformancePercentile: string; + /** @enum {string} */ + direction: 'HIGHER_IS_BETTER' | 'LOWER_IS_BETTER'; + }; + PeerComparisonData: { + eligiblePeerCount: number; + combinedPeerSessionCount: number; + metrics: components['schemas']['PeerMetric'][]; + }; + OverviewReadySection: { + /** @constant */ + sectionId: 'overview'; + /** @constant */ + state: 'READY'; + metadata: components['schemas']['ReadySectionMetadata']; + data: components['schemas']['OverviewData']; + }; + PaymentPerformanceReadySection: { + /** @constant */ + sectionId: 'payment_performance'; + /** @constant */ + state: 'READY'; + metadata: components['schemas']['ReadySectionMetadata']; + data: components['schemas']['PaymentPerformanceData']; + }; + FeesReadySection: { + /** @constant */ + sectionId: 'fees'; + /** @constant */ + state: 'READY'; + metadata: components['schemas']['ReadySectionMetadata']; + data: components['schemas']['FeesData']; + }; + PeerComparisonReadySection: { + /** @constant */ + sectionId: 'peer_comparison'; + /** @constant */ + state: 'READY'; + metadata: components['schemas']['ReadySectionMetadata']; + data: components['schemas']['PeerComparisonData']; + }; + PeerComparisonSuppressedSection: { + /** @constant */ + sectionId: 'peer_comparison'; + /** @constant */ + state: 'SUPPRESSED'; + metadata: components['schemas']['ReadySectionMetadata']; + /** + * @description `TERMINAL_SCOPE_UNSUPPORTED` is returned for terminal-scoped runs because + * the approved peer cohort is merchant-grain and must not be presented as + * if it benchmarked one terminal. + * @enum {string} + */ + reason: 'INSUFFICIENT_COHORT' | 'TERMINAL_SCOPE_UNSUPPORTED'; + eligiblePeerCount: number; + combinedPeerSessionCount: number; + /** @constant */ + requiredPeerCount: 10; + /** @constant */ + requiredPeerSessionCount: 1000; + }; + OverviewSection: + | components['schemas']['QueuedSection'] + | components['schemas']['CalculatingSection'] + | components['schemas']['FailedSection'] + | components['schemas']['OverviewReadySection']; + PaymentPerformanceSection: + | components['schemas']['QueuedSection'] + | components['schemas']['CalculatingSection'] + | components['schemas']['FailedSection'] + | components['schemas']['PaymentPerformanceReadySection']; + FeesSection: + | components['schemas']['QueuedSection'] + | components['schemas']['CalculatingSection'] + | components['schemas']['FailedSection'] + | components['schemas']['FeesReadySection']; + PeerComparisonSection: + | components['schemas']['QueuedSection'] + | components['schemas']['CalculatingSection'] + | components['schemas']['FailedSection'] + | components['schemas']['PeerComparisonReadySection'] + | components['schemas']['PeerComparisonSuppressedSection']; + /** @enum {string} */ + DetailView: 'ALL' | 'UNVERIFIED_PAID' | 'FAILED' | 'RETRIED'; + /** @enum {string} */ + FailureStage: 'NO_ATTEMPT' | 'REACHED_BANK' | 'OTHER_OBSERVED'; + /** @enum {string} */ + SessionStatus: 'Paid' | 'Verified' | 'Failed' | 'Reversed'; + /** @enum {string} */ + AttemptStatus: 'InBank' | 'Paid' | 'Verified' | 'Failed' | 'Reversed'; + AttemptSummary: { + sequence: number; + status: components['schemas']['AttemptStatus']; + /** Format: date-time */ + createdAt?: string | null; + initTimeMs?: number | null; + verifyTimeMs?: number | null; + }; + SessionDetail: { + /** @description Opaque merchant-local API id, not the internal database key. */ + sessionId: string; + /** @description Existing opaque source terminal reference shown as `شناسه پایانه`; tenant-scoped display/trace data only. */ + terminalId: string; + sessionStatus: components['schemas']['SessionStatus']; + amountRial: number; + feeAmountRial: number; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + expiresAt?: string | null; + /** Format: date-time */ + verifiedAt?: string | null; + /** Format: date-time */ + settledAt?: string | null; + attemptCount: number; + failureStage?: components['schemas']['FailureStage'] | null; + /** @description Bounded ordered attempt summary; payer tokens and response codes are absent. */ + attempts: components['schemas']['AttemptSummary'][]; + }; + PageInfo: { + limit: number; + hasNextPage: boolean; + nextCursor: string | null; + }; + SessionDetailPage: { + period: components['schemas']['ReportingPeriod']; + view: components['schemas']['DetailView']; + appliedFilters: { + terminalId?: string | null; + failureStage?: components['schemas']['FailureStage'] | null; + status?: components['schemas']['SessionStatus'] | null; + /** Format: int64 */ + minAmountRial?: number | null; + /** Format: int64 */ + maxAmountRial?: number | null; + }; + items: components['schemas']['SessionDetail'][]; + pageInfo: components['schemas']['PageInfo']; + }; + EventId: string; + SseEventBase: { + eventId: components['schemas']['EventId']; + runId: components['schemas']['RunId']; + /** Format: date-time */ + occurredAt: string; + }; + RunSnapshotEvent: components['schemas']['SseEventBase'] & { + /** @enum {string} */ + recovery: 'INITIAL' | 'REPLAYED' | 'RESET_TO_SNAPSHOT'; + run: components['schemas']['AnalyticsRun']; + }; + SectionStartedEvent: components['schemas']['SseEventBase'] & { + sectionId: components['schemas']['SectionId']; + /** Format: date-time */ + startedAt: string; + }; + SectionReadyEvent: components['schemas']['SseEventBase'] & { + sectionId: components['schemas']['SectionId']; + section: + | components['schemas']['OverviewReadySection'] + | components['schemas']['PaymentPerformanceReadySection'] + | components['schemas']['FeesReadySection'] + | components['schemas']['PeerComparisonReadySection']; + }; + SectionFailedEvent: components['schemas']['SseEventBase'] & { + sectionId: components['schemas']['SectionId']; + section: components['schemas']['FailedSection']; + }; + SectionSuppressedEvent: components['schemas']['SseEventBase'] & { + /** @constant */ + sectionId: 'peer_comparison'; + section: components['schemas']['PeerComparisonSuppressedSection']; + }; + RunCompletedEvent: components['schemas']['SseEventBase'] & { + run: components['schemas']['AnalyticsRun']; + }; + HeartbeatEvent: components['schemas']['SseEventBase'] & { + /** Format: date-time */ + serverTime: string; + }; + /** @description JSON payload carried in the SSE `data` field; event name selects the schema. */ + AnalyticsSseEvent: + | components['schemas']['RunSnapshotEvent'] + | components['schemas']['SectionStartedEvent'] + | components['schemas']['SectionReadyEvent'] + | components['schemas']['SectionFailedEvent'] + | components['schemas']['SectionSuppressedEvent'] + | components['schemas']['RunCompletedEvent'] + | components['schemas']['HeartbeatEvent']; + FieldError: { + /** @description Allowlisted public field name; never a database path. */ + field: string; + reason: string; + }; + /** @description RFC 9457 Problem Details with stable application extensions. */ + Problem: { + /** + * Format: uri + * @description Stable problem type URI under `https://zarinpal.amiirkhl.ir/docs/errors/{error-slug}`. + */ + type: string; + title: string; + /** @description Must equal the actual HTTP response status. */ + status: number; + /** @description Safe occurrence-specific guidance; clients must not parse it. */ + detail: string; + /** Format: uri-reference */ + instance: string; + /** @description Stable application error code used by clients. */ + code: string; + requestId: string; + errors?: components['schemas']['FieldError'][]; + /** + * Format: uri-reference + * @description Optional safe recovery link for run-related problems. + */ + snapshotUrl?: string; + /** + * Format: date-time + * @description Present for REFRESH_LIMITED responses. + */ + nextRefreshAt?: string; + }; + }; + responses: { + /** @description Request syntax, cursor, event id, or parameter combination is invalid. */ + BadRequest: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description The current session is missing, malformed, expired, or revoked. */ + Unauthenticated: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + /** + * @example { + * "type": "https://zarinpal.amiirkhl.ir/docs/errors/unauthenticated", + * "title": "Authentication required", + * "status": 401, + * "detail": "Sign in to continue.", + * "instance": "/api/v1/analytics/runs", + * "code": "UNAUTHENTICATED", + * "requestId": "req_01K2M8A0000000000000000000" + * } + */ + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description The authenticated request is not permitted or failed CSRF/origin checks. */ + Forbidden: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Resource is absent or is outside the authenticated merchant boundary. */ + ResourceNotFound: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + /** + * @example { + * "type": "https://zarinpal.amiirkhl.ir/docs/errors/resource-not-found", + * "title": "Resource not found", + * "status": 404, + * "detail": "The requested resource is unavailable.", + * "instance": "/api/v1/analytics/runs/run_unknown", + * "code": "RESOURCE_NOT_FOUND", + * "requestId": "req_01K2M8A0000000000000000000" + * } + */ + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Current resource state does not permit the requested operation. */ + Conflict: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Request exceeds the 16 KiB JSON body limit. */ + RequestTooLarge: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Export exceeds 100,000 rows or 50 MiB estimated uncompressed output. */ + ExportTooLarge: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + /** + * @example { + * "type": "https://zarinpal.amiirkhl.ir/docs/errors/export-too-large", + * "title": "Export is too large", + * "status": 413, + * "detail": "Narrow the approved filters and try again.", + * "instance": "/api/v1/analytics/session-details.csv", + * "code": "EXPORT_TOO_LARGE", + * "requestId": "req_01K2M8A0000000000000000000" + * } + */ + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Request media type is not supported. */ + UnsupportedMediaType: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Request is well-formed but violates the interface contract. */ + ValidationFailed: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + /** + * @example { + * "type": "https://zarinpal.amiirkhl.ir/docs/errors/validation-error", + * "title": "Request validation failed", + * "status": 422, + * "detail": "One or more request values are invalid.", + * "instance": "/api/v1/analytics/runs", + * "code": "VALIDATION_ERROR", + * "requestId": "req_01K2M8A0000000000000000000", + * "errors": [ + * { + * "field": "periodId", + * "reason": "UNKNOWN_PERIOD" + * } + * ] + * } + */ + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Authenticated-user or merchant rate limit reached before work began. */ + RateLimited: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'Retry-After': components['headers']['RetryAfter']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description SSE concurrency limit reached for the user or merchant. */ + ConnectionLimited: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'Retry-After': components['headers']['RetryAfter']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Unexpected server failure; implementation details are not exposed. */ + InternalError: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Required authoritative dependency is unavailable; protected requests fail closed. */ + DependencyUnavailable: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'Retry-After': components['headers']['RetryAfter']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Analytics dependency is temporarily unavailable. */ + AnalyticsUnavailable: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'Retry-After': components['headers']['RetryAfter']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + /** @description Export did not complete within the 60-second deadline before streaming began. */ + ExportTimeout: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'Retry-After': components['headers']['RetryAfter']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/problem+json': components['schemas']['Problem']; + }; + }; + }; + parameters: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + RunIdPath: components['schemas']['RunId']; + SectionIdPath: components['schemas']['SectionId']; + /** + * @description Client-generated intent key reused unchanged for retries of one intent. + * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b + */ + IdempotencyKeyHeader: string; + /** @description CSRF token for the current authenticated session; never log this value. */ + CsrfTokenHeader: string; + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + OriginHeader: string; + /** @description Optional safe correlation id. The server may replace invalid values. */ + RequestIdHeader: string; + /** @description Stable id returned by `listAnalyticsPeriods`. */ + PeriodIdQuery: components['schemas']['PeriodId']; + DetailViewQuery: components['schemas']['DetailView']; + /** @description Valid only when `view=FAILED`. */ + FailureStageQuery: components['schemas']['FailureStage']; + /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ + TerminalIdQuery: string; + SessionStatusQuery: components['schemas']['SessionStatus']; + MinAmountRialQuery: number; + MaxAmountRialQuery: number; + /** @description Opaque integrity-protected cursor bound to tenant, view, filters, and ordering. */ + CursorQuery: string; + /** @description Page size; defaults to 25 and cannot exceed 100. */ + LimitQuery: number; + }; + requestBodies: never; + headers: { + /** @description Server correlation id for this request. */ + RequestId: string; + /** @description Prevents storage of authenticated merchant data. */ + PrivateNoStore: 'private, no-store'; + /** @description Seconds until retry is permitted. */ + RetryAfter: number; + }; + pathItems: never; +} +export type $defs = Record; +export interface operations { + createAuthSession: { + parameters: { + query?: never; + header: { + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + Origin: components['parameters']['OriginHeader']; + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['LoginRequest']; + }; }; responses: { - /** @description Request syntax, cursor, event id, or parameter combination is invalid. */ - BadRequest: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; + /** @description Authenticated session created. */ + 201: { + headers: { + /** + * @description Secure opaque session cookie; value is intentionally omitted from examples. + * @example __Host-zpa_session=; Path=/; Secure; HttpOnly; SameSite=Lax + */ + 'Set-Cookie'?: string; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; }; - /** @description The current session is missing, malformed, expired, or revoked. */ - Unauthenticated: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - /** - * @example { - * "type": "https://zarinpal.amiirkhl.ir/docs/errors/unauthenticated", - * "title": "Authentication required", - * "status": 401, - * "detail": "Sign in to continue.", - * "instance": "/api/v1/analytics/runs", - * "code": "UNAUTHENTICATED", - * "requestId": "req_01K2M8A0000000000000000000" - * } - */ - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description The authenticated request is not permitted or failed CSRF/origin checks. */ - Forbidden: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Resource is absent or is outside the authenticated merchant boundary. */ - ResourceNotFound: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - /** - * @example { - * "type": "https://zarinpal.amiirkhl.ir/docs/errors/resource-not-found", - * "title": "Resource not found", - * "status": 404, - * "detail": "The requested resource is unavailable.", - * "instance": "/api/v1/analytics/runs/run_unknown", - * "code": "RESOURCE_NOT_FOUND", - * "requestId": "req_01K2M8A0000000000000000000" - * } - */ - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Current resource state does not permit the requested operation. */ - Conflict: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Request exceeds the 16 KiB JSON body limit. */ - RequestTooLarge: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Export exceeds 100,000 rows or 50 MiB estimated uncompressed output. */ - ExportTooLarge: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - /** - * @example { - * "type": "https://zarinpal.amiirkhl.ir/docs/errors/export-too-large", - * "title": "Export is too large", - * "status": 413, - * "detail": "Narrow the approved filters and try again.", - * "instance": "/api/v1/analytics/session-details.csv", - * "code": "EXPORT_TOO_LARGE", - * "requestId": "req_01K2M8A0000000000000000000" - * } - */ - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Request media type is not supported. */ - UnsupportedMediaType: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Request is well-formed but violates the interface contract. */ - ValidationFailed: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - /** - * @example { - * "type": "https://zarinpal.amiirkhl.ir/docs/errors/validation-error", - * "title": "Request validation failed", - * "status": 422, - * "detail": "One or more request values are invalid.", - * "instance": "/api/v1/analytics/runs", - * "code": "VALIDATION_ERROR", - * "requestId": "req_01K2M8A0000000000000000000", - * "errors": [ - * { - * "field": "periodId", - * "reason": "UNKNOWN_PERIOD" - * } - * ] - * } - */ - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Authenticated-user or merchant rate limit reached before work began. */ - RateLimited: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "Retry-After": components["headers"]["RetryAfter"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description SSE concurrency limit reached for the user or merchant. */ - ConnectionLimited: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "Retry-After": components["headers"]["RetryAfter"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Unexpected server failure; implementation details are not exposed. */ - InternalError: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Required authoritative dependency is unavailable; protected requests fail closed. */ - DependencyUnavailable: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "Retry-After": components["headers"]["RetryAfter"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Analytics dependency is temporarily unavailable. */ - AnalyticsUnavailable: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "Retry-After": components["headers"]["RetryAfter"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; - }; - /** @description Export did not complete within the 60-second deadline before streaming began. */ - ExportTimeout: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "Retry-After": components["headers"]["RetryAfter"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["Problem"]; - }; + content: { + 'application/json': components['schemas']['AuthSession']; }; + }; + 400: components['responses']['BadRequest']; + 401: components['responses']['Unauthenticated']; + 413: components['responses']['RequestTooLarge']; + 415: components['responses']['UnsupportedMediaType']; + 422: components['responses']['ValidationFailed']; + 429: components['responses']['RateLimited']; + 500: components['responses']['InternalError']; + 503: components['responses']['DependencyUnavailable']; }; + }; + getAuthSession: { parameters: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - RunIdPath: components["schemas"]["RunId"]; - SectionIdPath: components["schemas"]["SectionId"]; + query?: never; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current authenticated merchant-user session. */ + 200: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AuthSession']; + }; + }; + 401: components['responses']['Unauthenticated']; + 500: components['responses']['InternalError']; + 503: components['responses']['DependencyUnavailable']; + }; + }; + deleteAuthSession: { + parameters: { + query?: never; + header: { + /** @description CSRF token for the current authenticated session; never log this value. */ + 'X-CSRF-Token': components['parameters']['CsrfTokenHeader']; + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + Origin: components['parameters']['OriginHeader']; + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current authenticated session revoked. */ + 204: { + headers: { + /** + * @description Expired session cookie. + * @example __Host-zpa_session=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax + */ + 'Set-Cookie'?: string; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content?: never; + }; + 401: components['responses']['Unauthenticated']; + 403: components['responses']['Forbidden']; + 429: components['responses']['RateLimited']; + 500: components['responses']['InternalError']; + 503: components['responses']['DependencyUnavailable']; + }; + }; + listAnalyticsPeriods: { + parameters: { + query?: { + /** @description Optional allowlisted period type. */ + periodType?: components['schemas']['PeriodType']; + }; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Approved period choices and default selection. */ + 200: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PeriodList']; + }; + }; + 400: components['responses']['BadRequest']; + 401: components['responses']['Unauthenticated']; + 422: components['responses']['ValidationFailed']; + 429: components['responses']['RateLimited']; + 500: components['responses']['InternalError']; + 503: components['responses']['DependencyUnavailable']; + }; + }; + listMerchantTerminals: { + parameters: { + query?: never; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Merchant-owned terminal choices ordered by session volume. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['TerminalList']; + }; + }; + 401: components['responses']['Unauthenticated']; + 503: components['responses']['AnalyticsUnavailable']; + }; + }; + createAnalyticsRun: { + parameters: { + query?: never; + header: { /** * @description Client-generated intent key reused unchanged for retries of one intent. * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b */ - IdempotencyKeyHeader: string; + 'Idempotency-Key': components['parameters']['IdempotencyKeyHeader']; /** @description CSRF token for the current authenticated session; never log this value. */ - CsrfTokenHeader: string; + 'X-CSRF-Token': components['parameters']['CsrfTokenHeader']; /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - OriginHeader: string; + Origin: components['parameters']['OriginHeader']; /** @description Optional safe correlation id. The server may replace invalid values. */ - RequestIdHeader: string; + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateAnalyticsRunRequest']; + }; + }; + responses: { + /** @description Existing current run returned with no new work required. */ + 200: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + /** @description Whether this response replayed a prior intent result. */ + 'Idempotency-Replayed'?: boolean; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AnalyticsRun']; + }; + }; + /** @description Current run accepted and one or more missing sections are queued/calculating. */ + 202: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + /** @description Whether this response replayed a prior intent result. */ + 'Idempotency-Replayed'?: boolean; + /** @description Relative URL of the run snapshot. */ + Location?: string; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AnalyticsRun']; + }; + }; + 400: components['responses']['BadRequest']; + 401: components['responses']['Unauthenticated']; + 403: components['responses']['Forbidden']; + 413: components['responses']['RequestTooLarge']; + 415: components['responses']['UnsupportedMediaType']; + 422: components['responses']['ValidationFailed']; + 429: components['responses']['RateLimited']; + 500: components['responses']['InternalError']; + 503: components['responses']['AnalyticsUnavailable']; + }; + }; + getAnalyticsRun: { + parameters: { + query?: never; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components['parameters']['RunIdPath']; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current state of every run section. */ + 200: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AnalyticsRun']; + }; + }; + 400: components['responses']['BadRequest']; + 401: components['responses']['Unauthenticated']; + 404: components['responses']['ResourceNotFound']; + 429: components['responses']['RateLimited']; + 500: components['responses']['InternalError']; + 503: components['responses']['AnalyticsUnavailable']; + }; + }; + streamAnalyticsRunEvents: { + parameters: { + query?: never; + header?: { + /** @description Opaque last processed event id; must not contain NUL, LF, or CR. */ + 'Last-Event-ID'?: string; + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components['parameters']['RunIdPath']; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description UTF-8 event stream. A named heartbeat is emitted every 15 seconds. */ + 200: { + headers: { + /** @description Prevents transformation or storage of the live event stream. */ + 'Cache-Control'?: 'no-cache, no-transform'; + /** @description Deployment hint disabling Nginx response buffering. */ + 'X-Accel-Buffering'?: 'no'; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'text/event-stream': string; + }; + }; + 400: components['responses']['BadRequest']; + 401: components['responses']['Unauthenticated']; + 404: components['responses']['ResourceNotFound']; + 429: components['responses']['ConnectionLimited']; + 500: components['responses']['InternalError']; + 503: components['responses']['AnalyticsUnavailable']; + }; + }; + createAnalyticsSectionRetryAttempt: { + parameters: { + query?: never; + header: { + /** + * @description Client-generated intent key reused unchanged for retries of one intent. + * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b + */ + 'Idempotency-Key': components['parameters']['IdempotencyKeyHeader']; + /** @description CSRF token for the current authenticated session; never log this value. */ + 'X-CSRF-Token': components['parameters']['CsrfTokenHeader']; + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + Origin: components['parameters']['OriginHeader']; + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components['parameters']['RunIdPath']; + sectionId: components['parameters']['SectionIdPath']; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Retry accepted or equivalent current retry returned. */ + 202: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + /** @description Relative run snapshot URL. */ + Location?: string; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AnalyticsRun']; + }; + }; + 400: components['responses']['BadRequest']; + 401: components['responses']['Unauthenticated']; + 403: components['responses']['Forbidden']; + 404: components['responses']['ResourceNotFound']; + 409: components['responses']['Conflict']; + 422: components['responses']['ValidationFailed']; + 429: components['responses']['RateLimited']; + 500: components['responses']['InternalError']; + 503: components['responses']['AnalyticsUnavailable']; + }; + }; + listAnalyticsSessionDetails: { + parameters: { + query: { /** @description Stable id returned by `listAnalyticsPeriods`. */ - PeriodIdQuery: components["schemas"]["PeriodId"]; - DetailViewQuery: components["schemas"]["DetailView"]; + periodId: components['parameters']['PeriodIdQuery']; + view: components['parameters']['DetailViewQuery']; /** @description Valid only when `view=FAILED`. */ - FailureStageQuery: components["schemas"]["FailureStage"]; + failureStage?: components['parameters']['FailureStageQuery']; /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ - TerminalIdQuery: string; - SessionStatusQuery: components["schemas"]["SessionStatus"]; - MinAmountRialQuery: number; - MaxAmountRialQuery: number; + terminalId?: components['parameters']['TerminalIdQuery']; + status?: components['parameters']['SessionStatusQuery']; + minAmountRial?: components['parameters']['MinAmountRialQuery']; + maxAmountRial?: components['parameters']['MaxAmountRialQuery']; /** @description Opaque integrity-protected cursor bound to tenant, view, filters, and ordering. */ - CursorQuery: string; + cursor?: components['parameters']['CursorQuery']; /** @description Page size; defaults to 25 and cannot exceed 100. */ - LimitQuery: number; + limit?: components['parameters']['LimitQuery']; + }; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; }; - requestBodies: never; - headers: { - /** @description Server correlation id for this request. */ - RequestId: string; - /** @description Prevents storage of authenticated merchant data. */ - PrivateNoStore: "private, no-store"; - /** @description Seconds until retry is permitted. */ - RetryAfter: number; - }; - pathItems: never; -} -export type $defs = Record; -export interface operations { - createAuthSession: { - parameters: { - query?: never; - header: { - /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - Origin: components["parameters"]["OriginHeader"]; - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["LoginRequest"]; - }; - }; - responses: { - /** @description Authenticated session created. */ - 201: { - headers: { - /** - * @description Secure opaque session cookie; value is intentionally omitted from examples. - * @example __Host-zpa_session=; Path=/; Secure; HttpOnly; SameSite=Lax - */ - "Set-Cookie"?: string; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AuthSession"]; - }; - }; - 400: components["responses"]["BadRequest"]; - 401: components["responses"]["Unauthenticated"]; - 413: components["responses"]["RequestTooLarge"]; - 415: components["responses"]["UnsupportedMediaType"]; - 422: components["responses"]["ValidationFailed"]; - 429: components["responses"]["RateLimited"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["DependencyUnavailable"]; - }; - }; - getAuthSession: { - parameters: { - query?: never; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Current authenticated merchant-user session. */ - 200: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AuthSession"]; - }; - }; - 401: components["responses"]["Unauthenticated"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["DependencyUnavailable"]; - }; - }; - deleteAuthSession: { - parameters: { - query?: never; - header: { - /** @description CSRF token for the current authenticated session; never log this value. */ - "X-CSRF-Token": components["parameters"]["CsrfTokenHeader"]; - /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - Origin: components["parameters"]["OriginHeader"]; - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Current authenticated session revoked. */ - 204: { - headers: { - /** - * @description Expired session cookie. - * @example __Host-zpa_session=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax - */ - "Set-Cookie"?: string; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content?: never; - }; - 401: components["responses"]["Unauthenticated"]; - 403: components["responses"]["Forbidden"]; - 429: components["responses"]["RateLimited"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["DependencyUnavailable"]; - }; - }; - listAnalyticsPeriods: { - parameters: { - query?: { - /** @description Optional allowlisted period type. */ - periodType?: components["schemas"]["PeriodType"]; - }; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Approved period choices and default selection. */ - 200: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PeriodList"]; - }; - }; - 400: components["responses"]["BadRequest"]; - 401: components["responses"]["Unauthenticated"]; - 422: components["responses"]["ValidationFailed"]; - 429: components["responses"]["RateLimited"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["DependencyUnavailable"]; - }; - }; - listMerchantTerminals: { - parameters: { - query?: never; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Merchant-owned terminal choices ordered by session volume. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TerminalList"]; - }; - }; - 401: components["responses"]["Unauthenticated"]; - 503: components["responses"]["AnalyticsUnavailable"]; - }; - }; - createAnalyticsRun: { - parameters: { - query?: never; - header: { - /** - * @description Client-generated intent key reused unchanged for retries of one intent. - * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b - */ - "Idempotency-Key": components["parameters"]["IdempotencyKeyHeader"]; - /** @description CSRF token for the current authenticated session; never log this value. */ - "X-CSRF-Token": components["parameters"]["CsrfTokenHeader"]; - /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - Origin: components["parameters"]["OriginHeader"]; - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateAnalyticsRunRequest"]; - }; - }; - responses: { - /** @description Existing current run returned with no new work required. */ - 200: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - /** @description Whether this response replayed a prior intent result. */ - "Idempotency-Replayed"?: boolean; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AnalyticsRun"]; - }; - }; - /** @description Current run accepted and one or more missing sections are queued/calculating. */ - 202: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - /** @description Whether this response replayed a prior intent result. */ - "Idempotency-Replayed"?: boolean; - /** @description Relative URL of the run snapshot. */ - Location?: string; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AnalyticsRun"]; - }; - }; - 400: components["responses"]["BadRequest"]; - 401: components["responses"]["Unauthenticated"]; - 403: components["responses"]["Forbidden"]; - 413: components["responses"]["RequestTooLarge"]; - 415: components["responses"]["UnsupportedMediaType"]; - 422: components["responses"]["ValidationFailed"]; - 429: components["responses"]["RateLimited"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["AnalyticsUnavailable"]; - }; - }; - getAnalyticsRun: { - parameters: { - query?: never; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components["parameters"]["RunIdPath"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Current state of every run section. */ - 200: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AnalyticsRun"]; - }; - }; - 400: components["responses"]["BadRequest"]; - 401: components["responses"]["Unauthenticated"]; - 404: components["responses"]["ResourceNotFound"]; - 429: components["responses"]["RateLimited"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["AnalyticsUnavailable"]; - }; - }; - streamAnalyticsRunEvents: { - parameters: { - query?: never; - header?: { - /** @description Opaque last processed event id; must not contain NUL, LF, or CR. */ - "Last-Event-ID"?: string; - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components["parameters"]["RunIdPath"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description UTF-8 event stream. A named heartbeat is emitted every 15 seconds. */ - 200: { - headers: { - /** @description Prevents transformation or storage of the live event stream. */ - "Cache-Control"?: "no-cache, no-transform"; - /** @description Deployment hint disabling Nginx response buffering. */ - "X-Accel-Buffering"?: "no"; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "text/event-stream": string; - }; - }; - 400: components["responses"]["BadRequest"]; - 401: components["responses"]["Unauthenticated"]; - 404: components["responses"]["ResourceNotFound"]; - 429: components["responses"]["ConnectionLimited"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["AnalyticsUnavailable"]; - }; - }; - createAnalyticsSectionRetryAttempt: { - parameters: { - query?: never; - header: { - /** - * @description Client-generated intent key reused unchanged for retries of one intent. - * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b - */ - "Idempotency-Key": components["parameters"]["IdempotencyKeyHeader"]; - /** @description CSRF token for the current authenticated session; never log this value. */ - "X-CSRF-Token": components["parameters"]["CsrfTokenHeader"]; - /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - Origin: components["parameters"]["OriginHeader"]; - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components["parameters"]["RunIdPath"]; - sectionId: components["parameters"]["SectionIdPath"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Retry accepted or equivalent current retry returned. */ - 202: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - /** @description Relative run snapshot URL. */ - Location?: string; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AnalyticsRun"]; - }; - }; - 400: components["responses"]["BadRequest"]; - 401: components["responses"]["Unauthenticated"]; - 403: components["responses"]["Forbidden"]; - 404: components["responses"]["ResourceNotFound"]; - 409: components["responses"]["Conflict"]; - 422: components["responses"]["ValidationFailed"]; - 429: components["responses"]["RateLimited"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["AnalyticsUnavailable"]; - }; - }; - listAnalyticsSessionDetails: { - parameters: { - query: { - /** @description Stable id returned by `listAnalyticsPeriods`. */ - periodId: components["parameters"]["PeriodIdQuery"]; - view: components["parameters"]["DetailViewQuery"]; - /** @description Valid only when `view=FAILED`. */ - failureStage?: components["parameters"]["FailureStageQuery"]; - /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ - terminalId?: components["parameters"]["TerminalIdQuery"]; - status?: components["parameters"]["SessionStatusQuery"]; - minAmountRial?: components["parameters"]["MinAmountRialQuery"]; - maxAmountRial?: components["parameters"]["MaxAmountRialQuery"]; - /** @description Opaque integrity-protected cursor bound to tenant, view, filters, and ordering. */ - cursor?: components["parameters"]["CursorQuery"]; - /** @description Page size; defaults to 25 and cannot exceed 100. */ - limit?: components["parameters"]["LimitQuery"]; - }; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Authorized detail page. */ - 200: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SessionDetailPage"]; - }; - }; - 400: components["responses"]["BadRequest"]; - 401: components["responses"]["Unauthenticated"]; - 404: components["responses"]["ResourceNotFound"]; - 422: components["responses"]["ValidationFailed"]; - 429: components["responses"]["RateLimited"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["AnalyticsUnavailable"]; - }; - }; - getAnalyticsSession: { - parameters: { - query?: never; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path: { - sessionId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Authorized merchant-owned session. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SessionDetail"]; - }; - }; - 401: components["responses"]["Unauthenticated"]; - 404: components["responses"]["ResourceNotFound"]; - 503: components["responses"]["AnalyticsUnavailable"]; - }; - }; - exportAnalyticsSessionDetailsCsv: { - parameters: { - query: { - /** @description Stable id returned by `listAnalyticsPeriods`. */ - periodId: components["parameters"]["PeriodIdQuery"]; - view: components["parameters"]["DetailViewQuery"]; - /** @description Valid only when `view=FAILED`. */ - failureStage?: components["parameters"]["FailureStageQuery"]; - /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ - terminalId?: components["parameters"]["TerminalIdQuery"]; - status?: components["parameters"]["SessionStatusQuery"]; - minAmountRial?: components["parameters"]["MinAmountRialQuery"]; - maxAmountRial?: components["parameters"]["MaxAmountRialQuery"]; - }; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Complete authorized CSV export. */ - 200: { - headers: { - "Cache-Control": components["headers"]["PrivateNoStore"]; - /** - * @description Attachment with ASCII fallback first and UTF-8 `filename*` second. - * Filename contains only server-generated view, period id, and time. - * @example attachment; filename="unverified_paid-jm-1405-05-20260820T093000Z.csv"; filename*=UTF-8''unverified_paid-jm-1405-05-20260820T093000Z.csv - */ - "Content-Disposition"?: string; - /** @description Prevents content sniffing. */ - "X-Content-Type-Options"?: "nosniff"; - "X-Request-ID": components["headers"]["RequestId"]; - [name: string]: unknown; - }; - content: { - /** - * @example "شناسه نشست پرداخت","شناسه پایانه","وضعیت پرداخت","مبلغ ریال","کارمزد ریال","زمان ایجاد","زمان انقضا","تعداد تلاش","وضعیت آخرین تلاش","زمان آخرین تلاش","زمان تأیید","زمان تسویه","مرحله مشاهده شده شکست" - * ses_01K2M8,term_0042,پرداخت شده و تأیید نشده,1500000,2000,2026-06-01T08:00:00Z,2026-06-01T08:15:00Z,1,پرداخت شده و تأیید نشده,2026-06-01T08:02:00Z,,, - */ - "text/csv; charset=utf-8": string; - }; - }; - 400: components["responses"]["BadRequest"]; - 401: components["responses"]["Unauthenticated"]; - 404: components["responses"]["ResourceNotFound"]; - 413: components["responses"]["ExportTooLarge"]; - 422: components["responses"]["ValidationFailed"]; - 429: components["responses"]["RateLimited"]; - 500: components["responses"]["InternalError"]; - 503: components["responses"]["AnalyticsUnavailable"]; - 504: components["responses"]["ExportTimeout"]; - }; + requestBody?: never; + responses: { + /** @description Authorized detail page. */ + 200: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SessionDetailPage']; + }; + }; + 400: components['responses']['BadRequest']; + 401: components['responses']['Unauthenticated']; + 404: components['responses']['ResourceNotFound']; + 422: components['responses']['ValidationFailed']; + 429: components['responses']['RateLimited']; + 500: components['responses']['InternalError']; + 503: components['responses']['AnalyticsUnavailable']; }; + }; + getAnalyticsSession: { + parameters: { + query?: never; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path: { + sessionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authorized merchant-owned session. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SessionDetail']; + }; + }; + 401: components['responses']['Unauthenticated']; + 404: components['responses']['ResourceNotFound']; + 503: components['responses']['AnalyticsUnavailable']; + }; + }; + exportAnalyticsSessionDetailsCsv: { + parameters: { + query: { + /** @description Stable id returned by `listAnalyticsPeriods`. */ + periodId: components['parameters']['PeriodIdQuery']; + view: components['parameters']['DetailViewQuery']; + /** @description Valid only when `view=FAILED`. */ + failureStage?: components['parameters']['FailureStageQuery']; + /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ + terminalId?: components['parameters']['TerminalIdQuery']; + status?: components['parameters']['SessionStatusQuery']; + minAmountRial?: components['parameters']['MinAmountRialQuery']; + maxAmountRial?: components['parameters']['MaxAmountRialQuery']; + }; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Complete authorized CSV export. */ + 200: { + headers: { + 'Cache-Control': components['headers']['PrivateNoStore']; + /** + * @description Attachment with ASCII fallback first and UTF-8 `filename*` second. + * Filename contains only server-generated view, period id, and time. + * @example attachment; filename="unverified_paid-jm-1405-05-20260820T093000Z.csv"; filename*=UTF-8''unverified_paid-jm-1405-05-20260820T093000Z.csv + */ + 'Content-Disposition'?: string; + /** @description Prevents content sniffing. */ + 'X-Content-Type-Options'?: 'nosniff'; + 'X-Request-ID': components['headers']['RequestId']; + [name: string]: unknown; + }; + content: { + /** + * @example "شناسه نشست پرداخت","شناسه پایانه","وضعیت پرداخت","مبلغ ریال","کارمزد ریال","زمان ایجاد","زمان انقضا","تعداد تلاش","وضعیت آخرین تلاش","زمان آخرین تلاش","زمان تأیید","زمان تسویه","مرحله مشاهده شده شکست" + * ses_01K2M8,term_0042,پرداخت شده و تأیید نشده,1500000,2000,2026-06-01T08:00:00Z,2026-06-01T08:15:00Z,1,پرداخت شده و تأیید نشده,2026-06-01T08:02:00Z,,, + */ + 'text/csv; charset=utf-8': string; + }; + }; + 400: components['responses']['BadRequest']; + 401: components['responses']['Unauthenticated']; + 404: components['responses']['ResourceNotFound']; + 413: components['responses']['ExportTooLarge']; + 422: components['responses']['ValidationFailed']; + 429: components['responses']['RateLimited']; + 500: components['responses']['InternalError']; + 503: components['responses']['AnalyticsUnavailable']; + 504: components['responses']['ExportTimeout']; + }; + }; + createGrowthReport: { + parameters: { + query?: never; + header: { + /** + * @description Client-generated intent key reused unchanged for retries of one intent. + * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b + */ + 'Idempotency-Key': components['parameters']['IdempotencyKeyHeader']; + /** @description CSRF token for the current authenticated session; never log this value. */ + 'X-CSRF-Token': components['parameters']['CsrfTokenHeader']; + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + Origin: components['parameters']['OriginHeader']; + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateGrowthReportRequest']; + }; + }; + responses: { + /** @description Matching durable report reused. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GrowthReport']; + }; + }; + /** @description Report accepted for generation. */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GrowthReport']; + }; + }; + 401: components['responses']['Unauthenticated']; + 403: components['responses']['Forbidden']; + 409: components['responses']['Conflict']; + 422: components['responses']['ValidationFailed']; + 429: components['responses']['RateLimited']; + 503: components['responses']['AnalyticsUnavailable']; + }; + }; + getLatestGrowthReport: { + parameters: { + query?: { + terminalId?: string; + }; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Latest successful report for the authorized scope. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GrowthReport']; + }; + }; + 401: components['responses']['Unauthenticated']; + 404: components['responses']['ResourceNotFound']; + 503: components['responses']['AnalyticsUnavailable']; + }; + }; + getGrowthReport: { + parameters: { + query?: never; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + 'X-Request-ID'?: components['parameters']['RequestIdHeader']; + }; + path: { + reportId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authorized report snapshot. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GrowthReport']; + }; + }; + 401: components['responses']['Unauthenticated']; + 404: components['responses']['ResourceNotFound']; + 503: components['responses']['AnalyticsUnavailable']; + }; + }; + streamGrowthReportEvents: { + parameters: { + query?: never; + header?: { + 'Last-Event-ID'?: string; + }; + path: { + reportId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Stable-id report snapshots and heartbeats. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/event-stream': string; + }; + }; + 401: components['responses']['Unauthenticated']; + 404: components['responses']['ResourceNotFound']; + }; + }; } diff --git a/src/lib/components/GrowthForecastChart.svelte b/src/lib/components/GrowthForecastChart.svelte index c25d461..87fa3fd 100644 --- a/src/lib/components/GrowthForecastChart.svelte +++ b/src/lib/components/GrowthForecastChart.svelte @@ -6,13 +6,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card'; import { buildForecastChart, formatConfidence } from '$lib/growth-chart'; import { formatMoney, formatValue } from '$lib/format'; + import type { components } from '$lib/api/schema'; - type Range = { p10: number; p50: number; p90: number }; - type ForecastPoint = { - date: string; - paidSessions: Range; - paidAmountRial: Range; - }; + type ForecastPoint = components['schemas']['GrowthForecastPoint']; let { points, confidence, startDate }: { points: ForecastPoint[]; confidence: string; startDate: string } = $props(); diff --git a/src/lib/components/GrowthForecastChart.test.ts b/src/lib/components/GrowthForecastChart.test.ts index 4de7943..fa2a9a8 100644 --- a/src/lib/components/GrowthForecastChart.test.ts +++ b/src/lib/components/GrowthForecastChart.test.ts @@ -6,11 +6,15 @@ import GrowthForecastChart from './GrowthForecastChart.svelte'; const points = [ { date: '2026-07-01T00:00:00Z', + baselineSessions: 95, + baselineAmountRial: 9_500_000, paidSessions: { p10: 80, p50: 100, p90: 130 }, paidAmountRial: { p10: 8_000_000, p50: 10_000_000, p90: 13_000_000 }, }, { date: '2026-07-02T00:00:00Z', + baselineSessions: 110, + baselineAmountRial: 11_000_000, paidSessions: { p10: 90, p50: 120, p90: 150 }, paidAmountRial: { p10: 9_000_000, p50: 12_000_000, p90: 15_000_000 }, }, diff --git a/src/lib/components/GrowthInsights.svelte b/src/lib/components/GrowthInsights.svelte new file mode 100644 index 0000000..c78a8fd --- /dev/null +++ b/src/lib/components/GrowthInsights.svelte @@ -0,0 +1,207 @@ + + +
+
+ + فرصت‌های پیش رو + + {#each opportunities.slice(0, 5) as opportunity, index} +
+
+
+ فرصت {formatValue(index + 1)} +

{opportunity.eventTitleFa}

+
+ شباهت {bps(opportunity.similarityBps)} +
+

{dateLabel(opportunity.date)}

+

+ برآورد معمول {formatValue(opportunity.expectedSessions.p50)} پرداخت به ارزش + {formatMoney(opportunity.expectedAmountRial.p50)} است. +

+
+ {:else} +

+ در افق فعلی، فرصت رویدادی معناداری پیدا نشد. +

+ {/each} +
+
+ + {#if briefs.length} +
+

+ پیشنهادهای آماده‌سازی +

+
+ {#each briefs as brief} + + +

{brief.headline}

+

{brief.interpretation}

+
+
+
پیشنهاد فروش
+
{brief.offerSuggestion}
+
+
+
پیام پیشنهادی
+
{brief.messageSuggestion}
+
+
+
قدم آماده‌سازی
+
{brief.preparationAction}
+
+
+

{brief.limitation}

+
+ {#each brief.evidenceIds as evidenceId}مشاهده شاهد{/each} +
+
+
+ {/each} +
+

+ این پیشنهادها نقش راهنما دارند؛ تصمیم نهایی را با شرایط کسب‌وکارتان هماهنگ کنید. +

+
+ {/if} +
+ +
+ + تجربه‌های موفق گذشته + + {#each wins as win} +
+

{win.eventTitleFa}

+

{dateLabel(win.startDate)}

+

+ تعداد پرداخت {bps(win.paidSessionUpliftBps ?? 0)} بیشتر + و مبلغ + {bps(win.amountUpliftBps ?? 0)} بیشتر از روزهای مشابه بوده است. +

+
+ {:else}

+ هنوز رویداد موفقی با شواهد کافی ثبت نشده است. +

{/each} +
+
+ + + دوره‌های نیازمند آمادگی + + {#each risks.slice(0, 3) as risk} +
+

{dateLabel(risk.date)}

+

+ ممکن است تعداد پرداخت حدود {bps(risk.sessionDeclineBps)} کمتر از روند معمول باشد. برآورد میانی این + روز {formatValue(risk.expectedSessions.p50)} پرداخت است. +

+
+ {:else}

افت معناداری برای هشدار پیدا نشد.

{/each} +
+
+ + + مقایسه امن با هم‌گروه‌ها + + {#if terminalScoped} +

+ برای حفظ معنای مقایسه، جایگاه هم‌گروهی فقط در سطح کل پذیرنده نمایش داده می‌شود و برای یک درگاه + محاسبه نمی‌شود. +

+ {:else if peer?.state === 'READY'} +

+ این مقایسه با {formatValue(peer.eligiblePeerCount ?? 0)} کسب‌وکار واجد شرایط و بدون نمایش اطلاعات هیچ + پذیرنده‌ای ساخته شده است. +

+
+
+
+

+ جایگاه شما: صدک {formatValue(Math.round((peer.percentileBps ?? 0) / 100))} +

+ {:else} +

+ برای یک مقایسه محرمانه و قابل اتکا، تعداد هم‌گروه‌های واجد شرایط هنوز کافی نیست. +

+ {/if} +
+
+
+
diff --git a/src/lib/components/GrowthPage.svelte b/src/lib/components/GrowthPage.svelte new file mode 100644 index 0000000..659c5e3 --- /dev/null +++ b/src/lib/components/GrowthPage.svelte @@ -0,0 +1,284 @@ + + +تحلیل‌گر رشد | تحلیل زرین‌پال + + + +{#if !growthFeatureEnabled} +

تحلیل‌گر رشد هنوز فعال نشده است

+

+ پس از آماده‌شدن سرویس تحلیل، این بخش برای حساب شما نمایش داده می‌شود. +

+{:else if (loading && !visibleReport) || displayMode === 'loading'} + +{:else if displayMode === 'error' || !report || !visibleReport || !evidence} + +

{message}

+
+{:else} +
+ + +
+

افق گزارش: {visibleReport.horizonDays} روز پس از آخرین داده کامل

+

+ {visibleReport.cached + ? 'این تحلیل از نسخه ذخیره‌شده نمایش داده می‌شود.' + : 'نسخه تازه تحلیل در حال آماده‌شدن است.'} +

+
+
+ + نوبت بعدی: {formatDate(report.nextRefreshAt)} +
+
+
+ + {#if message}
+ {message} +
{/if} + + {#if report.status === 'PARTIAL'} +
+ اعداد و پیش‌بینی‌ها آماده‌اند، اما توضیح نوشتاری تازه در دسترس نبود. داده‌های قابل اتکا همچنان نمایش + داده می‌شوند. +
+ {:else if report.status === 'FAILED'} + + {/if} + + {#if evidence.forecast.confidence === 'INSUFFICIENT_DATA'} +

برای پیش‌بینی قابل اتکا هنوز داده کافی نداریم

+

+ این بخش فقط زمانی پیش‌بینی نشان می‌دهد که سابقه کافی برای مقایسه روزهای مشابه و ارزیابی دقت مدل + وجود داشته باشد. +

+ {:else} + + + {/if} + + {#if refreshing || isRunning}{/if} +
+{/if} diff --git a/src/lib/growth-report-state.test.ts b/src/lib/growth-report-state.test.ts new file mode 100644 index 0000000..af057ea --- /dev/null +++ b/src/lib/growth-report-state.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; + +import { reportDisplayMode } from './growth-report-state'; + +describe('growth report display state', () => { + it('keeps an uncached queued report in a visible loading state', () => { + expect(reportDisplayMode('QUEUED', false, false)).toBe('loading'); + }); + + it('keeps prior deterministic evidence visible during refresh', () => { + expect(reportDisplayMode('GENERATING', false, true)).toBe('content'); + }); + + it('shows deterministic content when AI copy is partial', () => { + expect(reportDisplayMode('PARTIAL', true, false)).toBe('content'); + }); + + it('offers recovery only when a failed report has no prior evidence', () => { + expect(reportDisplayMode('FAILED', false, false)).toBe('error'); + }); +}); diff --git a/src/lib/growth-report-state.ts b/src/lib/growth-report-state.ts new file mode 100644 index 0000000..8621a44 --- /dev/null +++ b/src/lib/growth-report-state.ts @@ -0,0 +1,11 @@ +export type GrowthDisplayMode = 'loading' | 'content' | 'error'; + +export function reportDisplayMode( + status: string | undefined, + hasCurrentEvidence: boolean, + hasPreviousEvidence: boolean, +): GrowthDisplayMode { + if (hasCurrentEvidence || hasPreviousEvidence) return 'content'; + if (status === 'QUEUED' || status === 'ANALYZING' || status === 'GENERATING') return 'loading'; + return 'error'; +} diff --git a/src/routes/(app)/growth/+page.svelte b/src/routes/(app)/growth/+page.svelte new file mode 100644 index 0000000..95e5292 --- /dev/null +++ b/src/routes/(app)/growth/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/tasks/ai-growth-analyst.md b/tasks/ai-growth-analyst.md index 6553475..e8a46f6 100644 --- a/tasks/ai-growth-analyst.md +++ b/tasks/ai-growth-analyst.md @@ -1,8 +1,7 @@ # AI Growth Analyst Frontend Tasks -- [ ] U1: Regenerate OpenAPI types and extend the typed client/report state model. -- [ ] U2: Add `/growth`, sidebar navigation, dashboard teaser, and global-terminal scope integration. -- [ ] U3: Add accessible 90-day forecast chart, confidence/summary states, event evidence, opportunity/risk, peer, and campaign brief components. -- [ ] U4: Add initial/refresh skeletons, preserved previous report, SSE/poll recovery, daily refresh feedback, partial/insufficient/error states, and reduced-motion behavior. -- [ ] U5: Add unit/component tests, format/check/build verification, responsive RTL review, and light/dark accessibility review without browser automation unless the user grants permission. - +- [x] U1: Regenerate OpenAPI types and extend the typed client/report state model. +- [x] U2: Add `/growth`, sidebar navigation, dashboard teaser, and global-terminal scope integration. +- [x] U3: Add accessible 90-day forecast chart, confidence/summary states, event evidence, opportunity/risk, peer, and campaign brief components. +- [x] U4: Add initial/refresh skeletons, preserved previous report, SSE/poll recovery, daily refresh feedback, partial/insufficient/error states, and reduced-motion behavior. +- [x] U5: Add unit/component tests, format/check/build verification, responsive RTL review, and light/dark accessibility review without browser automation unless the user grants permission.