Severity: HIGH — Invoice PDFs Cannot Be Generated or Stored
Problem
The Invoice module has no service or controller — the Prisma model exists but no business logic is wired. PDF generation via Puppeteer and storage via R2 are completely absent.
Implementation Required
1. Invoice Service (core CRUD)
```typescript
// apps/api/src/modules/invoices/invoices.service.ts
@Injectable()
export class InvoicesService {
async create(merchantId: string, dto: CreateInvoiceDto): Promise
async findAll(merchantId: string, filters: InvoiceFiltersDto): Promise<PaginatedResult>
async findOne(merchantId: string, invoiceId: string): Promise
async update(merchantId: string, invoiceId: string, dto: UpdateInvoiceDto): Promise
async send(merchantId: string, invoiceId: string, dto: SendInvoiceDto): Promise
async generatePdf(invoiceId: string): Promise
async getPdfUrl(merchantId: string, invoiceId: string): Promise
}
```
2. PDF generation with Puppeteer
```typescript
async generatePdf(invoiceId: string): Promise {
const invoice = await this.prisma.invoice.findUnique({
where: { id: invoiceId },
include: { merchant: true },
});
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
const page = await browser.newPage();
// Render invoice HTML template
const html = this.renderInvoiceTemplate(invoice);
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '40px', bottom: '40px', left: '48px', right: '48px' },
});
await browser.close();
return Buffer.from(pdf);
}
```
3. Invoice HTML template
Styled with:
- Merchant logo + branding color
- Invoice number, dates, due date
- Line items table (description, qty, unit price, total)
- Subtotal, tax, discount, total
- Payment instructions / pay link button
- "Powered by Useroutr" footer
4. Wire to R2 storage
After generating PDF:
- Upload to R2:
invoices/{merchantId}/{invoiceId}.pdf
- Save signed URL to
invoice.pdfUrl
- Return URL on
GET /v1/invoices/:id/pdf
Dependencies
Acceptance Criteria
Severity: HIGH — Invoice PDFs Cannot Be Generated or Stored
Problem
The Invoice module has no service or controller — the Prisma model exists but no business logic is wired. PDF generation via Puppeteer and storage via R2 are completely absent.
Implementation Required
1. Invoice Service (core CRUD)
```typescript
// apps/api/src/modules/invoices/invoices.service.ts
@Injectable()
export class InvoicesService {
async create(merchantId: string, dto: CreateInvoiceDto): Promise
async findAll(merchantId: string, filters: InvoiceFiltersDto): Promise<PaginatedResult>
async findOne(merchantId: string, invoiceId: string): Promise
async update(merchantId: string, invoiceId: string, dto: UpdateInvoiceDto): Promise
async send(merchantId: string, invoiceId: string, dto: SendInvoiceDto): Promise
async generatePdf(invoiceId: string): Promise
async getPdfUrl(merchantId: string, invoiceId: string): Promise
}
```
2. PDF generation with Puppeteer
```typescript
async generatePdf(invoiceId: string): Promise {
const invoice = await this.prisma.invoice.findUnique({
where: { id: invoiceId },
include: { merchant: true },
});
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
const page = await browser.newPage();
// Render invoice HTML template
const html = this.renderInvoiceTemplate(invoice);
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '40px', bottom: '40px', left: '48px', right: '48px' },
});
await browser.close();
return Buffer.from(pdf);
}
```
3. Invoice HTML template
Styled with:
4. Wire to R2 storage
After generating PDF:
invoices/{merchantId}/{invoiceId}.pdfinvoice.pdfUrlGET /v1/invoices/:id/pdfDependencies
Acceptance Criteria
GET /v1/invoices/:id/pdfreturns signed download URL