@girardmedia/bootspring 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: payment-integration
|
|
3
|
+
description: Payment integration patterns with Stripe, webhooks, idempotency, subscription billing, and PCI compliance.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Payment Integration
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when your application needs to accept payments -- one-time purchases, subscriptions, usage-based billing, or marketplace payouts. Stripe is the default choice for most SaaS and e-commerce applications. These patterns apply broadly to any payment provider. Never store card numbers or build custom card forms unless PCI DSS compliance is already in place.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Stripe Checkout -- One-Time Payments
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import Stripe from "stripe";
|
|
17
|
+
|
|
18
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
19
|
+
|
|
20
|
+
app.post("/api/checkout", async (req, res) => {
|
|
21
|
+
const session = await stripe.checkout.sessions.create({
|
|
22
|
+
mode: "payment",
|
|
23
|
+
customer_email: req.user.email,
|
|
24
|
+
line_items: [{
|
|
25
|
+
price_data: {
|
|
26
|
+
currency: "usd",
|
|
27
|
+
product_data: { name: req.body.productName },
|
|
28
|
+
unit_amount: req.body.priceInCents, // always cents, never dollars
|
|
29
|
+
},
|
|
30
|
+
quantity: req.body.quantity,
|
|
31
|
+
}],
|
|
32
|
+
metadata: { userId: req.user.id, orderId: req.body.orderId },
|
|
33
|
+
success_url: `${process.env.APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
34
|
+
cancel_url: `${process.env.APP_URL}/checkout/cancel`,
|
|
35
|
+
expires_at: Math.floor(Date.now() / 1000) + 1800,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
res.json({ url: session.url });
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Subscriptions with Stripe Billing
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
app.post("/api/subscribe", async (req, res) => {
|
|
46
|
+
let customerId = req.user.stripeCustomerId;
|
|
47
|
+
if (!customerId) {
|
|
48
|
+
const customer = await stripe.customers.create({
|
|
49
|
+
email: req.user.email,
|
|
50
|
+
metadata: { userId: req.user.id },
|
|
51
|
+
});
|
|
52
|
+
customerId = customer.id;
|
|
53
|
+
await db.users.update(req.user.id, { stripeCustomerId: customerId });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const session = await stripe.checkout.sessions.create({
|
|
57
|
+
mode: "subscription",
|
|
58
|
+
customer: customerId,
|
|
59
|
+
line_items: [{ price: req.body.stripePriceId, quantity: 1 }],
|
|
60
|
+
subscription_data: {
|
|
61
|
+
trial_period_days: 14,
|
|
62
|
+
metadata: { userId: req.user.id, plan: req.body.plan },
|
|
63
|
+
},
|
|
64
|
+
success_url: `${process.env.APP_URL}/dashboard?upgraded=true`,
|
|
65
|
+
cancel_url: `${process.env.APP_URL}/pricing`,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
res.json({ url: session.url });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Customer portal for self-service billing management
|
|
72
|
+
app.post("/api/billing/portal", async (req, res) => {
|
|
73
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
74
|
+
customer: req.user.stripeCustomerId,
|
|
75
|
+
return_url: `${process.env.APP_URL}/settings/billing`,
|
|
76
|
+
});
|
|
77
|
+
res.json({ url: session.url });
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Webhook Handling -- Source of Truth
|
|
82
|
+
|
|
83
|
+
Webhooks are the source of truth for payment state. The success_url page is just UI:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
app.post(
|
|
87
|
+
"/api/webhooks/stripe",
|
|
88
|
+
express.raw({ type: "application/json" }),
|
|
89
|
+
async (req, res) => {
|
|
90
|
+
const sig = req.headers["stripe-signature"] as string;
|
|
91
|
+
let event: Stripe.Event;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
event = stripe.webhooks.constructEvent(
|
|
95
|
+
req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!
|
|
96
|
+
);
|
|
97
|
+
} catch {
|
|
98
|
+
return res.status(400).send("Invalid signature");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
switch (event.type) {
|
|
102
|
+
case "checkout.session.completed": {
|
|
103
|
+
const session = event.data.object as Stripe.Checkout.Session;
|
|
104
|
+
await fulfillOrder(session);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "invoice.paid": {
|
|
108
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
109
|
+
await extendSubscription(invoice);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "invoice.payment_failed": {
|
|
113
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
114
|
+
await handlePaymentFailure(invoice);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "customer.subscription.deleted": {
|
|
118
|
+
const sub = event.data.object as Stripe.Subscription;
|
|
119
|
+
await revokeAccess(sub);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
res.json({ received: true });
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Idempotency
|
|
129
|
+
|
|
130
|
+
Payment operations must be safe to retry:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// Stripe idempotency key tied to business entity
|
|
134
|
+
const charge = await stripe.charges.create(
|
|
135
|
+
{ amount: 2000, currency: "usd", customer: customerId, metadata: { orderId } },
|
|
136
|
+
{ idempotencyKey: `charge-${orderId}` }
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Webhook deduplication
|
|
140
|
+
async function processWebhook(event: Stripe.Event): Promise<void> {
|
|
141
|
+
const acquired = await redis.set(`stripe:event:${event.id}`, "1", "NX", "EX", 86400);
|
|
142
|
+
if (!acquired) return; // duplicate
|
|
143
|
+
// Process event...
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Database-level idempotency for fulfillment
|
|
147
|
+
async function fulfillOrder(session: Stripe.Checkout.Session): Promise<void> {
|
|
148
|
+
const orderId = session.metadata!.orderId;
|
|
149
|
+
const result = await db.query(
|
|
150
|
+
`UPDATE orders SET status = 'fulfilled', stripe_session_id = $1
|
|
151
|
+
WHERE id = $2 AND status = 'pending'`,
|
|
152
|
+
[session.id, orderId]
|
|
153
|
+
);
|
|
154
|
+
if (result.rowCount === 0) return; // already fulfilled
|
|
155
|
+
await provisionAccess(orderId);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Refunds
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
app.post("/api/orders/:id/refund", async (req, res) => {
|
|
163
|
+
const order = await db.orders.findById(req.params.id);
|
|
164
|
+
if (!order) return res.status(404).json({ error: "Order not found" });
|
|
165
|
+
if (order.userId !== req.user.id) return res.status(403).json({ error: "Forbidden" });
|
|
166
|
+
|
|
167
|
+
const daysSincePurchase = (Date.now() - order.createdAt.getTime()) / 86400000;
|
|
168
|
+
if (daysSincePurchase > 30) {
|
|
169
|
+
return res.status(400).json({ error: "Refund window closed (30 days)" });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const refund = await stripe.refunds.create(
|
|
173
|
+
{
|
|
174
|
+
payment_intent: order.stripePaymentIntentId,
|
|
175
|
+
amount: req.body.partial ? req.body.amountInCents : undefined,
|
|
176
|
+
reason: "requested_by_customer",
|
|
177
|
+
},
|
|
178
|
+
{ idempotencyKey: `refund-${order.id}` }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await db.orders.update(order.id, {
|
|
182
|
+
status: refund.amount === order.totalCents ? "refunded" : "partially_refunded",
|
|
183
|
+
});
|
|
184
|
+
res.json({ refundId: refund.id, amount: refund.amount });
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### PCI Compliance Essentials
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
NEVER DO:
|
|
192
|
+
- Store card numbers, CVV, or full magnetic stripe data
|
|
193
|
+
- Build custom card input forms (use Stripe Elements or Checkout)
|
|
194
|
+
- Log raw payment API responses containing card data
|
|
195
|
+
- Send card data to your server (use client-side tokenization)
|
|
196
|
+
|
|
197
|
+
ALWAYS DO:
|
|
198
|
+
- Use Stripe Checkout or Elements (SAQ A compliance)
|
|
199
|
+
- Verify webhook signatures on every request
|
|
200
|
+
- Use HTTPS for all payment-related endpoints
|
|
201
|
+
- Store only Stripe customer/subscription/payment IDs
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Examples
|
|
205
|
+
|
|
206
|
+
| Scenario | Stripe Feature | Key Consideration |
|
|
207
|
+
|----------|---------------|-------------------|
|
|
208
|
+
| SaaS monthly plan | Checkout + Subscriptions | Dunning emails for failed payments |
|
|
209
|
+
| One-time purchase | Checkout Session | Fulfill via webhook only |
|
|
210
|
+
| Marketplace payouts | Stripe Connect | Destination charges for platform fee |
|
|
211
|
+
| Usage-based billing | Metered subscriptions | Report usage via Usage Records API |
|
|
212
|
+
| B2B invoicing | Invoices API | Net-30 terms, PDF generation |
|
|
213
|
+
|
|
214
|
+
## Checklist
|
|
215
|
+
- [ ] Payments use Stripe Checkout or Elements for PCI compliance
|
|
216
|
+
- [ ] Webhook signature verified on every request
|
|
217
|
+
- [ ] Webhook route uses `express.raw()`, not `express.json()`
|
|
218
|
+
- [ ] All mutating Stripe calls include idempotency keys
|
|
219
|
+
- [ ] Fulfillment happens in webhook handlers, not success page
|
|
220
|
+
- [ ] Duplicate webhook events detected and skipped
|
|
221
|
+
- [ ] Amounts always in cents (integer), never dollars (float)
|
|
222
|
+
- [ ] Failed subscription payments trigger dunning sequence
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pdf-generation
|
|
3
|
+
description: PDF generation patterns with Puppeteer, jsPDF, server-side rendering from HTML templates, streaming, and batch processing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PDF Generation Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Generate PDFs for invoices, reports, certificates, contracts, and any document that needs consistent formatting across devices. Use Puppeteer for pixel-perfect HTML-to-PDF conversion with full CSS support. Use jsPDF for lightweight client-side generation. Choose server-side rendering when you need consistent output regardless of client, and streaming for large documents that should not be held in memory.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Puppeteer HTML-to-PDF
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// src/pdf/puppeteer-generator.ts
|
|
17
|
+
import puppeteer, { type Browser } from 'puppeteer';
|
|
18
|
+
|
|
19
|
+
let browser: Browser | null = null;
|
|
20
|
+
|
|
21
|
+
async function getBrowser(): Promise<Browser> {
|
|
22
|
+
if (!browser) {
|
|
23
|
+
browser = await puppeteer.launch({
|
|
24
|
+
headless: true,
|
|
25
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return browser;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function generatePdfFromHtml(html: string, options?: {
|
|
32
|
+
format?: 'A4' | 'Letter';
|
|
33
|
+
landscape?: boolean;
|
|
34
|
+
margin?: { top: string; right: string; bottom: string; left: string };
|
|
35
|
+
headerTemplate?: string;
|
|
36
|
+
footerTemplate?: string;
|
|
37
|
+
}): Promise<Buffer> {
|
|
38
|
+
const browser = await getBrowser();
|
|
39
|
+
const page = await browser.newPage();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await page.setContent(html, { waitUntil: 'networkidle0' });
|
|
43
|
+
|
|
44
|
+
const pdf = await page.pdf({
|
|
45
|
+
format: options?.format ?? 'A4',
|
|
46
|
+
landscape: options?.landscape ?? false,
|
|
47
|
+
printBackground: true,
|
|
48
|
+
margin: options?.margin ?? { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
|
|
49
|
+
displayHeaderFooter: !!(options?.headerTemplate || options?.footerTemplate),
|
|
50
|
+
headerTemplate: options?.headerTemplate ?? '',
|
|
51
|
+
footerTemplate: options?.footerTemplate ?? `
|
|
52
|
+
<div style="font-size:9px; width:100%; text-align:center; color:#666;">
|
|
53
|
+
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
|
|
54
|
+
</div>
|
|
55
|
+
`,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return Buffer.from(pdf);
|
|
59
|
+
} finally {
|
|
60
|
+
await page.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Cleanup on shutdown
|
|
65
|
+
process.on('SIGTERM', async () => {
|
|
66
|
+
if (browser) await browser.close();
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Invoice Template
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// src/pdf/templates/invoice.ts
|
|
74
|
+
interface InvoiceData {
|
|
75
|
+
invoiceNumber: string;
|
|
76
|
+
date: string;
|
|
77
|
+
dueDate: string;
|
|
78
|
+
company: { name: string; address: string; email: string };
|
|
79
|
+
customer: { name: string; address: string; email: string };
|
|
80
|
+
items: Array<{ description: string; quantity: number; unitPrice: number }>;
|
|
81
|
+
taxRate: number;
|
|
82
|
+
notes?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function renderInvoiceHtml(data: InvoiceData): string {
|
|
86
|
+
const subtotal = data.items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
|
|
87
|
+
const tax = subtotal * data.taxRate;
|
|
88
|
+
const total = subtotal + tax;
|
|
89
|
+
|
|
90
|
+
return `<!DOCTYPE html>
|
|
91
|
+
<html>
|
|
92
|
+
<head>
|
|
93
|
+
<style>
|
|
94
|
+
body { font-family: 'Helvetica Neue', Arial, sans-serif; color: #333; margin: 0; padding: 40px; }
|
|
95
|
+
.header { display: flex; justify-content: space-between; margin-bottom: 40px; }
|
|
96
|
+
.company-name { font-size: 24px; font-weight: 700; color: #1a1a1a; }
|
|
97
|
+
.invoice-title { font-size: 32px; font-weight: 300; color: #666; text-align: right; }
|
|
98
|
+
.meta { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 40px; }
|
|
99
|
+
.meta-label { font-size: 12px; text-transform: uppercase; color: #999; margin-bottom: 4px; }
|
|
100
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
|
|
101
|
+
th { text-align: left; padding: 12px 8px; border-bottom: 2px solid #333; font-size: 12px; text-transform: uppercase; }
|
|
102
|
+
td { padding: 12px 8px; border-bottom: 1px solid #eee; }
|
|
103
|
+
.amount { text-align: right; }
|
|
104
|
+
.totals { width: 300px; margin-left: auto; }
|
|
105
|
+
.totals td { border: none; padding: 6px 8px; }
|
|
106
|
+
.totals .total { font-size: 18px; font-weight: 700; border-top: 2px solid #333; }
|
|
107
|
+
.notes { margin-top: 40px; padding: 20px; background: #f8f8f8; border-radius: 4px; font-size: 14px; }
|
|
108
|
+
</style>
|
|
109
|
+
</head>
|
|
110
|
+
<body>
|
|
111
|
+
<div class="header">
|
|
112
|
+
<div>
|
|
113
|
+
<div class="company-name">${data.company.name}</div>
|
|
114
|
+
<div>${data.company.address}</div>
|
|
115
|
+
<div>${data.company.email}</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="invoice-title">INVOICE</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="meta">
|
|
121
|
+
<div>
|
|
122
|
+
<div class="meta-label">Bill To</div>
|
|
123
|
+
<div><strong>${data.customer.name}</strong></div>
|
|
124
|
+
<div>${data.customer.address}</div>
|
|
125
|
+
<div>${data.customer.email}</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div style="text-align: right;">
|
|
128
|
+
<div class="meta-label">Invoice #</div>
|
|
129
|
+
<div>${data.invoiceNumber}</div>
|
|
130
|
+
<div class="meta-label" style="margin-top: 8px;">Date</div>
|
|
131
|
+
<div>${data.date}</div>
|
|
132
|
+
<div class="meta-label" style="margin-top: 8px;">Due Date</div>
|
|
133
|
+
<div>${data.dueDate}</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<table>
|
|
138
|
+
<thead>
|
|
139
|
+
<tr><th>Description</th><th class="amount">Qty</th><th class="amount">Unit Price</th><th class="amount">Amount</th></tr>
|
|
140
|
+
</thead>
|
|
141
|
+
<tbody>
|
|
142
|
+
${data.items.map((item) => `
|
|
143
|
+
<tr>
|
|
144
|
+
<td>${item.description}</td>
|
|
145
|
+
<td class="amount">${item.quantity}</td>
|
|
146
|
+
<td class="amount">$${item.unitPrice.toFixed(2)}</td>
|
|
147
|
+
<td class="amount">$${(item.quantity * item.unitPrice).toFixed(2)}</td>
|
|
148
|
+
</tr>
|
|
149
|
+
`).join('')}
|
|
150
|
+
</tbody>
|
|
151
|
+
</table>
|
|
152
|
+
|
|
153
|
+
<table class="totals">
|
|
154
|
+
<tr><td>Subtotal</td><td class="amount">$${subtotal.toFixed(2)}</td></tr>
|
|
155
|
+
<tr><td>Tax (${(data.taxRate * 100).toFixed(0)}%)</td><td class="amount">$${tax.toFixed(2)}</td></tr>
|
|
156
|
+
<tr class="total"><td>Total</td><td class="amount">$${total.toFixed(2)}</td></tr>
|
|
157
|
+
</table>
|
|
158
|
+
|
|
159
|
+
${data.notes ? `<div class="notes"><strong>Notes:</strong> ${data.notes}</div>` : ''}
|
|
160
|
+
</body>
|
|
161
|
+
</html>`;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### jsPDF Client-Side Generation
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// src/pdf/client-generator.ts
|
|
169
|
+
import jsPDF from 'jspdf';
|
|
170
|
+
import autoTable from 'jspdf-autotable';
|
|
171
|
+
|
|
172
|
+
export function generateReport(data: ReportData): jsPDF {
|
|
173
|
+
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
|
174
|
+
|
|
175
|
+
// Header
|
|
176
|
+
doc.setFontSize(24);
|
|
177
|
+
doc.text('Monthly Report', 20, 30);
|
|
178
|
+
|
|
179
|
+
doc.setFontSize(12);
|
|
180
|
+
doc.setTextColor(100);
|
|
181
|
+
doc.text(`Generated: ${new Date().toLocaleDateString()}`, 20, 40);
|
|
182
|
+
|
|
183
|
+
// Summary section
|
|
184
|
+
doc.setFontSize(16);
|
|
185
|
+
doc.setTextColor(0);
|
|
186
|
+
doc.text('Summary', 20, 60);
|
|
187
|
+
|
|
188
|
+
doc.setFontSize(11);
|
|
189
|
+
doc.text(`Total Revenue: $${data.revenue.toLocaleString()}`, 20, 70);
|
|
190
|
+
doc.text(`Active Users: ${data.activeUsers.toLocaleString()}`, 20, 78);
|
|
191
|
+
doc.text(`Conversion Rate: ${(data.conversionRate * 100).toFixed(1)}%`, 20, 86);
|
|
192
|
+
|
|
193
|
+
// Data table
|
|
194
|
+
autoTable(doc, {
|
|
195
|
+
startY: 100,
|
|
196
|
+
head: [['Month', 'Revenue', 'Users', 'Conversion']],
|
|
197
|
+
body: data.monthly.map((m) => [
|
|
198
|
+
m.month,
|
|
199
|
+
`$${m.revenue.toLocaleString()}`,
|
|
200
|
+
m.users.toLocaleString(),
|
|
201
|
+
`${(m.conversion * 100).toFixed(1)}%`,
|
|
202
|
+
]),
|
|
203
|
+
styles: { fontSize: 10, cellPadding: 4 },
|
|
204
|
+
headStyles: { fillColor: [37, 99, 235], textColor: 255 },
|
|
205
|
+
alternateRowStyles: { fillColor: [245, 247, 250] },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return doc;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Download in browser
|
|
212
|
+
function downloadPdf(data: ReportData) {
|
|
213
|
+
const doc = generateReport(data);
|
|
214
|
+
doc.save('monthly-report.pdf');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Get as blob for upload
|
|
218
|
+
async function uploadPdf(data: ReportData): Promise<void> {
|
|
219
|
+
const doc = generateReport(data);
|
|
220
|
+
const blob = doc.output('blob');
|
|
221
|
+
const formData = new FormData();
|
|
222
|
+
formData.append('file', blob, 'report.pdf');
|
|
223
|
+
await fetch('/api/upload', { method: 'POST', body: formData });
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Express PDF Endpoint with Streaming
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// src/routes/pdf.ts
|
|
231
|
+
import { Router } from 'express';
|
|
232
|
+
import { generatePdfFromHtml } from '../pdf/puppeteer-generator';
|
|
233
|
+
import { renderInvoiceHtml } from '../pdf/templates/invoice';
|
|
234
|
+
|
|
235
|
+
const router = Router();
|
|
236
|
+
|
|
237
|
+
router.get('/api/invoices/:id/pdf', async (req, res) => {
|
|
238
|
+
try {
|
|
239
|
+
const invoice = await getInvoice(req.params.id);
|
|
240
|
+
if (!invoice) return res.status(404).json({ error: 'Invoice not found' });
|
|
241
|
+
|
|
242
|
+
const html = renderInvoiceHtml(invoice);
|
|
243
|
+
const pdf = await generatePdfFromHtml(html, {
|
|
244
|
+
footerTemplate: `<div style="font-size:8px;text-align:center;width:100%;color:#999;">
|
|
245
|
+
Invoice ${invoice.invoiceNumber} | Page <span class="pageNumber"></span>
|
|
246
|
+
</div>`,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
res.set({
|
|
250
|
+
'Content-Type': 'application/pdf',
|
|
251
|
+
'Content-Disposition': `inline; filename="invoice-${invoice.invoiceNumber}.pdf"`,
|
|
252
|
+
'Content-Length': String(pdf.length),
|
|
253
|
+
'Cache-Control': 'private, max-age=3600',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
res.send(pdf);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error('PDF generation failed:', err);
|
|
259
|
+
res.status(500).json({ error: 'PDF generation failed' });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Batch PDF generation
|
|
264
|
+
router.post('/api/invoices/batch-pdf', async (req, res) => {
|
|
265
|
+
const { invoiceIds } = req.body;
|
|
266
|
+
const archiver = require('archiver');
|
|
267
|
+
const archive = archiver('zip');
|
|
268
|
+
|
|
269
|
+
res.set({
|
|
270
|
+
'Content-Type': 'application/zip',
|
|
271
|
+
'Content-Disposition': 'attachment; filename="invoices.zip"',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
archive.pipe(res);
|
|
275
|
+
|
|
276
|
+
for (const id of invoiceIds) {
|
|
277
|
+
const invoice = await getInvoice(id);
|
|
278
|
+
if (!invoice) continue;
|
|
279
|
+
const html = renderInvoiceHtml(invoice);
|
|
280
|
+
const pdf = await generatePdfFromHtml(html);
|
|
281
|
+
archive.append(pdf, { name: `invoice-${invoice.invoiceNumber}.pdf` });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await archive.finalize();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
export default router;
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Examples
|
|
291
|
+
|
|
292
|
+
| Method | Environment | CSS Support | Best For |
|
|
293
|
+
|--------|-------------|-------------|----------|
|
|
294
|
+
| Puppeteer | Server (Node.js) | Full (Chrome rendering) | Complex layouts, pixel-perfect |
|
|
295
|
+
| jsPDF | Client or server | None (manual layout) | Simple reports, tables |
|
|
296
|
+
| @react-pdf/renderer | Server or client | Subset (flexbox) | React-based templates |
|
|
297
|
+
| wkhtmltopdf | Server (binary) | Good (WebKit) | Legacy systems |
|
|
298
|
+
|
|
299
|
+
## Checklist
|
|
300
|
+
- [ ] Puppeteer browser instance reused across requests (not launched per PDF)
|
|
301
|
+
- [ ] Templates use print-friendly CSS (no background colors unless `printBackground: true`)
|
|
302
|
+
- [ ] Page margins, headers, and footers configured with page numbers
|
|
303
|
+
- [ ] PDF Content-Disposition header set correctly (inline for preview, attachment for download)
|
|
304
|
+
- [ ] Large batch generations use streaming or queued processing
|
|
305
|
+
- [ ] Browser instance cleaned up on server shutdown
|
|
306
|
+
- [ ] Fonts embedded or using web-safe fallbacks for consistent rendering
|
|
307
|
+
- [ ] PDF generation errors return proper HTTP error responses
|