# PRD — datafy-lambda-new

Evidence date: 2026-10-17.

Source: `git@bitbucket.org:overdrive/datafy-lambda-new.git`, `main` at `9519bea` (2026-07-20, "update documents").

There is no prior PRD in Silicon Ark. Requirements below are recovered from handlers, SAM templates, `CHANGELOG.md`, `NewKnowledgeBase.md`, stack READMEs, and `docs/`. Provenance is labelled. Secret values and dump credentials from the root readme are omitted.

`lambda/datafy_new/README.md` is mostly the AWS SAM Hello World sample. Where it disagrees with `template.yaml`, the template is the implemented behaviour.

## Product purpose

Run Datafy's server-side file import, task build, store-issue refresh, report generation, and shelf-photo check as AWS Lambda functions against Supabase.

Provenance: **Inferred** from the root readme title "Tools and Lambdas for Datafy", the SJ-report section of that readme, and the handlers under `lambda/datafy_new` and `lambda/bedrock_analysis`. No file states a customer name, a success metric, or a contract.

`datafy_web` (separate repository) is a Flutter client for the same retail domain and can POST an image id to an analyze-image API. Whether that URL is the API in this repository is **Unknown / Requires Confirmation**. This template's stage is `prod`. The web client URL recorded in the `datafy_web` PRD uses stage `Stage`.

## Users and interactions

| Actor | What they do | Evidence | Provenance |
|---|---|---|---|
| Caller of API Gateway | POST the routes below. Templates set CORS `*` and do not set an authorizer or API key | `ReadFilesApi`, `BedrockAnalysisApi` | **Implemented**. Who is allowed to call is **Unknown** |
| EventBridge | Daily combined report, daily KaChing report, daily Bedrock retry | Schedules in the two templates | **Implemented** |
| Person or system that inserts `sjreport_uploads` / `promo_kaching_uploads` | Creates the row the import Lambdas poll. This repo does not create those rows | `read_files/app.py`, `read_promo_kaching/app.py` | **Inferred** |
| Operator reading email | Receives Gmail SMTP notices (admin and, on some paths, client) | `email_service.py` usage; SSM `/gmail/*` | **Implemented**. Recipient lists are in SSM, not in git |
| Operator running SAM | Builds and deploys with profile `datafy` (readme example) | Root `readme.md`, `samconfig.toml` | **Documented** |
| `datafy_web` user | May trigger image analysis from the tasks screen | `datafy_web` PRD, not this repo | **Requires confirmation** that the target is this stack |

No application role check (Admin, Manager, Rep) exists in these handlers. Those roles are in `datafy_web`.

## Systems the product talks to

1. **API Gateway REST**, stage `prod`, region implied by ARNs `eu-west-1`. Routes have no authorizer in the templates.
2. **Lambda Python 3.9**, account `587594388832`, VPC `vpc-0ada1e5366ad5c132`, subnets `subnet-0aa14f7bb45db8b87` and `subnet-0b651a99800a9d99e`. Globals for `datafyNew`: timeout 900 seconds, memory 1024 MB, ephemeral storage 1536 MB. Exceptions are noted per function.
3. **SSM Parameter Store** (decrypted): `/supabase/url`, `/supabase/anon`, `/supabase/service_role` (weekly report only), `/gmail/username`, `/gmail/app_password`, `/gmail/to_emails`, `/gmail/to_emails_client`.
4. **Supabase** via `create_client(url, key)`: PostgREST tables, RPCs, and Storage. The Python client in use does not accept `ClientOptions(http_client=...)`. **Documented** in the root readme and `NewKnowledgeBase.md`.
5. **Gmail SMTP** from `email_service.py`.
6. **S3**: task-insert staging bucket (SAM-managed, lifecycle 1 day on `batches/`); Bedrock artifacts bucket (parameter default `datafy-bedrock-artifacts`); processes training bucket; Rekognition source and training buckets.
7. **Step Functions**: `CreateTasksWorkflow`; `BedrockRetryWorkflow`; `BedrockBatchWorkflow`.
8. **Amazon Bedrock** `InvokeModel` / `Converse` for Claude inference profiles and, on the test function, Qwen.
9. **CloudWatch** logs (JSON log format) and Application Insights (`AutoConfigurationEnabled: true`) on the datafy and processes templates.
10. **Amazon Rekognition Custom Labels** in the Rekognition function code. Not declared in `lambda/Rekognition/template.yaml`.

