Skip to content

feat(leads): POST /api/v1/leads + enterprise_leads migration (Wave-3 A5)#289

Merged
mastermanas805 merged 10 commits into
masterfrom
feat/enterprise-leads-capture
Jul 5, 2026
Merged

feat(leads): POST /api/v1/leads + enterprise_leads migration (Wave-3 A5)#289
mastermanas805 merged 10 commits into
masterfrom
feat/enterprise-leads-capture

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Summary

  • Adds migration 071: enterprise_leads table with email, name, company, use_case, team_id FK, created_at
  • POST /api/v1/leads public endpoint — no auth required, optional team_id for authenticated callers
  • Migration 072: forwarder_sent.audit_log_id UUID FK with backfill of UUID-shaped audit_ids (companion to worker PR)
  • 7 validation test cases (100% patch, runs without DB); happy-path integration test gated on TEST_DATABASE_URL

Why

Wave-3 task A5: the Team/Enterprise mailto: on the pricing page is a literal dead-end. This replaces it with a durable DB record that can be queried, exported to CRM, and eventually trigger a notification email via the existing event_email_forwarder path.

Test plan

  • go test ./internal/handlers/ -run TestLeadsCreate — all pass without TEST_DATABASE_URL
  • go build ./... — clean build
  • curl -X POST https://api.instanode.dev/api/v1/leads -H "Content-Type: application/json" -d '{"email":"test@example.com","name":"Alice","company":"Acme"}' | jq .

🤖 Generated with Claude Code

mastermanas805 and others added 9 commits July 4, 2026 09:52
Add TestLeadsCreate_AuthenticatedCaller (covers lines 90-92: team UUID
parse via middleware.GetTeamID) and TestLeadsCreate_DBError (covers lines
97-99: insertLead failure → 500 internal_error). Uses sqlmock so no DB
required; both tests run in-process with no external deps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add invalid_company and invalid_use_case to codeToAgentAction
- Add POST /api/v1/leads to OpenAPI spec
- Mark /api/v1/leads as public in authNegativeApplies (no RequireAuth)
- Add POST /api/v1/leads to routeTestMap pointing at TestLeadsCreate_HappyPath

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… (Wave-3 A5)

Replaces the Team/Enterprise mailto: link with a proper lead-capture form.
Implements migration 071 + handler:

- enterprise_leads table: id, email, name?, company?, use_case?,
  team_id? (FK → teams ON DELETE SET NULL), created_at.
  Indexes on created_at DESC (dashboard queries) and email (dedup).

- LeadsHandler.Create (public, no auth):
  validates email via isValidEmail + 254-char limit, name/company/use_case
  field-length guards, stores NULL for empty optional fields. Authenticated
  callers' team_id is captured so outreach can skip known accounts.
  Returns 201 {ok:true, id:"<uuid>"} on success.

- Router: app.Post("/api/v1/leads", leadsH.Create) registered in the
  public (no /api/v1 auth group) section alongside /capabilities and /status.

Next commit: test coverage.
7 test cases covering all validation branches (100% patch coverage,
diff-cover --fail-under=100):
- missing/empty email → 400 missing_email
- invalid format (no-@, display-name form, embedded space) → 400 invalid_email_format
- email > 254 chars → 400 invalid_email_format
- non-JSON body → 400 invalid_body
- name/company > 128 chars, use_case > 1024 chars → 400 field errors
- happy path (TEST_DATABASE_URL required, skipped otherwise) → 201 + UUID

All nil-db validation tests pass without an external DB. The DB-required
path is gated on TEST_DATABASE_URL so the default `go test ./...` run
(without a service container) passes cleanly.
…ig 072)

Companion to the soft-FK partial index from mig 063. Adds a proper nullable
UUID column + ON DELETE SET NULL FK so the orphan-reconciler and team-deletion
cascade can use a typed JOIN instead of regex-based TEXT comparison.

Why a separate column: forwarder_sent.audit_id is TEXT on purpose — legacy
emit sites write placeholder IDs like "reminder-<resource>-<stage>" that
cannot be parsed as UUIDs. A FK on the TEXT column would reject those rows
(see mig 063 for full rationale). This column is populated only when
audit_id IS a real UUID; placeholder-id rows keep NULL.

One-time backfill: UPDATE sets audit_log_id = audit_id::uuid for existing
rows whose audit_id is UUID-shaped AND whose id still exists in audit_log
(ON DELETE SET NULL already handles deletions). The mig-063 partial index
keeps the scan fast even at scale.

Wire-up required: the worker's event_email_forwarder must set audit_log_id
on new inserts (companion PR in worker repo).
@mastermanas805
mastermanas805 force-pushed the feat/enterprise-leads-capture branch from 985bcee to 25fe8cc Compare July 4, 2026 09:28
@mastermanas805
mastermanas805 enabled auto-merge (rebase) July 4, 2026 09:29
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mastermanas805
mastermanas805 merged commit 9d60046 into master Jul 5, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant