docs(cache): remove static dataset invalidation

This commit is contained in:
2026-08-20 18:40:28 +03:30
parent f471eaf3a2
commit 27903ff67e
9 changed files with 62 additions and 102 deletions

View File

@@ -1,7 +1,7 @@
# PostgreSQL Physical Schema Specification
- Status: Decision-ready draft — stakeholder approval required
- Scope: Data foundation, demo identity persistence, and analytics invalidation state
- Status: Approved specification — implementation not yet authorized
- Scope: Data foundation, demo identity persistence, and immutable dataset cache identity
- Implementation authorization: Not granted
- Last updated: 2026-08-20
- Visual companion: [`PHYSICAL-SCHEMA-ERD.html`](./PHYSICAL-SCHEMA-ERD.html)
@@ -26,7 +26,7 @@ Define the implementation-ready PostgreSQL shape for the normalized ZarinPal arc
2. A terminal belongs to exactly one merchant.
3. A payment session belongs to exactly one merchant and one terminal; a composite foreign key proves that the terminal belongs to that merchant.
4. A payment attempt belongs to exactly one session and has a positive sequence unique within that session.
5. A bootstrap session belongs to exactly one completed import lineage; a live session has no synthetic import run.
5. Every MVP payment session belongs to exactly one completed import lineage.
6. Attempt provenance is inherited through its session rather than duplicated on every attempt.
7. PSP and issuer-bank codes are reference entities. Switch response codes remain nullable PSP-scoped text because no authoritative meaning catalogue was supplied.
8. Payer-card tokens are nullable, merchant-scoped through the session relationship, and never globally unique.
@@ -56,7 +56,7 @@ Define the implementation-ready PostgreSQL shape for the normalized ZarinPal arc
| `started_at` | `timestamptz` required | run start |
| `completed_at` | `timestamptz` nullable | terminal completion/failure time |
A partial unique index on `(source_sha256, source_schema_version, importer_revision)` where `status = 'completed'` makes a completed import idempotent while permitting explicit retry after a failed run. Terminal state requires `completed_at`; non-terminal state forbids it.
A partial unique index on `(source_sha256, source_schema_version)` where `status = 'completed'` makes a completed source snapshot idempotent across importer rebuilds while permitting explicit retry after a failed run. `importer_revision` remains provenance, not business identity. Terminal state requires `completed_at`; non-terminal state forbids it.
#### `import_issues`
@@ -206,10 +206,9 @@ The ownership FK guarantees that the user belongs to the recorded merchant. Chec
| `session_status` | `text` required | non-empty source lifecycle code |
| `created_at` | `timestamptz` required | source Tehran-local value normalized to UTC |
| `expires_at` | `timestamptz` required | source `expire_in` normalized to UTC |
| `source_origin` | `text` required | `bootstrap` or `zarinpal_live` |
| `import_run_id` | `bigint` nullable | restrictive FK to `import_runs` |
| `import_run_id` | `bigint` required | restrictive FK to the completed `import_runs` lineage |
The origin check requires an import run for `bootstrap` and forbids one for `zarinpal_live`. Timestamp ordering is not enforced because source ordering anomalies are preserved as evidence rather than rewritten; expiry-before-creation, if found during exact validation, is reported according to the import specification.
Timestamp ordering is not enforced because source ordering anomalies are preserved as evidence rather than rewritten; expiry-before-creation, if found during exact validation, is reported according to the import specification.
Approved indexes:
@@ -238,27 +237,11 @@ Approved indexes:
Unique `(payment_session_id, try_seq)` is the attempt identity and lookup index. A check requires `issuer_bank_id` and `payer_card_key` to be either both null or both populated, matching the authoritative data guide and observed missingness. No timestamp-order check is used because documented anomalies are retained. No standalone payer-token, bank, PSP, or status index is added until an approved measured query justifies it.
### Analytics invalidation
### Static dataset cache namespace
#### `merchant_analytics_revisions`
The MVP performs no normal payment-fact writes after the one-time bootstrap, so the physical schema contains no analytics-revision tables and no insert/update/delete invalidation triggers. The importer permits one completed dataset in the MVP; Redis keys include that completed `import_runs.id` plus its immutable source checksum/schema version, merchant, period, section, and metric-definition version.
`merchant_id bigint` PK/FK to `merchants`; `source_revision bigint` required default `0` and non-negative; `updated_at timestamptz` required.
#### `cohort_analytics_revisions`
`category_id bigint` PK/FK to `merchant_categories`; `source_revision bigint` required default `0` and non-negative; `updated_at timestamptz` required.
Revision values are cache namespace markers, not transaction counts. They may advance more than once in one business operation without changing correctness.
## Database-managed revision contract
Direct ZarinPal inserts may bypass Go, so invalidation cannot rely only on application callbacks. Versioned PostgreSQL statement-level `AFTER INSERT` triggers with transition tables update each affected merchant and category once per insert statement:
- session insert: use merchant/category relationships from the inserted sessions;
- attempt insert: join inserted attempts to their sessions, merchants, and categories;
- update/delete triggers do not exist in MVP because payment facts are append-only.
This is deliberate schema-owned logic, named in migrations and covered by integration tests. Bulk promotion remains efficient because a multi-row `INSERT ... SELECT` advances each affected revision once, not once per imported row. Trigger failure aborts the fact insert; analytics can never observe a new fact under an old revision.
Dataset replacement and future ZarinPal live writes are outside the MVP. Either change requires a separately approved recovery, namespace-selection, and invalidation design before payment-fact mutation is allowed.
## Staging boundary
@@ -266,7 +249,7 @@ Importer staging tables are run-scoped, migration-owned technical structures rat
## Deletion, mutation, and retention
- Imported and live payment facts are append-only in MVP. Corrections require a new specification/ADR and matching invalidation behavior.
- Imported payment facts are immutable in MVP. Corrections or live writes require a new specification/ADR and matching invalidation behavior.
- Merchant/category/terminal/import provenance referenced by facts cannot be deleted. Synthetic display names and password hashes may be updated through approved identity workflows.
- Session rows are revoked/expired, not hard-deleted during their security lifetime. Authentication audit events expire after 30 days; analytics-access and CSV-export audit events expire after 90 days. Cleanup is allowed only after `expires_at` and must not remove payment/import facts.
- Redis analytics entries are disposable and expire after 30 days; no calculated analytics table exists in PostgreSQL.
@@ -275,7 +258,7 @@ Importer staging tables are run-scoped, migration-owned technical structures rat
- Atlas-generated versioned SQL is reviewed and committed; production startup never auto-migrates.
- Migration tests create the schema from zero and upgrade from every supported release state.
- Constraint tests cover terminal ownership, user/audit tenant ownership, no-attempt exclusion, attempt uniqueness, bootstrap/live provenance, paired card/bank nullability, session token digest length, audit fingerprint length, and revision-trigger atomicity.
- Constraint tests cover terminal ownership, user/audit tenant ownership, no-attempt exclusion, attempt uniqueness, bootstrap provenance, paired card/bank nullability, session token digest length, and audit fingerprint length.
- Query-plan verification uses production-shaped volumes and `EXPLAIN (ANALYZE, BUFFERS)` for approved analytics, detail pagination, CSV export, and session-authentication paths before adding further indexes.
- Initial fact tables are not partitioned. Partitioning requires measured operational need and a separate ADR.
@@ -287,20 +270,20 @@ Importer staging tables are run-scoped, migration-owned technical structures rat
- Session-level amount, fee, merchant, terminal, status, creation, expiry, origin, and provenance are stored once.
- `verify_type` is stored once per terminal and reconciles to every source record for that terminal.
- Payer tokens remain unchanged and restricted; they are absent from uniqueness constraints, logs, exports, peer results, and AI inputs.
- Direct session/attempt inserts atomically select new merchant/cohort revision namespaces.
- The sole completed import run provides an immutable dataset cache namespace without database triggers.
- Approved period, authentication, drill-down, and export access paths have an explicit initial index or a documented measure-first rule.
- Schema creation, FK/check constraints, trigger behavior, 30/90-day retention cleanup, and query plans are integration-tested before implementation approval can be considered complete.
- Schema creation, FK/check constraints, snapshot selection, 30/90-day retention cleanup, and query plans are integration-tested before implementation approval can be considered complete.
## Boundaries
- Always: preserve source keys and nulls, enforce tenant ownership with FKs, store money at session grain, use UTC instants, version migrations, and test revision changes atomically.
- Always: preserve source keys and nulls, enforce tenant ownership with FKs, store money at session grain, use UTC instants, version migrations, and treat imported payment facts as immutable.
- Ask first: add a table/column/index, denormalize tenant/category data, introduce partitioning, classify a new lifecycle status, or permit fact mutation/deletion.
- Never: create one wide CSV table, create a fake no-attempt row, duplicate session money on attempts, trust file order, store raw session tokens, or hide production migrations in startup.
## Decision batch for approval
## Resolved decisions
1. Store terminal-invariant `verify_type` on `terminals`, not every payment attempt.
2. Preserve future non-empty lifecycle status values as text and classify them through the metric catalogue instead of using rigid PostgreSQL enums.
3. Use explicit statement-level insert triggers for merchant/cohort analytics revisions because direct ZarinPal writes can bypass Go.
3. Omit cache-revision tables and database triggers because the MVP dataset is immutable; use the sole completed import run as the Redis cache namespace.
4. Persist minimal 30-day authentication audit events and 90-day analytics/export audit events because the approved Identity and API specs require queryable operator evidence without raw payloads, cursors, credentials, or sensitive data.
5. Approve the table, constraint, index, and restrictive-deletion contract above as the migration target.