diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 54fb044e..1c3df33e 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -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], }) diff --git a/apps/backend/src/pandadoc/docs.MD b/apps/backend/src/pandadoc/docs.MD new file mode 100644 index 00000000..80fd4103 --- /dev/null +++ b/apps/backend/src/pandadoc/docs.MD @@ -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 diff --git a/apps/backend/src/pandadoc/pandadoc.controller.ts b/apps/backend/src/pandadoc/pandadoc.controller.ts new file mode 100644 index 00000000..50c56d01 --- /dev/null +++ b/apps/backend/src/pandadoc/pandadoc.controller.ts @@ -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); + } +} diff --git a/apps/backend/src/pandadoc/pandadoc.module.ts b/apps/backend/src/pandadoc/pandadoc.module.ts new file mode 100644 index 00000000..3428fb20 --- /dev/null +++ b/apps/backend/src/pandadoc/pandadoc.module.ts @@ -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 {} diff --git a/apps/backend/src/pandadoc/pandadoc.service.ts b/apps/backend/src/pandadoc/pandadoc.service.ts new file mode 100644 index 00000000..966ff6e4 --- /dev/null +++ b/apps/backend/src/pandadoc/pandadoc.service.ts @@ -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', { + 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', + }; + } + } +} diff --git a/example.env b/example.env index 211b1472..fd8d1aa6 100644 --- a/example.env +++ b/example.env @@ -2,4 +2,5 @@ NX_DB_HOST=localhost, NX_DB_USERNAME=postgres, NX_DB_PASSWORD=, NX_DB_DATABASE=jumpstart, -NX_DB_PORT=5432, \ No newline at end of file +NX_DB_PORT=5432, +NX_PANDADOC_API_KEY=your_pandadoc_api_key_here \ No newline at end of file