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
4 changes: 2 additions & 2 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TaskModule } from './task/task.module';
import { PandadocModule } from './pandadoc/pandadoc.module';
import AppDataSource from './data-source';

@Module({
imports: [TypeOrmModule.forRoot(AppDataSource.options), TaskModule],
imports: [TypeOrmModule.forRoot(AppDataSource.options), PandadocModule],
controllers: [AppController],
providers: [AppService],
})
Expand Down
95 changes: 95 additions & 0 deletions apps/backend/src/pandadoc/docs.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
### Testing the Integration

### 1. Test API Connection

Test that the connection works:

```bash
curl http://localhost:3333/api/pandadoc/test
```

**Expected Response (Success):**

```json
{
"success": true,
"message": "Successfully connected to PandaDoc API",
"apiVersion": "v1",
"authenticated": true
}
```

**Expected Response (Authentication Failure):**

```json
{
"success": false,
"message": "Authentication failed. Please check your API key.",
"statusCode": 401
}
```

### 2. Fetch Available Templates

Retrieve all templates from your PandaDoc account:

```bash
curl http://localhost:3333/api/pandadoc/templates
```

**Expected Response:**

```json
{
"success": true,
"count": 2,
"data": {
"results": [
{
"id": "abc123def456",
"name": "Sample Template",
"date_created": "2024-01-01T00:00:00.000Z",
"date_modified": "2024-01-01T00:00:00.000Z"
}
]
}
}
```

### 3. Fetch a Specific Template

Get details for a specific template (replace `TEMPLATE_ID` with an actual template ID from step 2):

```bash
curl http://localhost:3333/api/pandadoc/templates/TEMPLATE_ID
```

**Expected Response:**

```json
{
"success": true,
"data": {
"id": "abc123def456",
"name": "Sample Template",
"tokens": [...],
"fields": {...}
}
}
```

| Method | Endpoint | Description |
| ------ | ----------------------------- | -------------------------------------- |
| GET | `/api/pandadoc/test` | Test API connection and authentication |
| GET | `/api/pandadoc/templates` | Fetch all available templates |
| GET | `/api/pandadoc/templates/:id` | Fetch specific template by ID |

The PandaDoc integration consists of three main files:

- `apps/backend/src/pandadoc/pandadoc.service.ts` - Service class containing API logic
- `apps/backend/src/pandadoc/pandadoc.controller.ts` - REST API endpoints
- `apps/backend/src/pandadoc/pandadoc.module.ts` - NestJS module

## Next Steps

Refer to the [PandaDoc API documentation](https://developers.pandadoc.com/reference/about) if needed
83 changes: 83 additions & 0 deletions apps/backend/src/pandadoc/pandadoc.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Controller, Get, Param, Logger } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
import { PandadocService } from './pandadoc.service';

@ApiTags('PandaDoc')
@Controller('pandadoc')
export class PandadocController {
private readonly logger = new Logger(PandadocController.name);

constructor(private readonly pandadocService: PandadocService) {}

@Get('test')
@ApiOperation({ summary: 'Test PandaDoc API connection' })
@ApiResponse({
status: 200,
description: 'Connection test result',
schema: {
example: {
success: true,
message: 'Successfully connected to PandaDoc API',
apiVersion: 'v1',
authenticated: true,
},
},
})
async testConnection() {
this.logger.log('Testing PandaDoc API connection via endpoint');
return this.pandadocService.testConnection();
}

@Get('templates')
@ApiOperation({ summary: 'Fetch all available PandaDoc templates' })
@ApiResponse({
status: 200,
description: 'List of templates',
schema: {
example: {
success: true,
count: 2,
data: {
results: [
{
id: 'template-id-1',
name: 'Template Name',
date_created: '2024-01-01T00:00:00.000Z',
},
],
},
},
},
})
async getTemplates() {
this.logger.log('Fetching templates via endpoint');
return this.pandadocService.getTemplates();
}

@Get('templates/:id')
@ApiOperation({ summary: 'Fetch a specific template by ID' })
@ApiParam({
name: 'id',
description: 'The PandaDoc template ID',
example: 'abc123def456',
})
@ApiResponse({
status: 200,
description: 'Template details',
schema: {
example: {
success: true,
data: {
id: 'abc123def456',
name: 'Template Name',
tokens: [],
fields: [],
},
},
},
})
async getTemplateById(@Param('id') id: string) {
this.logger.log(`Fetching template ${id} via endpoint`);
return this.pandadocService.getTemplateById(id);
}
}
10 changes: 10 additions & 0 deletions apps/backend/src/pandadoc/pandadoc.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { PandadocService } from './pandadoc.service';
import { PandadocController } from './pandadoc.controller';