### Supabase host conflict

| Source | Host or URL | Provenance |
|---|---|---|
| Root `readme.md` dump example | `aws-0-eu-central-1.pooler.supabase.com` port 6543, project ref in the username | **Documented**. Region is eu-central-1, not eu-west-1 |
| `docs/supabase-auth-hook-send-sms-lambda.md` | Self-hosted API `https://supabase.datafy.co.za` | **Documented** |
| `datafy_web` client (other repo) | `https://datafy.appmonitor.co.za` | **Implemented** in that repo |

Which value is stored in SSM `/supabase/url` for the Lambdas is **Unknown / Requires Confirmation**.

## API routes

| Method and path | Stack / stage | Function | Auth in template |
|---|---|---|---|
| `POST /trigger-read-files` | datafyNew / prod | `TriggerReadFiles` | None |
| `OPTIONS` same path | datafyNew / prod | same | CORS preflight route |
| `POST /trigger-read-promos-kaching` | datafyNew / prod | `TriggerReadPromosKaching` | None |
| `POST /generate-combined-week-report` | datafyNew / prod | `CreateCombinedWeeklyReport` | None |
| `POST /analyze-image` | bedrock-analysis / prod | `BedrockImageAnalysis` | None |
| `POST /analyze-image-test` | bedrock-analysis / prod | `BedrockImageAnalysisTest` | None |

`ReadFiles` and `ReadPromosKaching` have no API event. The one-minute schedule on `ReadPromosKaching` is commented out. Provenance: **Implemented** in `lambda/datafy_new/template.yaml`.

## Major workflows

### WF-1 — Trigger an SJ report import

**Implemented** in `lambda/datafy_new/trigger_read_files/app.py`.

Trigger: `POST /trigger-read-files`. The handler does not read the body for validation.

1. Email a start notice (`client_email=True`, process name `SJREPORT Import`).
2. Invoke `READ_FILES_ARN` with `InvocationType=Event` and the raw event as payload.
3. Return HTTP 202 and `{"message": "read_files function triggered"}`.

A second copy at `lambda/trigger_read_files/app.py` does the invoke without the email. It is not a resource in `template.yaml`.

### WF-2 — Import one SJ spreadsheet

**Implemented** in `lambda/datafy_new/read_files/app.py`. **Documented** behaviour for duplicates, timeouts, and failure marking is also in the root readme and changelog entry 2026-02-27.

Trigger: async invoke from WF-1, or a direct invoke. The handler ignores a specific upload id and polls the table.

1. Read SSM and create a Supabase client. Set PostgREST HTTP read timeout to 300 seconds. Call RPC `raw_sql` with `SET statement_timeout = '0'`.
2. Select one `sjreport_uploads` row where `processed` is the string `FALSE` and `times_ran` < 5, limit 1. If none, return an error body `No unprocessed uploads found`.
3. Download the file from Storage bucket `public/{bucket}` using `payload`. Destination table name is `payload.path_tokens[0]`.
4. Load visible Excel sheets only (`sheet_state == visible`). Hidden sheets are skipped. Fallback: all sheets if workbook inspection throws.
5. Fuzzy-match headers (`rapidfuzz`, ratio) to the expected names: `date`, `region`, `channel`, `store_code`, `banner`, `store_name`, `category`, `brand`, `product`, `variant`, `size`, `article`, `barcode`, `store_soh`, `system_dros`, `days_cover`, `last_sold`, `last_receive`, `last_order`, `rsp_value_loss`, `issue_for_feedback`, `status`, `dc_soh`, `customer_id`.
6. A sheet is processed only if it has `date` and `store_code` after matching. If no sheet qualifies, set `processed` true, store the error, email admin and client, and return. The upload is not retried.
7. For each data row, compute `week_start_date` as the row date plus `(7 - weekday)` days (Python Monday=0). Add `row` (1-based), `file`, and `sheetname`. Upsert in batches on conflict `row, week_start_date, file, sheetname`.
8. Then, each in its own try/except: `insert_distinct_products` (ignore Postgres `23505`), loop `batch_update_sjreport_product_id` with `p_batch_size` 1000 and `p_hours` or `p_hours_back` 1 until empty, and the same loop shape for `update_sjreport_customer_ids` and `update_product_customer_ids`. RPC failure is logged, emailed, stored on the upload, and does not leave `processed` false (changelog 2026-02-27).
9. Invoke `BatchUpdateSjreport` using the hardcoded function ARN in the template (not `!GetAtt`). Payload includes `week_start_date` and `record_id`.
10. Top-level exceptions also set `processed` true.

A commented block that prefixed SPAR `store_code` with `S` is not active.

### WF-3 — Build tasks after import

**Implemented.** **Documented** in `CREATE_TASKS_CONTEXT.md`.

`BatchUpdateSjreport` requires `week_start_date` and `record_id` (HTTP 400 if either is missing). The in-function product-id batch update is commented out (`total_updated` stays 0). It asynchronously invokes `CreateTasksBulk` with only `week_start_date`, then sets the upload `processed` true.

`CreateTasksBulk`:

1. Pages `get_sjreport_data_pages_v2` for that week.
2. Transforms rows into task payloads (including `customer_id` with a trailing `.0` stripped).
3. Uploads JSON batches of 5,000 rows to `batches/{week_start_date}/{YYYYmmdd_HHMMSS}/`.
4. Starts `CreateTasksWorkflow` with those S3 items.

State machine:

1. `CreateTasksListBatches` — if `items` were passed, pass them through; otherwise list S3 under `batches/{date}/`.
2. Map `InsertTasksBatch` — read one object and upsert `task` on conflict `chain,store_name,product_id,week_start_date`. Timeout 300 seconds, memory 512 MB.
3. `CreateTasksFinalize` — RPC `update_store_issue_counts`, async invoke `StartProcessQueue`, success email.

`CreateTasks` (in-process upsert, 1,000-row batches, then the same finalize-style calls) remains in the template. The import chain does not call it. `CREATE_TASKS_CONTEXT.md` says to use it for small runs because it times out around 10,000 tasks. Provenance: **Documented** and **Implemented** as a second entry point with no API event.

Starting the state machine with only `week_start_date` and no prior bulk upload yields empty `items`. Finalize still runs. **Documented** in `CREATE_TASKS_CONTEXT.md`.

### WF-4 — Post-task queue, backup, and purge

**Implemented** in `start_process_queue/app.py` and the backup/clear handlers. **Documented** in `FLOWCHART.md`.

`StartProcessQueue` expects `week_start_date`.

1. Loop RPC `update_task_customer_ids`. On error, email and continue.
2. RPC `archive_old_tasks`. On error, email and continue.
3. Delete all `store_processing_queue` rows with `id != 0`, then RPC `populate_store_process_queue`. On error, email and continue.
4. Async invoke `BackupSjreport`.
5. Success email. Uncaught exceptions return HTTP 500 and an error email.

`BackupSjreport` pages `get_old_sjreport_rows_paged` (10,000). If empty, it returns without clearing. Otherwise it uploads `sjreport_old_dates_backup_{timestamp}.csv` to Storage bucket `uploads/backups` and async-invokes `SJReportClearOldData`.

`SJReportClearOldData` calls `clear_old_sjreport_records` and async-invokes `BackupTasks`.

`BackupTasks` pages `get_old_task_rows_paged` and async-invokes `TasksClearOldData`.

