fix: deliver validated AI insight responses

This commit is contained in:
2026-08-22 15:47:38 +08:00
parent ca55c2fb3d
commit 3fba4261ee
68 changed files with 6870 additions and 493 deletions

View File

@@ -4,7 +4,7 @@ Status: implemented platform contract; reviewed production calendar data must be
## Scope and authority
The platform serves `overview`, `payments`, `fees`, `peers`, `transactions`, and `growth`. Transaction-detail pages are excluded. Merchant identity comes only from the authenticated session; the optional terminal must belong to that merchant. Reporting periods and table filters never enter the insight scope or fingerprint.
The platform serves `overview`, `payments`, `fees`, `peers`, `transactions`, and `growth`. Transaction-detail pages are excluded. Merchant identity comes only from the authenticated session; the optional terminal must belong to that merchant. The transactions module may additionally use a validated reporting period so its summary matches the page; that period enters its persisted scope and fingerprint. Table filters never enter the insight scope.
PostgreSQL and Go own dates, aggregates, rankings, confidence, forecasts, calendar classifications, and freshness. Provider input may contain aggregate evidence IDs only. Raw payment rows, payer-card keys, session source keys, and unsuppressed peer values are forbidden.

View File

@@ -59,7 +59,7 @@ Every section uses a versioned typed contract and has independent `queued`, `cal
## Metric and period contract
- `METRIC-CATALOGUE.md` version `0.4.0` is canonical for formulas, units, grains, missing-data behavior, and peer direction.
- `METRIC-CATALOGUE.md` version `0.5.0` is canonical for formulas, units, grains, missing-data behavior, and peer direction.
- `Paid` and `Verified` both mean bank payment succeeded; only `Verified` is finalized. Never label `Paid` as failure.
- Amount and fee calculations count each eligible payment session once. Attempt rows cannot multiply session values.
- Rates are calculated from summed numerator and denominator components for the selected period, never averaged from daily percentages.

View File

@@ -21,11 +21,11 @@ Give an authenticated merchant a trustworthy 90-day Persian growth outlook: hist
## AI boundary
- AvalAI is backend-only and receives minimized aggregate evidence, never raw transactions, payer keys, credentials, arbitrary merchant ids, SQL, or peer rows.
- AvalAI is backend-only and receives five sanitized, chronologically upcoming events from the active reviewed calendar plus non-identifying merchant category/niche/channel context. It never receives raw transactions, payer keys, credentials, merchant ids, SQL, or peer rows.
- The consumer-owned `GrowthAnalyticsTools` boundary exposes only `get_growth_context`, `get_event_evidence`, `get_peer_context`, and `get_campaign_playbook`. Scope is injected from the authenticated job, not accepted from model arguments.
- Use `POST /v1/responses`, `store:false`, strict function schemas, strict structured output, `parallel_tool_calls:false`, no more than four tool calls and two model turns, 1,600 output tokens, a 30-second deadline, and one transient retry.
- Treat model output as untrusted. Accept only known evidence ids and qualitative Persian fields. Reject numeric claims, unsafe language, unknown references, refusal/incomplete output, or schema drift and use deterministic Persian fallback copy.
- Skip AvalAI when no material opportunity/risk exists or there are no historical observations. Low-confidence evidence may be interpreted, but the response must preserve its limitation. AI failure produces `PARTIAL`, never an empty report.
- The model selects exactly three distinct candidate evidence ids and returns one short qualitative Persian relevance reason per event; it cannot introduce dates, quantities, or event facts. Invalid output or provider failure produces `PARTIAL` with the first three chronological reviewed events and deterministic reasons.
## Persistence, caching, jobs, and limits
@@ -43,7 +43,7 @@ Give an authenticated merchant a trustworthy 90-day Persian growth outlook: hist
- `GET /api/v1/analytics/growth-reports/latest?terminalId=...` returns the latest authorized scope report.
- `GET /api/v1/analytics/growth-reports/{reportId}` returns an owned report; absent and cross-tenant ids are indistinguishable.
- `GET /api/v1/analytics/growth-reports/{reportId}/events` is authenticated SSE with stable ids, heartbeat, replay/snapshot recovery, and terminal status.
- Responses include deterministic series/ranges/confidence/evidence, opportunities, risks, peer result or suppression reason, validated briefs, cache metadata, and next refresh time.
- Responses include the five deterministic calendar candidates, exactly three validated AI-ranked event recommendations (or deterministic fallback), cache metadata, and next refresh time.
## Boundaries and acceptance

View File

@@ -0,0 +1,12 @@
# واژه‌نامه مربی رشد
- **مربی رشد:** فضای اصلی تصمیم‌سازی و پیگیری رشد هر پایانه.
- **قدم زرین:** اقدام پذیرفته‌شده مشترک merchant؛ دستی یا metric.
- **پیشنهاد:** خروجی تغییرناپذیر AI که merchant می‌پذیرد یا رد می‌کند.
- **برنامه ماهانه:** هدف والد در محدوده ماه جلالی.
- **برنامه هفتگی:** قدم‌های محدوده شنبه تا جمعه، متصل به هدف ماهانه در صورت وجود.
- **گزارش امروز:** توضیح روزانه بر پایه آخرین داده موجود، نه الزاماً امروز.
- **watermark:** شناسه نسخه داده پرداخت که ارزیابی تکراری را idempotent می‌کند.
- **سطح اقدام:** `SMALL/MEDIUM/STRETCH`؛ ورودی policy قطعی target.
- **سطح دشواری:** عدد یک تا پنج که فقط با تأیید merchant تغییر می‌کند.
- **چرخه تطبیقی:** استفاده از نتیجه دوره‌های قبلی برای پیشنهاد سطح بعد؛ بدون آموزش آنلاین مدل.

11
docs/GROWTH-COACH-SPEC.md Normal file
View File

@@ -0,0 +1,11 @@
# مربی رشد و قدم‌های زرین
`/growth` قابلیت اصلی محصول است. هر پایانه یک پروفایل نسخه‌دار، سطح دشواری و برنامه‌های مستقل روزانه، هفتگی و ماهانه دارد. نمای همه پایانه‌ها فقط rollup است و هدف مشترک میان nicheهای متفاوت ایجاد نمی‌کند.
برنامه ماهانه والد قدم‌های هفتگی است. پیشنهاد AI تغییرناپذیر و در یکی از وضعیت‌های `OFFERED/ACCEPTED/REJECTED/EXPIRED` قرار دارد. پذیرش، یک «قدم زرین» مشترک برای کاربران مجاز merchant می‌سازد. actor هر mutation ثبت می‌شود.
قدم دستی توسط merchant شروع، تکمیل یا کنار گذاشته می‌شود. قدم metric فقط توسط backend و در برابر watermark واقعی تکمیل می‌شود؛ merchant تنها می‌تواند آن را کنار بگذارد. حداقل نمونه metric صد session است. target نرخ‌ها به‌ترتیب دو، پنج و ده واحد درصد و target count/amount/latency/fee به‌ترتیب پنج، ده و بیست درصد است. جهت latency و fee کاهشی است.
AvalAI فقط متن فارسی، اولویت و انتخاب metric/level از allowlist را تولید می‌کند. baseline، target، deadline، completion و difficulty قطعی‌اند. این چرخه تطبیقی و توضیح‌پذیر است و online RL نیست.
زمان‌بندی با `cmd/scheduler`، ساعت ۰۷:۰۰ تهران، شنبه و روز نخست ماه جلالی انجام می‌شود. render هیچ generationای اجرا نمی‌کند. outage آخرین plan معتبر و progress قطعی را حفظ می‌کند.

View File

@@ -1,8 +1,8 @@
# Analytics Metric Catalogue
- Status: Approved specification — implementation authorized 2026-08-20
- Version: 0.4.0
- Last updated: 2026-08-20
- Version: 0.5.0
- Last updated: 2026-08-22
Every merchant-facing number identifies its grain, formula, source fields, filters, Tehran-local period, unit, missing-data behavior, trace path, and definition version.
@@ -55,6 +55,14 @@ Every merchant-facing number identifies its grain, formula, source fields, filte
- Formula: `verified_sessions / bank_paid_sessions`; undefined when no bank-paid sessions exist
- Unit: percentage
## Transaction status analytics
- `total_sessions`: count all merchant-scoped sessions in the selected half-open reporting period.
- `bank_success_rate`: sessions with status `Paid` or `Verified` divided by `total_sessions`; undefined when the total is zero.
- `failure_rate`: sessions with status `Failed` divided by `total_sessions`; undefined when the total is zero.
- Status distribution reports `Paid`, `Verified`, `Failed`, and `Reversed` separately as count and share of `total_sessions`.
- Trend grain is hourly for a day, daily for a week or Jalali month, and Saturday-aligned weekly for a Persian season. Empty buckets are returned as zeroes.
## Paid volume
- Canonical id: `bank_paid_amount_rial`

View File

@@ -0,0 +1,3 @@
# ADR 0017: Keep growth-coach decisions deterministic
AvalAI may rank and explain actions and select only an allowlisted metric, direction, and action level. Go locks baselines, targets, periods, state transitions, completion, and difficulty proposals. AI cannot mutate product state. Provider storage is disabled and request memory is reconstructed from persisted merchant state. Profile text is untrusted prompt data. Raw payments, payer keys, and secrets are excluded.

View File

@@ -0,0 +1,3 @@
# ADR 0018: Evaluate coach progress from versioned payment watermarks
Payment ingestion publishes a version identifier through `PaymentUpdateListener`. The current CSV importer uses its source checksum; future webhook/API adapters use the same boundary. Metric evaluation snapshots are unique per action and watermark. Static datasets remain static in the UI and never create simulated progress.

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: ZarinPal Merchant Analytics API
version: 1.1.0
version: 1.2.0
summary: Contract-first API for authenticated merchant analytics and growth reports.
description: |
Approved analytics and AI growth-report interface. This document is the API source of truth; generated
@@ -20,6 +20,7 @@ tags:
- name: Reporting periods
- name: Analytics runs
- name: Analytics details
- name: Growth coach
security:
- merchantSession: []
paths:
@@ -646,6 +647,7 @@ paths:
parameters:
- {name: module, in: path, required: true, schema: {$ref: '#/components/schemas/AIInsightModule'}}
- {name: terminalId, in: query, required: false, schema: {type: string, maxLength: 128}}
- {name: periodId, in: query, required: false, description: Used only by the transactions module to match the selected reporting period., schema: {type: string, maxLength: 128}}
- $ref: '#/components/parameters/RequestIdHeader'
responses:
'200': {description: Latest ready or deterministic-partial snapshot., content: {application/json: {schema: {$ref: '#/components/schemas/AIInsight'}}}}
@@ -676,6 +678,30 @@ paths:
'422': {$ref: '#/components/responses/ValidationFailed'}
'429': {$ref: '#/components/responses/RateLimited'}
'503': {$ref: '#/components/responses/AnalyticsUnavailable'}
/analytics/ai-insights/{module}/sections/{sectionId}/refreshes:
post:
tags: [AI page insights]
operationId: refreshAIInsightSection
summary: Refresh one independently persisted AI narrative section
parameters:
- {name: module, in: path, required: true, schema: {$ref: '#/components/schemas/AIInsightModule'}}
- {name: sectionId, in: path, required: true, schema: {$ref: '#/components/schemas/AIInsightSection'}}
- $ref: '#/components/parameters/IdempotencyKeyHeader'
- $ref: '#/components/parameters/CsrfTokenHeader'
- $ref: '#/components/parameters/OriginHeader'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/AIInsightRefreshRequest'}}}
responses:
'200': {description: Idempotent job reused., content: {application/json: {schema: {$ref: '#/components/schemas/AIInsightJob'}}}}
'202': {description: Section refresh accepted., content: {application/json: {schema: {$ref: '#/components/schemas/AIInsightJob'}}}}
'401': {$ref: '#/components/responses/Unauthenticated'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/ResourceNotFound'}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationFailed'}
'429': {$ref: '#/components/responses/RateLimited'}
'503': {$ref: '#/components/responses/AnalyticsUnavailable'}
/analytics/ai-insight-refreshes/{jobId}:
get:
tags: [AI page insights]
@@ -699,6 +725,159 @@ paths:
'401': {$ref: '#/components/responses/Unauthenticated'}
'404': {$ref: '#/components/responses/ResourceNotFound'}
/growth-coach:
get:
tags: [Growth coach]
operationId: getGrowthCoachDashboard
summary: Return a terminal coach workspace or an all-terminal rollup
parameters:
- {name: terminalId, in: query, required: false, schema: {type: string, maxLength: 128}}
responses:
'200': {description: Persisted coach state and deterministic progress., content: {application/json: {schema: {$ref: '#/components/schemas/GrowthCoachDashboard'}}}}
'401': {$ref: '#/components/responses/Unauthenticated'}
'404': {$ref: '#/components/responses/ResourceNotFound'}
'503': {$ref: '#/components/responses/AnalyticsUnavailable'}
/growth-coach/terminals/{terminalId}/profile:
get:
tags: [Growth coach]
operationId: getGrowthCoachProfile
parameters:
- {name: terminalId, in: path, required: true, schema: {type: string, maxLength: 128}}
responses:
'200': {description: Current profile version., content: {application/json: {schema: {$ref: '#/components/schemas/GrowthCoachProfile'}}}}
'404': {$ref: '#/components/responses/ResourceNotFound'}
put:
tags: [Growth coach]
operationId: putGrowthCoachProfile
parameters:
- {name: terminalId, in: path, required: true, schema: {type: string, maxLength: 128}}
- $ref: '#/components/parameters/IdempotencyKeyHeader'
- $ref: '#/components/parameters/CsrfTokenHeader'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/GrowthCoachProfileInput'}}}
responses:
'201': {description: New profile version and generation jobs., content: {application/json: {schema: {$ref: '#/components/schemas/GrowthCoachProfileResult'}}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationFailed'}
/growth-coach/terminals/{terminalId}/refreshes:
post:
tags: [Growth coach]
operationId: refreshGrowthCoach
parameters:
- {name: terminalId, in: path, required: true, schema: {type: string, maxLength: 128}}
- $ref: '#/components/parameters/IdempotencyKeyHeader'
- $ref: '#/components/parameters/CsrfTokenHeader'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/GrowthCoachRefreshInput'}}}
responses:
'202': {description: Cadence-scoped coach refresh queued., content: {application/json: {schema: {$ref: '#/components/schemas/GrowthCoachJob'}}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationFailed'}
'429': {$ref: '#/components/responses/RateLimited'}
'404': {$ref: '#/components/responses/ResourceNotFound'}
/growth-coach/plans:
get:
tags: [Growth coach]
operationId: listGrowthCoachPlans
parameters:
- {name: terminalId, in: query, required: false, schema: {type: string, maxLength: 128}}
responses:
'200': {description: Coach plans., content: {application/json: {schema: {type: object, required: [plans], properties: {plans: {type: array, items: {$ref: '#/components/schemas/GrowthCoachPlan'}}}}}}}
/growth-coach/actions:
get:
tags: [Growth coach]
operationId: listGoldenSteps
parameters:
- {name: terminalId, in: query, required: false, schema: {type: string, maxLength: 128}}
responses:
'200': {description: Shared Golden Steps workspace., content: {application/json: {schema: {type: object, required: [actions], properties: {actions: {type: array, items: {$ref: '#/components/schemas/GoldenStep'}}}}}}}
/growth-coach/goals:
get:
tags: [Growth coach]
operationId: listMonthlyGoals
parameters:
- {name: terminalId, in: query, required: false, schema: {type: string, maxLength: 128}}
responses:
'200': {description: Automatically tracked monthly goals with manual action subtasks., content: {application/json: {schema: {type: object, required: [goals], properties: {goals: {type: array, items: {$ref: '#/components/schemas/MonthlyGoal'}}}}}}}
/growth-coach/goals/{goalId}/subtasks/{subtaskId}/transitions:
post:
tags: [Growth coach]
operationId: transitionGoalSubtask
parameters:
- {name: goalId, in: path, required: true, schema: {type: string}}
- {name: subtaskId, in: path, required: true, schema: {type: string}}
- $ref: '#/components/parameters/IdempotencyKeyHeader'
- $ref: '#/components/parameters/CsrfTokenHeader'
requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: false, required: [transition], properties: {transition: {type: string, enum: [COMPLETE, REOPEN]}}}}}}
responses:
'200': {description: Updated user-completed subtask., content: {application/json: {schema: {$ref: '#/components/schemas/GoalSubtask'}}}}
'409': {$ref: '#/components/responses/Conflict'}
/growth-coach/deadline-proposals/{proposalId}/decisions:
post:
tags: [Growth coach]
operationId: decideGoalDeadlineProposal
parameters:
- {name: proposalId, in: path, required: true, schema: {type: string}}
- $ref: '#/components/parameters/IdempotencyKeyHeader'
- $ref: '#/components/parameters/CsrfTokenHeader'
requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: false, required: [decision], properties: {decision: {type: string, enum: [ACCEPT, REJECT]}}}}}}
responses:
'200': {description: Deadline proposal decision., content: {application/json: {schema: {$ref: '#/components/schemas/GoalDeadlineProposal'}}}}
'409': {$ref: '#/components/responses/Conflict'}
/growth-coach/recommendations/{id}/decisions:
post:
tags: [Growth coach]
operationId: decideGrowthCoachRecommendation
parameters:
- {name: id, in: path, required: true, schema: {type: string}}
- $ref: '#/components/parameters/IdempotencyKeyHeader'
- $ref: '#/components/parameters/CsrfTokenHeader'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CoachDecision'}}}}
responses:
'200': {description: Decision recorded and accepted action created when applicable.}
'409': {$ref: '#/components/responses/Conflict'}
/growth-coach/actions/{id}/transitions:
post:
tags: [Growth coach]
operationId: transitionGoldenStep
parameters:
- {name: id, in: path, required: true, schema: {type: string}}
- $ref: '#/components/parameters/IdempotencyKeyHeader'
- $ref: '#/components/parameters/CsrfTokenHeader'
requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: false, required: [transition], properties: {transition: {type: string, enum: [START, COMPLETE, ABANDON]}}}}}}
responses:
'200': {description: Updated action., content: {application/json: {schema: {$ref: '#/components/schemas/GoldenStep'}}}}
'409': {$ref: '#/components/responses/Conflict'}
/growth-coach/difficulty-proposals/{id}/decisions:
post:
tags: [Growth coach]
operationId: decideGrowthCoachDifficulty
parameters:
- {name: id, in: path, required: true, schema: {type: string}}
- $ref: '#/components/parameters/IdempotencyKeyHeader'
- $ref: '#/components/parameters/CsrfTokenHeader'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CoachDecision'}}}}
responses:
'200': {description: Difficulty decision recorded.}
'409': {$ref: '#/components/responses/Conflict'}
/growth-coach/refreshes/{jobId}:
get:
tags: [Growth coach]
operationId: getGrowthCoachJob
parameters: [{name: jobId, in: path, required: true, schema: {type: string}}]
responses:
'200': {description: Generation job., content: {application/json: {schema: {$ref: '#/components/schemas/GrowthCoachJob'}}}}
'404': {$ref: '#/components/responses/ResourceNotFound'}
/growth-coach/refreshes/{jobId}/events:
get:
tags: [Growth coach]
operationId: streamGrowthCoachJob
parameters: [{name: jobId, in: path, required: true, schema: {type: string}}]
responses:
'200': {description: Job snapshots., content: {text/event-stream: {schema: {type: string}}}}
components:
securitySchemes:
merchantSession:
@@ -1064,6 +1243,208 @@ components:
$ref: '#/components/schemas/Problem'
schemas:
GrowthCoachProfileInput:
type: object
additionalProperties: false
required: [businessDescription, niches, salesChannels, primaryGoal, constraints]
properties:
businessDescription: {type: string, minLength: 20, maxLength: 2000}
niches: {type: array, minItems: 1, maxItems: 10, items: {type: string, maxLength: 80}}
salesChannels: {type: array, minItems: 1, maxItems: 10, items: {type: string, maxLength: 80}}
primaryGoal: {type: string, minLength: 3, maxLength: 500}
constraints: {type: string, maxLength: 1000}
GrowthCoachProfile:
allOf:
- $ref: '#/components/schemas/GrowthCoachProfileInput'
- type: object
required: [profileId, terminalId, version, difficulty, createdAt]
properties:
profileId: {type: string}
terminalId: {type: string}
version: {type: integer, minimum: 1}
difficulty: {type: integer, minimum: 1, maximum: 5}
createdAt: {type: string, format: date-time}
GrowthCoachJob:
type: object
additionalProperties: false
required: [jobId, state, eventsUrl, createdAt, updatedAt]
properties:
jobId: {type: string}
state: {type: string, enum: [QUEUED, RUNNING, COMPLETED, FAILED]}
eventsUrl: {type: string}
createdAt: {type: string, format: date-time}
updatedAt: {type: string, format: date-time}
GrowthCoachRefreshInput:
type: object
additionalProperties: false
required: [cadence]
properties:
cadence: {type: string, enum: [WEEKLY, MONTHLY]}
GrowthCoachProfileResult:
type: object
additionalProperties: false
required: [profile, generationJobs, pageInsightJobs]
properties:
profile: {$ref: '#/components/schemas/GrowthCoachProfile'}
generationJobs: {type: array, items: {$ref: '#/components/schemas/GrowthCoachJob'}}
pageInsightJobs: {type: array, items: {$ref: '#/components/schemas/AIInsightJob'}}
GrowthCoachRecommendation:
type: object
additionalProperties: false
required: [recommendationId, planId, rank, status, actionType, title, rationale, expectedImpact, effort, evidenceIds, sampleSize, deadline, subtasks, eventIds]
properties:
recommendationId: {type: string}
planId: {type: string}
rank: {type: integer, minimum: 1, maximum: 3}
status: {type: string, enum: [OFFERED, ACCEPTED, REJECTED, EXPIRED]}
actionType: {type: string, enum: [MANUAL, METRIC]}
title: {type: string}
rationale: {type: string}
expectedImpact: {type: string}
effort: {type: string, enum: [LOW, MEDIUM, HIGH]}
evidenceIds: {type: array, items: {type: string}}
metricId: {type: [string, 'null']}
metricDirection: {type: [string, 'null']}
actionLevel: {type: [string, 'null']}
baseline: {type: [number, 'null']}
target: {type: [number, 'null']}
sampleSize: {type: integer, format: int64}
deadline: {type: string, format: date}
subtasks: {type: array, minItems: 3, maxItems: 7, items: {$ref: '#/components/schemas/GeneratedGoalSubtask'}}
eventIds: {type: array, items: {type: string}}
GeneratedGoalSubtask:
type: object
additionalProperties: false
required: [title, deadline, eventId]
properties:
title: {type: string}
deadline: {type: [string, 'null'], format: date}
eventId: {type: [string, 'null']}
GoalSubtask:
type: object
additionalProperties: false
required: [subtaskId, position, title, status, deadline]
properties:
subtaskId: {type: string}
position: {type: integer, minimum: 1, maximum: 7}
title: {type: string}
status: {type: string, enum: [TODO, DONE]}
deadline: {type: string, format: date}
eventId: {type: [string, 'null']}
completedAt: {type: [string, 'null'], format: date-time}
GoalDeadlineProposal:
type: object
additionalProperties: false
required: [proposalId, goalId, subtaskId, eventId, currentDeadline, proposedDeadline, reason, status]
properties:
proposalId: {type: string}
goalId: {type: string}
subtaskId: {type: string}
eventId: {type: string}
currentDeadline: {type: string, format: date}
proposedDeadline: {type: string, format: date}
reason: {type: string}
status: {type: string, enum: [OFFERED, ACCEPTED, REJECTED, EXPIRED]}
decidedAt: {type: [string, 'null'], format: date-time}
MonthlyGoal:
type: object
additionalProperties: false
required: [goalId, recommendationId, terminalId, title, expectedImpact, status, metricId, metricDirection, baseline, target, currentValue, progressPercent, sampleSize, deadline, eventIds, subtasks, deadlineProposals, updatedAt]
properties:
goalId: {type: string}
recommendationId: {type: string}
terminalId: {type: string}
title: {type: string}
expectedImpact: {type: string}
status: {type: string, enum: [ACTIVE, COMPLETED, MISSED, ABANDONED]}
metricId: {type: string, enum: [paid_count, paid_amount, fee_amount, avg_latency, success_rate]}
metricDirection: {type: string, enum: [INCREASE, DECREASE]}
baseline: {type: number}
target: {type: number}
currentValue: {type: number}
progressPercent: {type: number, minimum: 0, maximum: 100}
sampleSize: {type: integer, format: int64}
deadline: {type: string, format: date}
eventIds: {type: array, items: {type: string}}
subtasks: {type: array, items: {$ref: '#/components/schemas/GoalSubtask'}}
deadlineProposals: {type: array, items: {$ref: '#/components/schemas/GoalDeadlineProposal'}}
updatedAt: {type: string, format: date-time}
GrowthCoachPlan:
type: object
additionalProperties: false
required: [planId, cadence, status, periodStart, periodEnd, difficulty, suggestions, generatedAt]
properties:
planId: {type: string}
parentId: {type: [string, 'null']}
cadence: {type: string, enum: [DAILY, WEEKLY, MONTHLY]}
status: {type: string}
periodStart: {type: string, format: date}
periodEnd: {type: string, format: date}
dailyBrief: {type: [string, 'null']}
difficulty: {type: integer, minimum: 1, maximum: 5}
suggestions: {type: array, items: {$ref: '#/components/schemas/GrowthCoachRecommendation'}}
generatedAt: {type: string, format: date-time}
GoldenStep:
type: object
additionalProperties: false
required: [actionId, recommendationId, terminalId, actionType, title, status, sampleSize, deadline, updatedAt]
properties:
actionId: {type: string}
recommendationId: {type: string}
terminalId: {type: string}
actionType: {type: string, enum: [MANUAL, METRIC]}
title: {type: string}
status: {type: string, enum: [NOT_STARTED, IN_PROGRESS, COMPLETED, ABANDONED, MISSED]}
metricId: {type: [string, 'null']}
metricDirection: {type: [string, 'null']}
baseline: {type: [number, 'null']}
target: {type: [number, 'null']}
currentValue: {type: [number, 'null']}
sampleSize: {type: integer, format: int64}
deadline: {type: string, format: date}
updatedAt: {type: string, format: date-time}
GrowthCoachTerminalRollup:
type: object
additionalProperties: false
required: [terminalId, onboardingStatus, activeActions, overdueActions, completionRate]
properties:
terminalId: {type: string}
onboardingStatus: {type: string, enum: [REQUIRED, COMPLETE]}
activeActions: {type: integer}
overdueActions: {type: integer}
completionRate: {type: number, minimum: 0, maximum: 1}
GrowthCoachDashboard:
type: object
additionalProperties: false
required: [scope, generationState, plans, actions, terminals, difficultyProposals]
properties:
scope: {type: string, enum: [TERMINAL, ALL_TERMINALS]}
terminalId: {type: [string, 'null']}
dataThrough: {type: [string, 'null'], format: date}
generationState: {type: string, enum: [ONBOARDING_REQUIRED, GENERATING, IDLE, UNAVAILABLE]}
profile: {anyOf: [{$ref: '#/components/schemas/GrowthCoachProfile'}, {type: 'null'}]}
plans: {type: array, items: {$ref: '#/components/schemas/GrowthCoachPlan'}}
actions: {type: array, items: {$ref: '#/components/schemas/GoldenStep'}}
terminals: {type: array, items: {$ref: '#/components/schemas/GrowthCoachTerminalRollup'}}
difficultyProposals: {type: array, items: {$ref: '#/components/schemas/GrowthCoachDifficultyProposal'}}
GrowthCoachDifficultyProposal:
type: object
additionalProperties: false
required: [proposalId, fromLevel, toLevel, reason, status]
properties:
proposalId: {type: string}
fromLevel: {type: integer, minimum: 1, maximum: 5}
toLevel: {type: integer, minimum: 1, maximum: 5}
reason: {type: string}
status: {type: string, enum: [OFFERED, ACCEPTED, REJECTED, EXPIRED]}
CoachDecision:
type: object
additionalProperties: false
required: [decision, reason, note]
properties:
decision: {type: string, enum: [ACCEPTED, REJECTED]}
reason: {type: string, maxLength: 100}
note: {type: string, maxLength: 500}
CreateGrowthReportRequest:
type: object
additionalProperties: false
@@ -1147,6 +1528,26 @@ components:
expectedSessions: {$ref: '#/components/schemas/GrowthRange'}
expectedAmountRial: {$ref: '#/components/schemas/GrowthRange'}
historicalEvidenceId: {type: string}
GrowthUpcomingEvent:
type: object
additionalProperties: false
required: [evidenceId, eventId, eventTitleFa, date, eventType, officialHoliday, merchantCategories, behavioralChannels]
properties:
evidenceId: {type: string}
eventId: {type: string}
eventTitleFa: {type: string}
date: {type: string, format: date-time}
eventType: {type: string}
officialHoliday: {type: boolean}
merchantCategories: {type: array, items: {type: string}}
behavioralChannels: {type: array, items: {type: string}}
GrowthEventRecommendation:
type: object
additionalProperties: false
required: [evidenceId, relevanceReason]
properties:
evidenceId: {type: string}
relevanceReason: {type: string, minLength: 1, maxLength: 320}
GrowthRisk:
type: object
additionalProperties: false
@@ -1197,14 +1598,17 @@ components:
GrowthEvidence:
type: object
additionalProperties: false
required: [forecast, historicalWins, opportunities, risks, fallbackBriefs]
required: [events, merchantContext]
properties:
forecast: {$ref: '#/components/schemas/GrowthForecast'}
historicalWins: {type: array, items: {$ref: '#/components/schemas/GrowthHistoricalWin'}}
opportunities: {type: array, maxItems: 5, items: {$ref: '#/components/schemas/GrowthOpportunity'}}
risks: {type: array, maxItems: 3, items: {$ref: '#/components/schemas/GrowthRisk'}}
peer: {$ref: '#/components/schemas/GrowthPeer'}
fallbackBriefs: {type: array, items: {$ref: '#/components/schemas/GrowthFallbackBrief'}}
events: {type: array, maxItems: 5, items: {$ref: '#/components/schemas/GrowthUpcomingEvent'}}
merchantContext:
type: object
additionalProperties: false
required: [category, niches, salesChannels]
properties:
category: {type: string}
niches: {type: array, items: {type: string}}
salesChannels: {type: array, items: {type: string}}
GrowthReport:
type: object
additionalProperties: false
@@ -1218,13 +1622,13 @@ components:
additionalProperties: false
properties: {terminalId: {type: [string, 'null']}}
horizonDays: {type: integer, enum: [90]}
forecastStartDate: {type: string, format: date-time}
deterministicEvidence: {$ref: '#/components/schemas/GrowthEvidence'}
aiCopy:
type: object
additionalProperties: false
required: [briefs]
properties: {briefs: {type: array, maxItems: 5, items: {$ref: '#/components/schemas/GrowthCampaignBrief'}}}
required: [eventRecommendations]
properties:
eventRecommendations: {type: array, minItems: 3, maxItems: 3, items: {$ref: '#/components/schemas/GrowthEventRecommendation'}}
failureCode: {type: string}
cached: {type: boolean}
stale: {type: boolean}
@@ -1279,13 +1683,13 @@ components:
AIInsight:
type: object
additionalProperties: false
required: [insightId, module, revision, status, scope, asOfDate, dataThroughDate, forecastStartDate, freshness, dataLagDays, confidence, copy, evidence, calendarContext, refreshState, generatedAt]
required: [insightId, module, revision, status, scope, asOfDate, dataThroughDate, forecastStartDate, freshness, dataLagDays, confidence, copy, evidence, calendarContext, refreshState, sectionRefreshState, generatedAt]
properties:
insightId: {type: string}
module: {$ref: '#/components/schemas/AIInsightModule'}
revision: {type: integer, minimum: 1}
status: {type: string, enum: [READY, PARTIAL]}
scope: {type: object, properties: {terminalId: {type: [string, 'null']}}}
scope: {type: object, properties: {terminalId: {type: [string, 'null']}, periodId: {type: [string, 'null']}}}
asOfDate: {type: string, format: date}
dataThroughDate: {type: string, format: date}
forecastStartDate: {type: string, format: date}
@@ -1296,22 +1700,41 @@ components:
evidence: {type: object, additionalProperties: true}
calendarContext: {type: array, items: {type: object, additionalProperties: true}}
refreshState: {type: object, additionalProperties: true}
sectionRefreshState:
type: object
additionalProperties: {$ref: '#/components/schemas/AIInsightRefreshState'}
generatedAt: {type: string, format: date-time}
AIInsightRefreshRequest:
type: object
additionalProperties: false
properties: {terminalId: {type: string, maxLength: 128}}
properties:
terminalId: {type: string, maxLength: 128}
periodId: {type: string, maxLength: 128, description: Used only by the transactions module.}
AIInsightJob:
type: object
additionalProperties: false
required: [jobId, snapshotId, state, eventsUrl, createdAt, updatedAt]
required: [jobId, snapshotId, module, sectionId, state, eventsUrl, createdAt, updatedAt]
properties:
jobId: {type: string}
snapshotId: {type: string}
module: {$ref: '#/components/schemas/AIInsightModule'}
sectionId: {type: string, enum: [full, past, latest-observed, future-7-days, future-30-days, future-90-days, findings, options]}
state: {type: string, enum: [QUEUED, RUNNING, COMPLETED, FAILED]}
eventsUrl: {type: string, format: uri-reference}
createdAt: {type: string, format: date-time}
updatedAt: {type: string, format: date-time}
AIInsightSection:
type: string
enum: [past, latest-observed, future-7-days, future-30-days, future-90-days, findings, options]
AIInsightRefreshState:
type: object
additionalProperties: false
required: [state]
properties:
state: {type: string, enum: [IDLE, QUEUED, RUNNING, FAILED, LIMITED]}
jobId: {type: string}
eventsUrl: {type: string, format: uri-reference}
nextRefreshAt: {type: string, format: date-time}
LoginRequest:
type: object
additionalProperties: false
@@ -1529,7 +1952,7 @@ components:
SectionId:
type: string
enum: [overview, payment_performance, fees, peer_comparison]
enum: [overview, payment_performance, fees, peer_comparison, transactions]
AnalyticsRunState:
type: string
@@ -1566,7 +1989,7 @@ components:
description: Opaque identifier for the immutable completed-import snapshot.
metricDefinitionVersion:
type: string
const: 0.4.0
const: 0.5.0
createdAt:
type: string
format: date-time
@@ -1581,7 +2004,7 @@ components:
sections:
type: object
additionalProperties: false
required: [overview, paymentPerformance, fees, peerComparison]
required: [overview, paymentPerformance, fees, peerComparison, transactions]
properties:
overview:
$ref: '#/components/schemas/OverviewSection'
@@ -1591,6 +2014,8 @@ components:
$ref: '#/components/schemas/FeesSection'
peerComparison:
$ref: '#/components/schemas/PeerComparisonSection'
transactions:
$ref: '#/components/schemas/TransactionsSection'
QueuedSection:
type: object
@@ -1649,7 +2074,7 @@ components:
pattern: '^ds_[A-Za-z0-9_-]{8,60}$'
metricDefinitionVersion:
type: string
const: 0.4.0
const: 0.5.0
IntegerMetric:
type: object
@@ -1823,6 +2248,58 @@ components:
initTiming: {$ref: '#/components/schemas/TimingDistribution'}
verifyTiming: {$ref: '#/components/schemas/TimingDistribution'}
TransactionStatusDistribution:
type: object
additionalProperties: false
required: [status, count, sharePercent]
properties:
status: {$ref: '#/components/schemas/SessionStatus'}
count:
type: integer
format: int64
minimum: 0
sharePercent:
type: [string, 'null']
pattern: '^(100|[0-9]{1,2})(\.[0-9]{1,6})?$'
TransactionStatusTrendPoint:
type: object
additionalProperties: false
required: [localDate, paidSessions, verifiedSessions, failedSessions, reversedSessions, totalSessions]
properties:
localDate:
type: string
format: date
localHour:
type: integer
minimum: 0
maximum: 23
paidSessions: {type: integer, format: int64, minimum: 0}
verifiedSessions: {type: integer, format: int64, minimum: 0}
failedSessions: {type: integer, format: int64, minimum: 0}
reversedSessions: {type: integer, format: int64, minimum: 0}
totalSessions: {type: integer, format: int64, minimum: 0}
TransactionsData:
type: object
additionalProperties: false
required: [totalSessions, bankSuccessRate, verificationCompletionRate, failureRate, statusDistribution, statusTrend]
properties:
totalSessions: {$ref: '#/components/schemas/IntegerMetric'}
bankSuccessRate: {$ref: '#/components/schemas/DecimalMetric'}
verificationCompletionRate: {$ref: '#/components/schemas/DecimalMetric'}
failureRate: {$ref: '#/components/schemas/DecimalMetric'}
statusDistribution:
type: array
minItems: 4
maxItems: 4
description: Canonical order is Paid, Verified, Failed, Reversed.
items: {$ref: '#/components/schemas/TransactionStatusDistribution'}
statusTrend:
type: array
maxItems: 31
items: {$ref: '#/components/schemas/TransactionStatusTrendPoint'}
FeesData:
type: object
additionalProperties: false
@@ -1898,6 +2375,16 @@ components:
metadata: {$ref: '#/components/schemas/ReadySectionMetadata'}
data: {$ref: '#/components/schemas/PaymentPerformanceData'}
TransactionsReadySection:
type: object
additionalProperties: false
required: [sectionId, state, metadata, data]
properties:
sectionId: {type: string, const: transactions}
state: {type: string, const: READY}
metadata: {$ref: '#/components/schemas/ReadySectionMetadata'}
data: {$ref: '#/components/schemas/TransactionsData'}
FeesReadySection:
type: object
additionalProperties: false
@@ -1966,6 +2453,12 @@ components:
- $ref: '#/components/schemas/CalculatingSection'
- $ref: '#/components/schemas/FailedSection'
- $ref: '#/components/schemas/PaymentPerformanceReadySection'
TransactionsSection:
oneOf:
- $ref: '#/components/schemas/QueuedSection'
- $ref: '#/components/schemas/CalculatingSection'
- $ref: '#/components/schemas/FailedSection'
- $ref: '#/components/schemas/TransactionsReadySection'
FeesSection:
oneOf:
- $ref: '#/components/schemas/QueuedSection'
@@ -2172,6 +2665,7 @@ components:
oneOf:
- $ref: '#/components/schemas/OverviewReadySection'
- $ref: '#/components/schemas/PaymentPerformanceReadySection'
- $ref: '#/components/schemas/TransactionsReadySection'
- $ref: '#/components/schemas/FeesReadySection'
- $ref: '#/components/schemas/PeerComparisonReadySection'
SectionFailedEvent: