Skip to content

Fix PandaDoc webhook: fetch fields via API and create applications#248

Open
rayyanmridha wants to merge 21 commits into
mainfrom
rm-221-pandadoc-webhook-create-app
Open

Fix PandaDoc webhook: fetch fields via API and create applications#248
rayyanmridha wants to merge 21 commits into
mainfrom
rm-221-pandadoc-webhook-create-app

Conversation

@rayyanmridha

@rayyanmridha rayyanmridha commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Root cause: PandaDoc webhooks only send event metadata (doc ID, event type) — not form field values. The handler was trying to read fields directly from the webhook payload, which never worked.
  • Fix: Receive webhook → extract document ID → call PandaDoc fields API (`GET /documents/{id}/fields`) → map fields → create Application + CandidateInfo + LearnerInfo in a transaction.
  • Field mapping fixes discovered by testing against all 8 completed real documents:
    • Map by `field_id` (semantic, e.g. `Volunteer_Pronouns`) not `name` (generic type, e.g. `"Text"`)
    • Inject `email` and `phone` from recipient `assigned_to` metadata — they are not form fields
    • Coalesce upload slots: `*1` = supervisor flow, `*2` = applicant self-submission; fall back to `*1` when `*2` is empty
    • Normalize `Volunteer_Discipline` label → kebab-case key to satisfy the `discipline` catalog FK
    • Make `Volunteer_Phone` optional — the form label exists but no input field is wired in the current PandaDoc template
    • Convert missing-required-fields mapper errors to 400 Bad Request (were incorrectly 500)
  • Names fix: The admin UI was showing raw email addresses instead of applicant names. Root cause: the webhook never created a `User` record, and the frontend resolves display names via the `users` table (`useApplications.ts`). Fix: extract `first_name`/`last_name` from PandaDoc recipient `assigned_to` metadata (same source as email/phone) and create a `User` row inside the same transaction — only if one doesn't already exist for that email.

Test plan

  • Fired a signed webhook locally against a real completed PandaDoc document (juCyjmgFCMLTNrfBbxfate). Confirmed all four records created atomically: Application (appId=46), CandidateInfo, LearnerInfo, and User (firstName=Sam, lastName=Nie)
  • All 6 real completed PandaDoc submissions create applications successfully (tested locally: appIds created for supervisor flow docs and applicant self-submission docs)
  • 2 blank/junk test forms correctly return 400 Bad Request with field names listed
  • HMAC-SHA256 signature verification still works (existing guard tests pass)
  • 22 pandadoc unit tests pass (including 3 new tests for user creation: creates user when name present, skips when no name, skips when user already exists)
  • PANDADOC_API_KEY must be set in ECS environment (confirm with Sam — believed already present)

rayyanmridha and others added 4 commits April 12, 2026 18:01
…sions

Adds a public POST /api/pandadoc-webhook endpoint that receives PandaDoc
webhook payloads when a recipient completes the application form. The
endpoint runs the payload through the existing pandadocMapper, sets
defaults (appStatus=APP_SUBMITTED, derives applicantType from
schoolDepartment), then creates Application, CandidateInfo, and
LearnerInfo records in sequence with logging at each step.

- New PandadocWebhookModule with controller, service, and tests
- Export ApplicationsService and LearnerInfoService from their modules
- Register PandadocWebhookModule and CandidateInfoModule in AppModule
- Optional webhook signature verification via PANDADOC_WEBHOOK_KEY env var
- Reuses existing error email filters for applicant notifications

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ostepan8 ostepan8 marked this pull request as ready for review May 19, 2026 15:24
ostepan8 and others added 8 commits May 19, 2026 13:29
…ilters

The controller previously did the x-pandadoc-signature check inline and
applied ApplicationCreationErrorFilter + ApplicationValidationEmailFilter
intended for the human application-form route. Those filters use
@catch(Error), which swallowed UnauthorizedException and rewrote it as
500, and also emailed the applicant on every failure — wrong actor on a
webhook where PandaDoc, not the applicant, is the caller.

Move the signature check into PandadocSignatureGuard so 401s flow through
Nest's default handler, and remove @UseFilters from the webhook
controller. The filters remain on POST /applications where they belong.
Previously the service called ApplicationsService.create,
CandidateInfoService.create, and LearnerInfoService.create sequentially.
Any failure mid-sequence — including the success-confirmation email
inside ApplicationsService.create — left an Application row without its
CandidateInfo / LearnerInfo siblings.

Inject DataSource and run all three em.save calls inside
dataSource.transaction so a failure rolls everything back. Drop the
inter-service dependency from the module (now only ConfigModule is
needed; entities are resolved via the global DataSource).

Also harden formatDate to convert ISO-8601 strings to YYYY-MM-DD instead
of returning them as-is.
Previously the webhook handler tried to read form field values directly
from the webhook payload, but PandaDoc only sends event metadata (doc ID,
event type) — not field values. This commit wires up the full flow:

- Receive webhook → extract document ID from array payload format
- Call PandaDoc fields API (GET /documents/{id}/fields) with PANDADOC_API_KEY
- Map fields by field_id (not name — name is a generic type like "Text")
- Inject email and phone from assigned_to recipient metadata since they
  are not form fields
- Coalesce upload slots: *1 = supervisor flow, *2 = applicant flow;
  fall back to *1 when *2 is empty so both submission paths work
- Normalize Volunteer_Discipline label to kebab-case key to satisfy
  the discipline catalog FK constraint
- Make Volunteer_Phone optional (form label exists but no input field
  in the current PandaDoc template)
- Convert missing-required-fields mapper errors to 400 Bad Request
  instead of 500

Tested against all 8 completed PandaDoc documents: 6 real submissions
create applications successfully, 2 blank/junk test forms return 400.
@ostepan8 ostepan8 changed the title pandadoc webhook create app Fix PandaDoc webhook: fetch fields via API and create applications Jun 11, 2026
ostepan8 and others added 9 commits June 12, 2026 14:12
Extract first_name/last_name from PandaDoc recipient assigned_to metadata
(same source as email/phone) and upsert a User row inside the transaction.
Without this the frontend fell back to displaying raw email addresses because
useApplications.ts resolves display names via the users table.

Tested end-to-end: fired a signed webhook for a real completed doc, confirmed
Application + CandidateInfo + LearnerInfo + User (Sam Nie) all created atomically.
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.

3 participants