Skip to content
Open
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
90 changes: 90 additions & 0 deletions apps/backend/PANDADOC_WEBHOOK_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# PandaDoc Webhook

This document describes the PandaDoc webhook integration for capturing volunteer application data. If confused talk to Owen!

## Overview

The backend now includes an endpoint that receives PandaDoc webhook events when a volunteer application form is completed. The endpoint automatically parses the form data and creates a new application record in the database.

## Endpoint Details

**URL:** `POST /api/applications/webhook/pandadoc`

**Supported Events:**

- `document_completed` - Triggered when a document is fully completed
- `recipient_completed` - Triggered when a recipient completes their part

**Response:**

```json
{
"success": true,
"application": {
"appId": 1,
"name": "John Doe",
"email": "john.doe@example.com",
...
}
}
```

## Database Schema Changes

The following fields have been added to the `Application` entity:

- **name** (varchar, required) - Applicant's full name
- **email** (varchar, required) - Applicant's email address
- **disciplineId** (integer, optional) - Foreign key reference to Discipline table

I wasn't entirely sure why this wasn't here already but my ticket mentioned these fields so i added them. Check new migration

## Field Mapping

The webhook handler maps PandaDoc form fields to Application fields. It supports **snake_case**, **Title Case**, and **camelCase** field name variations:

| PandaDoc Field Name | Application Field | Type | Notes |
| ------------------------------------------------------ | ----------------- | ------- | -------------------------------------------------------------- |
| `name`, `Name`, `Full Name` | name | string | Falls back to combining first_name + last_name from recipients |
| `email`, `Email` | email | string | Falls back to recipient email |
| `phone`, `Phone` | phone | string | |
| `discipline`, `Discipline` | disciplineId | number | Parsed as integer |
| `school`, `School` | school | enum | Must match School enum values |
| `experience_type`, `Experience Type`, `experienceType` | experienceType | enum | BS, MS, PhD, MD, etc. |
| `interest`, `Interest Area`, `interestArea` | interest | enum | Nursing, HarmReduction, WomensHealth |
| `days_available`, `Days Available`, `daysAvailable` | daysAvailable | string | |
| `weekly_hours`, `Weekly Hours`, `weeklyHours` | weeklyHours | number | |
| `license`, `License` | license | string | |
| `is_international`, `International`, `isInternational` | isInternational | boolean | Accepts: true/false, yes/no, 1/0 |
| `is_learner`, `Learner`, `isLearner` | isLearner | boolean | |
| `referred`, `Referred` | referred | boolean | |
| `referred_email`, `Referred Email`, `referredEmail` | referredEmail | string | |
| `file_uploads`, `File Uploads`, `fileUploads` | fileUploads | array | JSON array or comma-separated |

## Setting Up PandaDoc Webhook

1. Log in to your PandaDoc account
2. Navigate to Settings > Integrations > Webhooks
3. Click "Create Webhook"
4. Configure the webhook:
- **URL:** `https://your-backend-domain.com/api/applications/webhook/pandadoc`
- **Events:** Select `document.completed` and/or `recipient.completed`
- **Active:** Enable the webhook
5. Save the webhook configuration

## Local Testing

1. Ensure the database is running
2. Run migrations: `npm run migration:run`
3. Start the backend server

```bash
cd apps/backend/test-data
./test-webhook.sh
```

## API Documentation

