Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
13 changes: 10 additions & 3 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
27 changes: 24 additions & 3 deletions docs/lets-talk.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Original file line number Diff line number Diff line change
@@ -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');
21 changes: 17 additions & 4 deletions scripts/publish-lets-talk.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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(
Expand All @@ -14,7 +27,7 @@ async function publishLetsTalk(): Promise<void> {
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],
Expand All @@ -34,7 +47,7 @@ async function publishLetsTalk(): Promise<void> {
`${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) => {
Expand Down
Loading