11import type { InvoiceWithClient , Settings } from '@kivo/shared' ;
22import { formatCurrency , formatDateForTimezone } from '@kivo/shared' ;
33
4- // Simple PDF generation for Workers environment
5- // Using a basic approach that works without native dependencies
4+ // PDF generation using Cloudflare Browser Rendering API
5+ // Falls back to HTML if the API is unavailable
6+
7+ export interface PDFResult {
8+ body : ReadableStream | ArrayBuffer ;
9+ contentType : string ;
10+ filename : string ;
11+ }
612
713export class PDFService {
814 private storage : R2Bucket ;
15+ private accountId : string ;
16+ private apiToken : string ;
917
10- constructor ( storage : R2Bucket ) {
18+ constructor ( storage : R2Bucket , accountId : string , apiToken : string ) {
1119 this . storage = storage ;
20+ this . accountId = accountId ;
21+ this . apiToken = apiToken ;
1222 }
1323
1424 /**
15- * Generate a PDF for an invoice
16- * This creates a simple HTML-based PDF that can be rendered by browsers
25+ * Generate a PDF for an invoice using Cloudflare Browser Rendering API
26+ * Falls back to HTML if the API call fails
1727 */
1828 async generateInvoicePDF (
1929 invoice : InvoiceWithClient ,
2030 settings : Settings
21- ) : Promise < ArrayBuffer > {
31+ ) : Promise < { data : ArrayBuffer ; isPdf : boolean } > {
2232 const html = this . generateInvoiceHTML ( invoice , settings ) ;
23-
24- // Convert HTML to a simple PDF-like format
25- // In production, you might want to use a headless browser API or a PDF service
26- // For now, we'll store HTML that can be converted/printed to PDF
33+
34+ // Try Browser Rendering API first
35+ if ( this . accountId && this . apiToken ) {
36+ try {
37+ const response = await fetch (
38+ `https://api.cloudflare.com/client/v4/accounts/${ this . accountId } /browser-rendering/pdf` ,
39+ {
40+ method : 'POST' ,
41+ headers : {
42+ 'Authorization' : `Bearer ${ this . apiToken } ` ,
43+ 'Content-Type' : 'application/json' ,
44+ } ,
45+ body : JSON . stringify ( {
46+ html,
47+ pdfOptions : {
48+ format : 'a4' ,
49+ printBackground : true ,
50+ margin : {
51+ top : '20px' ,
52+ bottom : '20px' ,
53+ left : '20px' ,
54+ right : '20px' ,
55+ } ,
56+ } ,
57+ } ) ,
58+ }
59+ ) ;
60+
61+ if ( response . ok ) {
62+ const pdfBuffer = await response . arrayBuffer ( ) ;
63+ return { data : pdfBuffer , isPdf : true } ;
64+ }
65+
66+ // Log error but continue to fallback
67+ console . error ( 'Browser Rendering API error:' , response . status , await response . text ( ) ) ;
68+ } catch ( error ) {
69+ console . error ( 'Browser Rendering API failed:' , error ) ;
70+ }
71+ }
72+
73+ // Fallback to HTML
74+ console . warn ( 'Falling back to HTML generation' ) ;
2775 const encoder = new TextEncoder ( ) ;
28- return encoder . encode ( html ) . buffer as ArrayBuffer ;
76+ return { data : encoder . encode ( html ) . buffer as ArrayBuffer , isPdf : false } ;
2977 }
3078
3179 /**
@@ -90,7 +138,7 @@ export class PDFService {
90138 .invoice-number {
91139 font-size: 28px;
92140 font-weight: 700;
93- color: #6366F1 ;
141+ color: #10B981 ;
94142 margin-bottom: 8px;
95143 }
96144
@@ -202,9 +250,9 @@ export class PDFService {
202250
203251 .status-draft { background: #f3f4f6; color: #6b7280; }
204252 .status-sent { background: #dbeafe; color: #2563eb; }
205- .status-viewed { background: #f3e8ff ; color: #9333ea ; }
253+ .status-viewed { background: #ede9fe ; color: #8B5CF6 ; }
206254 .status-paid { background: #dcfce7; color: #16a34a; }
207- .status-overdue { background: #fee2e2 ; color: #dc2626 ; }
255+ .status-overdue { background: #fff7ed ; color: #F97316 ; }
208256 .status-void { background: #f1f5f9; color: #64748b; }
209257
210258 .notes {
@@ -362,14 +410,17 @@ export class PDFService {
362410 userId : string ,
363411 invoiceId : string ,
364412 invoiceNumber : string ,
365- pdfData : ArrayBuffer
413+ pdfData : ArrayBuffer ,
414+ isPdf : boolean
366415 ) : Promise < string > {
367- const key = `${ userId } /invoices/${ invoiceId } /${ invoiceNumber } .html` ;
416+ const extension = isPdf ? 'pdf' : 'html' ;
417+ const contentType = isPdf ? 'application/pdf' : 'text/html' ;
418+ const key = `${ userId } /invoices/${ invoiceId } /${ invoiceNumber } .${ extension } ` ;
368419
369420 await this . storage . put ( key , pdfData , {
370421 httpMetadata : {
371- contentType : 'text/html' ,
372- contentDisposition : `inline; filename="${ invoiceNumber } .html "` ,
422+ contentType,
423+ contentDisposition : `inline; filename="${ invoiceNumber } .${ extension } "` ,
373424 } ,
374425 } ) ;
375426
@@ -378,16 +429,47 @@ export class PDFService {
378429
379430 /**
380431 * Get PDF from R2
432+ * Tries PDF first, then falls back to HTML for legacy files
381433 */
382- async getPDF ( key : string ) : Promise < R2ObjectBody | null > {
383- return this . storage . get ( key ) ;
434+ async getPDF ( userId : string , invoiceId : string , invoiceNumber : string ) : Promise < PDFResult | null > {
435+ // Try PDF first
436+ const pdfKey = `${ userId } /invoices/${ invoiceId } /${ invoiceNumber } .pdf` ;
437+ let file = await this . storage . get ( pdfKey ) ;
438+
439+ if ( file ) {
440+ return {
441+ body : file . body ,
442+ contentType : 'application/pdf' ,
443+ filename : `${ invoiceNumber } .pdf` ,
444+ } ;
445+ }
446+
447+ // Fall back to HTML for legacy files
448+ const htmlKey = `${ userId } /invoices/${ invoiceId } /${ invoiceNumber } .html` ;
449+ file = await this . storage . get ( htmlKey ) ;
450+
451+ if ( file ) {
452+ return {
453+ body : file . body ,
454+ contentType : 'text/html' ,
455+ filename : `${ invoiceNumber } .html` ,
456+ } ;
457+ }
458+
459+ return null ;
384460 }
385461
386462 /**
387- * Delete PDF from R2
463+ * Delete PDF from R2 (both PDF and HTML versions)
388464 */
389- async deletePDF ( key : string ) : Promise < void > {
390- await this . storage . delete ( key ) ;
465+ async deletePDF ( userId : string , invoiceId : string , invoiceNumber : string ) : Promise < void > {
466+ const pdfKey = `${ userId } /invoices/${ invoiceId } /${ invoiceNumber } .pdf` ;
467+ const htmlKey = `${ userId } /invoices/${ invoiceId } /${ invoiceNumber } .html` ;
468+
469+ await Promise . all ( [
470+ this . storage . delete ( pdfKey ) ,
471+ this . storage . delete ( htmlKey ) ,
472+ ] ) ;
391473 }
392474
393475 /**
0 commit comments