`TasksClearOldData` calls `clear_old_task_records`.

What "old" means is defined inside those RPCs, which are not in this repo. **Unknown** beyond the backup log text "all records have the maximum week_start_date".

### WF-5 — Promo or KaChing spreadsheet import

**Implemented** in `read_promo_kaching/app.py`, started by `POST /trigger-read-promos-kaching` (start email, async invoke, HTTP 202).

1. Next `promo_kaching_uploads` row with `processed` string `FALSE`, limit 1. No `times_ran` cap.
2. Table name is `payload.path_tokens[0]`. File must end with `.xlsx` or the upload is marked processed, the storage object is deleted, and HTTP 400 is returned.
3. Visible sheets only. Headers lowercased with spaces replaced by underscores. No fuzzy match and no required-column list.
4. Drop `id` and `created_at` if present. Upsert in batches of 1,000 on conflict `row,file, sheetname` (one call site) or `row, file, sheetname` (the other). The comma spacing differs in source.
5. Success sets `processed` true and clears `error`. Failures also set `processed` true, email, and delete the storage object.

The template schedule `rate(1 minute)` is commented out. Cron is not active in this template.

### WF-6 — Combined weekly report

**Implemented** in `create_combined_weekly_report/app.py`. **Documented** in the root readme, `lambda/datafy_new/README.md`, changelog 2026-03-12, and `NewKnowledgeBase.md`.

Triggers:

- `POST /generate-combined-week-report` with `customer_id` or `customerId`, and optional `report_id` or `reportId`.
- Schedule `cron(0 3 * * ? *)` (03:00 UTC daily) with input `{"customer_id": 1,"is_cron": true}`.

Behaviour:

1. Prefer SSM `/supabase/service_role`. If that read fails, use `/supabase/anon` and log a warning. Changelog states the anon key returned 0 rows because the RPC is `SECURITY INVOKER`.
2. Page RPC `get_combined_week_report_step_by_step` with `page_offset` 0 and `limit_count` 10000 until a short page.
3. Build an Excel file, upload to Storage bucket `Reports`, insert `report_files`, and return the public URL.
4. Send the client email only when the event source is `aws.events` or the payload has `is_cron` / `isCron` / `send_email` / `sendEmail`. API calls without that flag generate the file and do not email.

### WF-7 — KaChing daily report

**Implemented** in `create_kaching_daily_report/app.py`.

Trigger: schedule `cron(0 3 * * ? *)` with `{"customer_id": 1}`. No API event.

Calls RPC `get_active_kachings_with_activity_daily`, writes Excel to Storage bucket `Reports`, and emails. The cron input does not set `is_cron`. Whether this handler gates email the same way as WF-6 was not confirmed line by line. **Requires confirmation** if a manual invoke should stay silent.

### WF-8 — Update store issues and task status

**Implemented.** No schedule and no API event. Something outside the template must invoke them. **Unknown** what does.

`UpdateStoreIssues` pages `get_store_data_pages` (limit 10), then `get_issue_counts`, `update_store_issues`, and `update_total_counts`. Errors email process name `Store Issues Update` with `client_email=False`.

`UpdateTaskIssues` requires `week_start_date`. The RPC call is `supabase.rpc('update_task_status_with_temp', { date })`. In Python that second argument is a set, not a dict of named parameters. Provenance: **Implemented** source. Whether invocations succeed is **Unknown / Requires Confirmation**.

### WF-9 — Analyse one shelf image (production)

**Implemented** in `lambda/bedrock_analysis/app/app.py`. **Documented** in `lambda/bedrock_analysis/README.md`, `BEDROCK_SETUP.md`, `NewKnowledgeBase.md`, and the changelog. The README opening line still says "Claude 3 Vision". The template model id is Claude Sonnet 4.6.

Trigger: `POST /analyze-image` with JSON `image_id` and/or `task_id`, or a direct invoke with those fields at the event root (Step Functions and the Lambda console).

