diff --git a/README.md b/README.md index a498b3f..82749b9 100644 --- a/README.md +++ b/README.md @@ -104,5 +104,5 @@ TEST_DATABASE_URL=postgresql://forms:forms_local@127.0.0.1:5546/forms?schema=for Coverage includes actual PostgreSQL writes, all field types, SQL views, CSV output, concurrent idempotency, immutable versions, JWT access, required-answer/ownership constraints, Payload synchronization over HTTP, and browser rendering/retry behavior. CI executes the same checks with PostgreSQL 17. Forms portal endpoints, inclusive UTC date filtering, and full CSV exports are -specified in [docs/api.md](docs/api.md). The development-only `/lets-talk` migration, +specified in [docs/api.md](docs/api.md). The dev and production `/lets-talk` migration, Kafka contract, seed command, and rollout order are in [docs/lets-talk.md](docs/lets-talk.md). diff --git a/docs/data-model.md b/docs/data-model.md index 2047a33..2db65d6 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -114,9 +114,10 @@ Versions and historical reporting views are retained. The service does not impos The API owns their migrations; the processor uses these schema-qualified tables without running DDL at startup. A flow has a stable ID, form key, enabled flag, AND-combined answer predicates (`rules.all`), action name, and JSON action settings. -The seeded `lets-talk-sales-email` flow matches all `lets-talk` submissions and is -disabled with TBD recipients, sender, and template. Manage it externally in SQL; -set `updatedAt` when changing settings. Disabling delivery preserves queued work. +The seeded `lets-talk-sales-email` flow matches all `lets_talk` submissions and is +disabled initially. Enable it externally in SQL after configuring the processor +SSM recipients/template; email-service-v6 supplies the sender. Set `updatedAt` +when changing the flow. Disabling delivery preserves queued work. The inbox stores the submitted event before Kafka commit. Routing records a unique receipt per submission/flow, including disabled matching flows. Retries read current @@ -135,3 +136,9 @@ The `sendgrid-email` action name is retained for compatibility, but forms-proces now publishes the v3 email contract to `external.action.email` through Bus API. `deliveredAt` records Bus API acceptance; email-service-v6 handles provider delivery and retries. Optional `fromEmail` overrides that service's default sender. + +The additive `20261013010000_lets_talk_processor_key` migration corrects the +original seed key and reroutes retained events lacking this flow receipt. Existing +receipts, rules, settings and the enabled flag are preserved. The processor now +overrides action settings for this flow from its two `LETS_TALK_*` SSM parameters; +other flows continue using database settings. Enabling remains an operational update. diff --git a/docs/lets-talk.md b/docs/lets-talk.md index 226253f..d98960a 100644 --- a/docs/lets-talk.md +++ b/docs/lets-talk.md @@ -1,4 +1,4 @@ -# Development /lets-talk migration +# /lets-talk rollout `examples/lets-talk.json` mirrors the form schema returned by `https://www.topcoder.com/__api/forms/XpPgFzW8lmX8frlStzzg0` on 2026-09-28. @@ -40,12 +40,13 @@ existing Bus API integration. Consumers must deduplicate by `submissionId`. ``` `FORMS_ADMIN_TOKEN` must be set without putting it in source control. The script - hardcodes the development API, creates no submissions, and fails on differing + defaults to the development API. Set `FORMS_API_BASE_URL=https://api.topcoder.com/v6` + for production. It creates no submissions and fails on differing immutable content. Re-running an identical version is safe; subsequent schema changes require a reviewed next version. 4. Deploy the companion `topcoder-website` develop change and the `platform-ui` dev change. No CMS content mutation is required: the website selects the known form - component only on the development `/lets-talk` route. Production and other + component on the matching dev or production `/lets-talk` route. Other routes continue using their existing forms. 5. Verify the public schema, then submit an explicitly marked development test with Kafka configured. Confirm the receipt, event, table row, date filtering, and CSV. @@ -55,3 +56,23 @@ The React integration accepts `kafka` (false by default) and `hiddenAnswers` (empty by default). Hidden answers are merged into the API payload and omitted from visible controls. They must match stored field types and validation rules. The website maintains its analytics adapter around the same rendering contract. + +## Production release order + +1. Release the API migration `20261013010000_lets_talk_processor_key`. It corrects + the initial flow's `lets-talk` key to `lets_talk` without changing its enabled + state or settings, and reroutes retained submissions missing a delivery receipt. + The existing migration remains unchanged. Existing successful receipts are preserved. +2. Publish the reviewed version using the command above with + `FORMS_API_BASE_URL=https://api.topcoder.com/v6` and a production admin token. +3. Deploy forms-processor-v6 through its master pipeline. Set SecureStrings + `/config/forms-processor-v6/appvar/LETS_TALK_RECIPIENTS` (comma-separated emails) + and `/config/forms-processor-v6/appvar/LETS_TALK_SENDGRID_TEMPLATE_ID` (dynamic + template ID). Redeploy to load changed parameters, then enable the database + flow after reviewing any queued submissions. See the processor rollout guide. +4. Release the website production selection. Confirm the public schema and exact + `https://www.topcoder.com` CORS origin before switching the live page. +5. Submit an identified test with `kafka: true`, correlate its submission ID through + `form.submitted` and `external.action.email`, and verify the received email. + Processor `deliveredAt` means Bus API acceptance; verify provider/inbox delivery + separately. No historical HubSpot data is imported. diff --git a/prisma/migrations/20261013010000_lets_talk_processor_key/migration.sql b/prisma/migrations/20261013010000_lets_talk_processor_key/migration.sql new file mode 100644 index 0000000..e06a071 --- /dev/null +++ b/prisma/migrations/20261013010000_lets_talk_processor_key/migration.sql @@ -0,0 +1,15 @@ +-- The published form key is lets_talk; preserve the flow's settings and enabled state. +UPDATE forms."ProcessorFlow" +SET "formKey" = 'lets_talk', "updatedAt" = CURRENT_TIMESTAMP +WHERE id = 'lets-talk-sales-email' AND "formKey" = 'lets-talk'; + +-- Retry routing retained submissions that previously missed this flow. Existing +-- delivery receipts stay intact, so already accepted emails are not replayed. +UPDATE forms."ProcessorEvent" e +SET "routedAt" = NULL, "nextAttemptAt" = CURRENT_TIMESTAMP +WHERE e."formKey" = 'lets_talk' + AND EXISTS (SELECT 1 FROM forms."ProcessorFlow" f + WHERE f.id = 'lets-talk-sales-email' AND f."formKey" = 'lets_talk') + AND NOT EXISTS (SELECT 1 FROM forms."ProcessorDelivery" d + WHERE d."submissionId" = e."submissionId" + AND d."flowId" = 'lets-talk-sales-email'); diff --git a/scripts/publish-lets-talk.ts b/scripts/publish-lets-talk.ts index 3321531..b86d526 100644 --- a/scripts/publish-lets-talk.ts +++ b/scripts/publish-lets-talk.ts @@ -1,11 +1,24 @@ import { readFile } from 'node:fs/promises'; /** - * Idempotently publishes the reviewed /lets-talk definition to the development Forms API only. + * Idempotently publishes the reviewed /lets-talk definition to the selected Forms API (development by default). + * @param No arguments; reads FORMS_API_BASE_URL and FORMS_ADMIN_TOKEN from the environment. * @returns Completion after registration, immutable version save, and publication; creates no submissions. - * @throws Error for a missing FORMS_ADMIN_TOKEN, conflicting revision, or failed API request. + * @throws Error for a missing token, unsupported API base, conflicting revision, or failed API request. */ async function publishLetsTalk(): Promise { + const apiBase = ( + process.env.FORMS_API_BASE_URL ?? 'https://api.topcoder-dev.com/v6' + ).replace(/\/+$/, ''); + if ( + ![ + 'https://api.topcoder-dev.com/v6', + 'https://api.topcoder.com/v6', + ].includes(apiBase) + ) + throw new Error( + 'FORMS_API_BASE_URL must select the dev or production Topcoder API.', + ); const token = process.env.FORMS_ADMIN_TOKEN; if (!token) throw new Error( @@ -14,7 +27,7 @@ async function publishLetsTalk(): Promise { const definition: unknown = JSON.parse( await readFile('examples/lets-talk.json', 'utf8'), ); - const base = 'https://api.topcoder-dev.com/v6/forms'; + const base = `${apiBase}/forms`; for (const [method, path, body] of [ ['POST', '', { key: 'lets_talk' }], ['PUT', '/lets_talk/versions/1', definition], @@ -34,7 +47,7 @@ async function publishLetsTalk(): Promise { `${method} ${path} failed (${response.status}); no credentials or response data logged.`, ); } - console.log('Published lets_talk version 1 to the development Forms API.'); + console.log(`Published lets_talk version 1 to ${apiBase}.`); } void publishLetsTalk().catch((error: unknown) => {