@Module({
controllers: [PandadocController],
providers: [PandadocService],
exports: [PandadocService], // Export service so other modules can use it
})
export class PandadocModule {}
159 changes: 159 additions & 0 deletions apps/backend/src/pandadoc/pandadoc.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Injectable, Logger } from '@nestjs/common';
import axios, { AxiosInstance } from 'axios';

@Injectable()
export class PandadocService {
private readonly logger = new Logger(PandadocService.name);
private readonly apiClient: AxiosInstance;
private readonly apiKey: string;

constructor() {
this.apiKey = process.env.NX_PANDADOC_API_KEY || '';

if (!this.apiKey) {
this.logger.warn(
'PandaDoc API key not configured. Set NX_PANDADOC_API_KEY in .env file.',
);
}

this.apiClient = axios.create({
baseURL: 'https://api.pandadoc.com/public/v1',
headers: {
Authorization: `API-Key ${this.apiKey}`,
'Content-Type': 'application/json',
},
});
}

/**
* Fetch all available templates from PandaDoc
* @returns Promise with templates data
*/
async getTemplates() {
try {
this.logger.log('Fetching templates from PandaDoc...');

const response = await this.apiClient.get('/templates');

this.logger.log(
`Successfully fetched ${response.data.results?.length || 0} templates`,
);

return {
success: true,
data: response.data,
count: response.data.results?.length || 0,
};
} catch (error) {
this.logger.error('Failed to fetch templates from PandaDoc', error);

if (axios.isAxiosError(error)) {
return {
success: false,
error: error.response?.data || error.message,
statusCode: error.response?.status,
};
}

return {
success: false,
error: 'An unexpected error occurred',
};
}
}

/**
* Test the connection to PandaDoc API
* @returns Promise with connection status
*/
async testConnection() {
try {
this.logger.log('Testing PandaDoc API connection...');

// A simple GET request to templates endpoint to verify authentication
const response = await this.apiClient.get('/templates', {

Check warning on line 74 in apps/backend/src/pandadoc/pandadoc.service.ts

View workflow job for this annotation

GitHub Actions / pre-deploy

'response' is assigned a value but never used
params: { count: 1 }, // Only fetch 1 template to minimize response size
});

this.logger.log('PandaDoc API connection successful');

return {
success: true,
message: 'Successfully connected to PandaDoc API',
apiVersion: 'v1',
authenticated: true,
};
} catch (error) {
this.logger.error('PandaDoc API connection test failed', error);

if (axios.isAxiosError(error)) {
const statusCode = error.response?.status;
let message = 'Connection failed';

if (statusCode === 401) {
message = 'Authentication failed. Please check your API key.';
} else if (statusCode === 403) {
message = 'Access forbidden. Please verify your API key permissions.';
} else if (
error.code === 'ENOTFOUND' ||
error.code === 'ECONNREFUSED'
) {
message =
'Cannot reach PandaDoc API. Please check your internet connection.';
}

return {
success: false,
message,
error: error.response?.data || error.message,
statusCode,
};
}

return {
success: false,
message: 'An unexpected error occurred',
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}

/**
* Get details of a specific template by ID
* @param templateId The ID of the template
* @returns Promise with template details
*/
async getTemplateById(templateId: string) {
try {
this.logger.log(`Fetching template with ID: ${templateId}`);

const response = await this.apiClient.get(
`/templates/${templateId}/details`,
);

this.logger.log(
`Successfully fetched template: ${response.data.name || templateId}`,
);

return {
success: true,
data: response.data,
};
} catch (error) {
this.logger.error(`Failed to fetch template ${templateId}`, error);

if (axios.isAxiosError(error)) {
return {
success: false,
error: error.response?.data || error.message,
statusCode: error.response?.status,
};
}

return {
success: false,
error: 'An unexpected error occurred',
};
}
}
}
3 changes: 2 additions & 1 deletion example.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ NX_DB_HOST=localhost,
NX_DB_USERNAME=postgres,
NX_DB_PASSWORD=,
NX_DB_DATABASE=jumpstart,
NX_DB_PORT=5432,
NX_DB_PORT=5432,
NX_PANDADOC_API_KEY=your_pandadoc_api_key_here
Loading