1. Load `product_images` by `image_id`, or `task` by `task_id` when there is no image row. Shelf bytes come from the image path or `task.image_url`. Product reference comes from `product.image_path`.
2. Apply EXIF orientation, resize, and send the image(s) to Bedrock. Primary `INFERENCE_PROFILE_ARN` is `eu.anthropic.claude-sonnet-4-6`. On throttling after retries, one call uses `BEDROCK_FALLBACK_INFERENCE_PROFILE_ARN`. The template default for that parameter is also `eu.anthropic.claude-sonnet-4-6`. The parameter description says an example fallback is Sonnet 4.5 and that empty disables it. Deployed behaviour with the default is a fallback to the same profile. **Discrepancy.**
3. Parse a structured block: `FINAL_RESULT`, `CONFIDENCE`, `FACINGS_COUNT`, `POSITIONS` (JSON array of shelf, position, facings), `HAS_PI_LABEL`. Positions include left, center-left, center, center-right, and right (changelog 2026-02-23). The README API section still says left, center, or right only.
4. The prompt rules in code (exact product, size must match when size is set, shelf 1 is the top row, exact integer counts) are **Implemented** and **Documented** in the changelog and knowledge base. The full prompt text is the requirement. It is not copied here.
5. Write `processed_genai`, result, message, confidence, facing count, position, and label onto `product_images` when `image_id` is set, and onto `task`. False results clear previous facing/position/label values (changelog 2026-02-20).
6. Errors set `processed_genai` false and `genai_error` on the image row when an image id exists.
7. If `GENAI_ARTIFACTS_S3_BUCKET` is non-empty, write `shelf_image.jpg`, `product_image.jpg` (when a product image exists), `prompt.txt`, and `response.txt` under `artifacts/{id}/{timestamp}/`. Template default bucket name is `datafy-bedrock-artifacts`, so artifacts are on unless the parameter is overridden to empty. The README says the default is empty. **Discrepancy.**
8. Log one `BedrockUsage` line with token counts and `model_id`.
9. OPTIONS returns HTTP 200. Other errors return HTTP 500 with an error string.

### WF-10 — Bedrock retry and batch

**Implemented** in the list Lambdas, `bedrock_batch_analyze/app.py`, and the state machines. **Documented** in the Bedrock README and knowledge base (later entries override earlier changelog text that still queried `product_images`).

Retry list (`ListBedrockRetryTasks`), task table only:

- `fixed_at` is not null and falls in the SAST calendar day (`Africa/Johannesburg`), or in `run_date` when the execution input provides `YYYY-MM-DD`.
- Task GenAI is incomplete (`processed_genai` is not true).
- `task.image_url` is non-empty. Tasks without a shelf URL are skipped.
- Page size `RETRY_LIST_BATCH_SIZE` default 500. Items are `{ "task_id" }` only.
- Map invokes `BedrockImageAnalysis` with `MaxConcurrency` default 2 and `ResultPath: null`.
- Schedule default on: `cron(0 20 * * ? *)` (20:00 UTC, described as 22:00 SAST) with input `{}`.

Batch list groups the same task filter by `product_id`, chunks to `BedrockBatchMaxShelfImages` default 6, page size default 80. Tasks without `product_id` or `image_url` are skipped. `BedrockBatchAnalyze` sends one product reference plus N shelf images and writes the same result columns. The README says this path does not use the fallback profile. Map concurrency default is 2. No EventBridge schedule is defined for the batch workflow.

### WF-11 — Crop training images from Pascal VOC XML

**Implemented** in `lambda/processes/get_training_images/app.py`, logical id `ConvertTrainingImages`.

Trigger: `s3:ObjectCreated:*` on `TrainingS3Bucket` for suffix `.xml`. Timeout 600 seconds. Policy `AmazonS3FullAccess`.

1. Decode the key. Expect a sibling `.jpg` with the same stem.
2. Retry GetObject up to 12 times, 5 seconds apart.
3. For each XML `<object>`, crop `<bndbox>` and write the crop under a folder named by `<name>`.