```
http://localhost:3000/api
```
110 changes: 110 additions & 0 deletions apps/backend/e2e-tests/mock-pandadoc-webhook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"event": "document_completed",
"data": {
"id": "abc123-pandadoc-document-id",
"name": "BHCHP Volunteer Application - John Doe",
"status": "document.completed",
"recipients": [
{
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe"
}
],
"fields": [
{
"uuid": "field-1",
"name": "name",
"title": "Full Name",
"value": "John Doe"
},
{
"uuid": "field-2",
"name": "email",
"title": "Email",
"value": "john.doe@example.com"
},
{
"uuid": "field-3",
"name": "phone",
"title": "Phone",
"value": "617-555-1234"
},
{
"uuid": "field-4",
"name": "discipline",
"title": "Discipline",
"value": ""
},
{
"uuid": "field-5",
"name": "school",
"title": "School",
"value": "Harvard Medical School"
},
{
"uuid": "field-6",
"name": "experience_type",
"title": "Experience Type",
"value": "MD"
},
{
"uuid": "field-7",
"name": "interest",
"title": "Interest Area",
"value": "Nursing"
},
{
"uuid": "field-8",
"name": "days_available",
"title": "Days Available",
"value": "Monday, Wednesday, Friday"
},
{
"uuid": "field-9",
"name": "weekly_hours",
"title": "Weekly Hours",
"value": "10"
},
{
"uuid": "field-10",
"name": "license",
"title": "License",
"value": "MD-12345"
},
{
"uuid": "field-11",
"name": "is_international",
"title": "International",
"value": "false"
},
{
"uuid": "field-12",
"name": "is_learner",
"title": "Learner",
"value": "true"
},
{
"uuid": "field-13",
"name": "referred",
"title": "Referred",
"value": "true"
},
{
"uuid": "field-14",
"name": "referred_email",
"title": "Referred Email",
"value": "referrer@example.com"
},
{
"uuid": "field-15",
"name": "file_uploads",
"title": "File Uploads",
"value": "[\"https://example.com/resume.pdf\", \"https://example.com/cv.pdf\"]"
}
],
"metadata": {
"source": "volunteer_application_form"
}
}
}
18 changes: 18 additions & 0 deletions apps/backend/e2e-tests/test-webhook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash

# Test script for PandaDoc webhook endpoint
# Usage: ./test-webhook.sh [port]
# Default port is 3000

PORT=${1:-3000}
BASE_URL="http://localhost:${PORT}/api/applications/webhook/pandadoc"

echo "Testing PandaDoc webhook endpoint at: ${BASE_URL}"

# Send the mock webhook payload
curl -X POST "${BASE_URL}" \
-H "Content-Type: application/json" \
-d @mock-pandadoc-webhook.json \
-v

echo "Test completed!"
13 changes: 9 additions & 4 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import { TypeOrmModule } from '@nestjs/typeorm';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TaskModule } from './task/task.module';
import AppDataSource from './data-source';
import { ApplicationsController } from './applications/applications.controller';
import { ApplicationsService } from './applications/applications.service';
import { Application } from './applications/application.entity';

@Module({
imports: [TypeOrmModule.forRoot(AppDataSource.options), TaskModule],
controllers: [AppController],
providers: [AppService],
imports: [
TypeOrmModule.forRoot(AppDataSource.options),
TypeOrmModule.forFeature([Application]),
],
controllers: [AppController, ApplicationsController],
providers: [AppService, ApplicationsService],
})
export class AppModule {}
68 changes: 68 additions & 0 deletions apps/backend/src/applications/application.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';

import { AppStatus, ExperienceType, InterestArea, School } from './types';
import { Discipline } from '../disciplines/disciplines.entity';

@Entity('application')
export class Application {
@PrimaryGeneratedColumn()
appId!: number;

@Column({ type: 'varchar' })
name!: string;

@Column({ type: 'varchar' })
email!: string;

@Column({ type: 'int', nullable: true })
disciplineId?: number;

@ManyToOne(() => Discipline, { nullable: true })
@JoinColumn({ name: 'disciplineId' })
discipline?: Discipline;

@Column({ type: 'enum', enum: AppStatus, default: AppStatus.APP_SUBMITTED })
appStatus!: AppStatus;

@Column({ type: 'varchar' })
daysAvailable!: string;

@Column({ type: 'enum', enum: ExperienceType })
experienceType!: ExperienceType;

@Column('text', { array: true, default: [] })
fileUploads!: string[];

@Column({ type: 'enum', enum: InterestArea })
interest!: InterestArea;

@Column({ type: 'varchar' })
license!: string;

@Column({ type: 'boolean', default: false })
isInternational!: boolean;

@Column({ type: 'boolean', default: false })
isLearner!: boolean;

@Column({ type: 'varchar' })
phone!: string;

@Column({ type: 'enum', enum: School })
school!: School;

@Column({ type: 'boolean', default: false, nullable: true })
referred?: boolean;

@Column({ type: 'varchar', nullable: true })
referredEmail?: string;

@Column({ type: 'int' })
weeklyHours!: number;
}
Loading
Loading