LLD: MM-Cron ingest routes (POST /api/ingest/<type>)¶
status: draft (MMDEP Design, 27 Oct). High-risk contract written by Claude; the API agent checks it against the schema and destinations (Lane P step P5) and builds it.
sprint: S2 (spec in OpenAPI), S3 (staging), S4 (production, Atul approves)
decisions: TD-057, TD-058, TD-060, TD-065
depends on: LLD-adapter-framework (schema v1), LLD-write-route-security (keys), LLD-nav-integrity (persist_nav)
Context¶
MM-Cron writes to the databases directly today, except for NAV through /save-nav (OR §13, HO-18 #4). Atul wants every writer to use the API (relay 17 Oct, MM-Cron priority 1). The adapter framework publishes envelope records in schema v1 (adapter LLD §5). This LLD gives each record type a route, a destination and a persistence rule.
Decision¶
1. One route pattern¶
POST /api/ingest/<type> for type ∈ nav, scheme, aum, ter, performance, nfo, index, holdings, fund-details, runs.
- Auth: class M,
@require_key(scope="ingest:<type>")(security LLD). The MM-Cron keys getingest:*. - Body:
{"run_id", "source_id", "raw_sha256", "schema": "mmcron/v1", "records": [<envelope>…]}, with at most 1,000 records. - Validation: every record is checked against the same JSON Schema files MM-Cron uses (
schema/v1/). They are vendored into the api repo atsrc/ingest/schema/v1/, and a test compares their sha256 against a pinned list, so a change on either side is a deliberate, reviewed change. Invalid records are rejected individually; valid ones go on. - Reply 200:
{"run_id", "accepted", "unchanged", "corrected", "rejected": [{"index", "reason"}]}. A wrong schema version or an over-size body gets 400; auth failures get 401/403. - Idempotent: sending the same body again gives
unchangedfor every record. - One transaction per request: a database error rolls back the whole request and returns 503, and MM-Cron retries.
2. Destinations and rules¶
Existing tables keep their meaning. New source facts go to new tables, so derived tables are not overwritten:
| Type | Destination (UNVERIFIED names to confirm in P5 preflight) | Key | On an existing key |
|---|---|---|---|
nav |
mm_data.funds_nav via persist_nav |
(scheme_code, nav_date) | NAV LLD stage B |
index |
mm_data.nifty_index via persist_nav |
(symbol, date) | NAV LLD stage B |
scheme |
mm_data.scheme_data |
scheme_code | update changed fields; log each change in ingest_changes |
scheme (new code) |
also a new MF product in moneymarche_www.mm_product, the way cliAum.py does it today (P5 preflight documents that logic) |
— | — |
aum |
mm_data.mm_funds_aum |
(scope, scheme_code or amc_code, period) | replace the value, log the change |
ter |
new mm_data.mm_fund_ter |
(scheme_code, effective_from) | unchanged or log a correction |
performance |
mm_data.mm_funds_performance |
(scheme_code, period, as_of) | replace, log |
nfo |
mm_data.mm_nfo_list |
(amc_code, scheme_name, open_date) | update |
holdings |
mm_data.mm_portfolio (one snapshot per amc_code, portfolio_name, as_of) → mm_portfolio_items |
the snapshot | a resend of the same snapshot = unchanged; a different one for the same month = a new snapshot version, the old one marked superseded (never edited in place) |
fund-details |
new mm_data.mm_fund_details_src (one row per portfolio and as_of; managers as JSON) |
(amc_code, portfolio_name, as_of) | new version, old marked superseded |
runs |
new mm_data.ingest_runs |
run_id | upsert counts |
- Product creation stays as it is. Following #107 (MCRM: the Catalogue Manager sets ID rules; the API database is the store and writer), the API reproduces today's behaviour exactly and changes it only when MCRM publishes its ID rule. That is a contract change with MCRM UAT.
- New tables (
mm_fund_ter,mm_fund_details_src,ingest_runs,ingest_changes, plusnav_correctionsfrom the NAV LLD) are created by one migration file, applied on staging in S3 and on production with the S4 release. - Read routes for the new data are not part of this LLD: holdings (TD-058), TER and fund details for the fund-factsheet contract (P4). They read these tables.
3. Every request leaves a trace¶
ingest_runs holds run_id, type, source_id, raw_sha256, the key id, received_at, and the counts. persisted_at is the database time of the commit. This closes MM-Cron's traceability chain (KN): source attempt → fetch → raw file → parse → load → API acknowledgement.
4. The OpenAPI entry¶
All ten routes go into docs/openapi.yaml in S2, with the request schema referencing the vendored JSON Schema files, the reply schema, the error codes, the auth scheme and the consumer (mm-cron). Contract tests (Sonnet T3) call them on stgapi from S3.
C4 element¶
apiIngestapiSecurityapiDataAccessmmcronPublishmmDatawwwDb
Evaluation¶
| Assessment | |
|---|---|
| V value | High. It removes MM-Cron's direct database writes, gives one write path with checks, and gives an acknowledgement for every record |
| S simplicity | One route pattern, one shared schema, one transaction per request; existing tables keep their meaning |
| C cost | About 3–4 agent sessions; four new tables; no new service |
| D data | Public market data; key ids in logs, secrets nowhere |
| L legal | None beyond the sources' own terms (MM-Cron side) |
Test plan¶
- Per type: a valid batch →
accepted; resend →unchanged; an invalid record (bad decimal string, unknown enum) → rejected with index and reason while the rest are accepted; a wrong schema version → 400. - Auth: no key → 401 in
enforce; a key withoutingest:<type>→ 403; a staging key on production → 403. - Transaction: a forced database error mid-batch → 503 and nothing written.
- Holdings versioning: the same snapshot twice →
unchanged; a changed snapshot for the same month → a new version, the old one superseded, the items not edited in place. - Scheme: a new scheme code creates exactly what
cliAum.pycreates today (a characterisation test built from the P5 preflight's reading ofcliAum.py). - Schema pin: the vendored schema files' sha256 match the pinned list; changing one without updating the pin fails CI.
- Contract: each route in
docs/openapi.yaml; contract tests green on stgapi (S3). - End to end (staging, S3): MM-Cron publishes one fixture per type to stgapi; the reply counts match;
ingest_runshas the run.
Risk¶
risk: high
It's a new write contract into production data, and new tables. Mitigations: - the schema is shared and pinned; - per-record validation; - one transaction per request; - snapshots are versioned, never edited in place; - existing tables keep their meaning; - staging first for a full cycle; - the production release needs Atul's approval; - MM-Cron enables production family by family (MC-4).
Work items¶
| # | Title | Risk | Model | Scope paths | Depends on | Cx | Tests |
|---|---|---|---|---|---|---|---|
| 1 | P5 preflight: confirm destination table names and keys; document cliAum.py's product-creation logic |
medium | chatgpt-api-mmcron | api: docs/design/ingest-routes-preflight.md | 2 | ||
| 2 | OpenAPI entries for the 10 ingest routes | medium | chatgpt-api-mmcron | api: docs/openapi.yaml | 1 | 2 | contract lint |
| 3 | Vendor schema v1 with a sha256 pin test | low | gemini-flash-lite | api: src/ingest/schema/v1/, tests/test_ingest_schema_pin.py | adapter LLD item 1 | 1 | tests/test_ingest_schema_pin.py |
| 4 | Migration: mm_fund_ter, mm_fund_details_src, ingest_runs, ingest_changes |
high | chatgpt-api-mmcron | api: migrations/ | 1 | 2 | migration test on a container |
| 5 | Ingest blueprint: auth, validation, reply, transaction; nav, index via persist_nav; runs |
high | chatgpt-api-mmcron | api: src/ingest/, tests/test_ingest_core.py | 3,4, NAV LLD 4, security LLD 2 | 3 | tests/test_ingest_core.py |
| 6 | Types scheme (with product-creation characterisation), aum, ter, performance, nfo |
high | chatgpt-api-mmcron | api: src/ingest/types/, tests/test_ingest_types.py | 5 | 3 | tests/test_ingest_types.py |
| 7 | Types holdings and fund-details with snapshot versioning |
high | chatgpt-api-mmcron | api: src/ingest/types/, tests/test_ingest_snapshots.py | 5 | 3 | tests/test_ingest_snapshots.py |
| 8 | Staging end to end with MM-Cron fixtures; then the production release (Atul) | high | chatgpt-api-mmcron | api: release MR | 6,7 | 2 | staging e2e evidence |