`ConvertXmlToJson` (`convert_xml_to_json/`) is commented out in the template. The code remains.

### WF-12 — Rekognition Custom Labels (code present, default template does not deploy it)

**Documented** in `lambda/Rekognition/README.md` and `REKOGNITION_PROJECT_GUIDE.md`. **Implemented** as Python under `lambda/Rekognition/functions/`. **Not implemented** as resources in `lambda/Rekognition/template.yaml`.

The README says image processing runs daily at 02:00 UTC and training runs Sundays at 01:00 UTC. In `template-copy.yaml` both `ScheduleExpression` blocks are commented out. Project id `1725357683732` and project name `Datafy` are **Documented** requirements in that README. Whether they match a live AWS project was not verified.

`analyze_image` reads `product_images` and `product_ml` and writes predictions. It is a different function from Bedrock `POST /analyze-image`.

## Functional requirements

### FR-001 — Poll SJ uploads

The system processes at most one `sjreport_uploads` row per `ReadFiles` invocation, and only when `processed` is `FALSE` and `times_ran` is less than 5.

Evidence: `read_files/app.py` `get_unprocessed_upload`. Implementation: **Implemented**. Confidence: **High**.

### FR-002 — Required SJ columns

A sheet is imported only when fuzzy-matched headers include `date` and `store_code`. If no visible sheet qualifies, the upload is marked processed and is not retried, and admin and client email are sent.

Evidence: `read_files/app.py`; changelog 2026-02-27. Implementation: **Implemented**. Confidence: **High**.

### FR-003 — SJ week date

Each imported row stores `week_start_date` as the parsed `YYYY-MM-DD` date plus `(7 - weekday)` days.

Evidence: `process_row` in `read_files/app.py`. The business name for that Monday is **Inferred**. Confidence: **High** on the formula.

### FR-004 — Duplicate products do not fail the import

`insert_distinct_products` errors with SQLSTATE `23505` are ignored. Other RPC errors are recorded and emailed, and the upload is still marked processed.

Evidence: root readme; `read_files/app.py`. Implementation: **Implemented**. Confidence: **High**.

### FR-005 — Import continues into task creation

A successful `ReadFiles` invoke starts `BatchUpdateSjreport`, which starts `CreateTasksBulk`, which starts the Step Functions insert, which starts `StartProcessQueue` and the backup chain.

Evidence: handlers and template env ARNs, except the `ReadFiles` → `BatchUpdateSjreport` ARN which is a literal. Implementation: **Implemented**. Confidence: **High**. The literal ARN will not follow a stack rename. **Documented** as a migration defect in `migration.md`.

### FR-006 — Task identity

Task upsert conflict key is `chain, store_name, product_id, week_start_date`.

Evidence: `create_tasks/app.py` and `insert_tasks_batch/app.py`. Implementation: **Implemented**. Confidence: **High**.

### FR-007 — Weekly report email is cron-gated

`CreateCombinedWeeklyReport` emails only for EventBridge or an explicit cron/send flag. The deployed schedule sends `customer_id` 1 and `is_cron` true.

Evidence: `is_cron_trigger`; template `Input`. Implementation: **Implemented**. Whether customer 1 is the only customer is **Unknown / Requires Confirmation**. `datafy_web` also forces selected customer 1 after login.

### FR-008 — Weekly report uses the service role

The weekly report Lambda reads `/supabase/service_role` so the invoker-rights RPC returns rows.

Evidence: changelog 2026-03-12; `create_combined_weekly_report/app.py`; IAM in the template. Implementation: **Implemented**. Confidence: **High**. Other Lambdas use the anon key. If RLS hides rows, those Lambdas return empty. **Documented** in `NewKnowledgeBase.md`.

### FR-009 — Shelf analysis persists GenAI columns

`POST /analyze-image` writes the model outcome onto `task` and, when an image id was used, onto `product_images`, and records token usage in logs.

