diff --git a/docs/data-model.md b/docs/data-model.md index c4ae0c5..083dd74 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -134,6 +134,13 @@ enabled. Migration `20261022010000_lets_talk_classification` seeds the Let's tal classify and label flows disabled; forms-processor-v6 ADR 0001 and its `examples/lets-talk-shadow-mode.sql` / `lets-talk-cutover.sql` describe the rollout. +Migration `20261102010000_space_tech_challenge_email` seeds `space-tech-challenge-email`, +which emails every `space_tech_challenge` submission to a fixed list, with the submitter's +`email` answer as reply-to. It is disabled with empty settings: forms-processor-v6 supplies +the recipients and template from `SPACE_TECH_CHALLENGE_RECIPIENTS` and +`SPACE_TECH_CHALLENGE_TEMPLATE_ID`. Enable it in SQL after both are set and the processor +is redeployed. + Processor events contain answers and must be included in personal-data retention and erasure procedures separately from `Submission`. Deleting a ProcessorEvent cascades its delivery records and removes deduplication protection, so retain it diff --git a/examples/space-tech-challenge.json b/examples/space-tech-challenge.json new file mode 100644 index 0000000..28efa36 --- /dev/null +++ b/examples/space-tech-challenge.json @@ -0,0 +1,36 @@ +{ + "title": "Share your space-data challenge", + "description": "", + "successMessage": "Thanks for sharing your challenge. We'll get back to you within 48 hours on whether it's a fit.", + "access": "ANONYMOUS", + "fields": [ + { + "key": "full_name", + "label": "Full Name", + "type": "TEXT", + "required": true, + "maxLength": 200 + }, + { + "key": "company", + "label": "Company", + "type": "TEXT", + "required": true, + "maxLength": 200 + }, + { + "key": "email", + "label": "Work Email", + "type": "EMAIL", + "required": true, + "maxLength": 254 + }, + { + "key": "challenge", + "label": "Which challenge are you trying to solve?", + "type": "TEXTAREA", + "required": true, + "maxLength": 5000 + } + ] +} diff --git a/prisma/migrations/20261102010000_space_tech_challenge_email/migration.sql b/prisma/migrations/20261102010000_space_tech_challenge_email/migration.sql new file mode 100644 index 0000000..e41820a --- /dev/null +++ b/prisma/migrations/20261102010000_space_tech_challenge_email/migration.sql @@ -0,0 +1,8 @@ +-- Internal notification for SpaceTech landing page challenges (form space_tech_challenge). +-- Every submission goes to a fixed recipient list, with the submitter as reply-to. +-- forms-processor-v6 supplies the settings from SPACE_TECH_CHALLENGE_RECIPIENTS and +-- SPACE_TECH_CHALLENGE_TEMPLATE_ID (SSM); they replace these database settings. +-- Seeded disabled: enable it after both parameters are set and the processor is redeployed. +INSERT INTO forms."ProcessorFlow" (id, "formKey", action, rules) +VALUES ('space-tech-challenge-email', 'space_tech_challenge', 'sendgrid-email', '{"all":[]}') +ON CONFLICT (id) DO NOTHING; diff --git a/scripts/publish-space-tech-challenge.ts b/scripts/publish-space-tech-challenge.ts new file mode 100644 index 0000000..79199c2 --- /dev/null +++ b/scripts/publish-space-tech-challenge.ts @@ -0,0 +1,56 @@ +import { readFile } from 'node:fs/promises'; + +/** + * Idempotently publishes the SpaceTech landing page 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 token, unsupported API base, conflicting revision, or failed API request. + */ +async function publishSpaceTechChallenge(): 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( + 'FORMS_ADMIN_TOKEN with manage:forms or Administrator access is required.', + ); + const definition: unknown = JSON.parse( + await readFile('examples/space-tech-challenge.json', 'utf8'), + ); + const base = `${apiBase}/forms`; + for (const [method, path, body] of [ + ['POST', '', { key: 'space_tech_challenge' }], + ['PUT', '/space_tech_challenge/versions/1', definition], + ['POST', '/space_tech_challenge/versions/1/publish', undefined], + ] as const) { + const response = await fetch(`${base}${path}`, { + method, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + ...(body ? { body: JSON.stringify(body) } : {}), + signal: AbortSignal.timeout(30000), + }); + if (!response.ok) + throw new Error( + `${method} ${path} failed (${response.status}); no credentials or response data logged.`, + ); + } + console.log(`Published space_tech_challenge version 1 to ${apiBase}.`); +} + +void publishSpaceTechChallenge().catch((error: unknown) => { + console.error(error instanceof Error ? error.message : 'Publication failed.'); + process.exitCode = 1; +}); diff --git a/test/forms.integration.spec.ts b/test/forms.integration.spec.ts index 986ae36..59e55de 100644 --- a/test/forms.integration.spec.ts +++ b/test/forms.integration.spec.ts @@ -422,6 +422,20 @@ describe('forms API with real PostgreSQL', () => { ); }); + it('seeds the SpaceTech challenge email disabled, with settings left to the processor environment', async () => { + const flows = await db.processorFlow.findMany({ + where: { formKey: 'space_tech_challenge' }, + }); + expect(flows).toHaveLength(1); + expect(flows[0]).toMatchObject({ + id: 'space-tech-challenge-email', + enabled: false, + action: 'sendgrid-email', + rules: { all: [] }, + settings: {}, + }); + }); + it('reports AI classification columns and filters by label only for forms with a classify flow', async () => { const key = await create({ ...definition(),