Evidence: `app.py` `update_analysis_result`; README response example. Implementation: **Implemented**. Confidence: **High**.

### FR-010 — Test analysis does not persist

`POST /analyze-image-test` calls Bedrock and does not write `task`, `product_images`, artifacts, or `genai_error`.

Evidence: Bedrock README; `test_app.py` (changelog). Implementation: **Implemented**. Confidence: **High** from documentation plus the dedicated handler. The test file was not executed.

### FR-011 — Daily GenAI retry

When `EnableBedrockRetrySchedule` is true (the default), EventBridge starts the retry state machine every day at 20:00 UTC for tasks fixed on the current SAST date whose GenAI work is incomplete and whose `image_url` is set.

Evidence: `template.yaml` rule; `list_retry_tasks/app.py`; README. Implementation: **Implemented** in template and code. Whether the stack is deployed with the default is **Unknown**.

### FR-012 — Public HTTP entry points

The six POST routes above are defined without an authorizer. CORS allows origin `*`.

Evidence: both API resources. Implementation: **Implemented** in templates. Whether API Gateway resource policies or a usage plan exist outside git is **Unknown**.

## Error conditions

| Condition | Behaviour | Provenance |
|---|---|---|
| No SJ upload waiting | `ReadFiles` returns an error body and does not email a start (the trigger already emailed) | Implemented |
| SJ file missing required columns | Upload `processed=true`, error stored, email | Implemented |
| SJ insert or RPC failure | Upload `processed=true`, error stored, email, function returns | Implemented |
| `BatchUpdateSjreport` missing `week_start_date` or `record_id` | HTTP 400 | Implemented |
| Promo file not `.xlsx` | HTTP 400, upload processed, storage object deleted, email | Implemented |
| Bedrock throttling | Backoff, then one fallback-profile call | Implemented. Default fallback id equals the primary |
| Bedrock or DB error on production analyse | HTTP 500; `genai_error` and `processed_genai=false` when an image id is known | Implemented |
| Image id not found | Exception, HTTP 500 | Implemented |
| Backup finds no old rows | HTTP 200, clear step not invoked | Implemented |
| `UpdateTaskIssues` RPC argument | Set literal passed to `rpc` | Implemented source; success **Unknown** |
| Missing SSM parameter | `get_parameter` raises; the Lambda fails | Implemented |

## Constraints

- Lambda timeout 15 minutes on most `datafyNew` functions. Bulk task insert exists because the in-process path times out near 10,000 tasks. **Documented** in `CREATE_TASKS_CONTEXT.md`.
- `InsertTasksBatch` timeout is 300 seconds.
- Bedrock Map concurrency defaults to 2 to stay under InvokeModel quotas. **Documented**.
- Qwen test images are capped by `BEDROCK_TEST_CONVERSE_MAX_IMAGE_BYTES` default 1,500,000 bytes. **Documented**.
- Staging JSON under `batches/` expires after 1 day. **Implemented** lifecycle on `CreateTasksStagingBucket`.
- Lambdas run in private subnets and need NAT for Supabase, Bedrock, SSM, and Gmail. **Documented** in `docs/private-subnet-lambda-sam.md` and the template description.

## Unknown / Requires Confirmation

- Production base URLs for `ReadFilesApi` and `BedrockAnalysisApi`.
- Whether `https://i78eyrnc87.execute-api.eu-west-1.amazonaws.com/Stage/analyze-image` (called by `datafy_web`) is this stack. This template uses stage `prod`.
- The live Supabase URL and whether migration off the hosted project has happened.
- Who may call the unauthenticated routes.
- Whether `UpdateStoreIssues` and `UpdateTaskIssues` are invoked in production, and by what.
- Whether customer id 1 on the crons is correct for every environment.
- Whether the Rekognition copy template, or the separate `datafy_rekognition` repository, is the live Custom Labels system.
- Business meaning of the week-start formula (next Monday, including a 7-day shift when the row date is already Monday).
