@girardmedia/bootspring 1.2.0 → 2.0.3
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/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
# Invoice Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for invoice management with Stripe.
|
|
4
|
-
|
|
5
|
-
## Get Invoices
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/billing.ts
|
|
9
|
-
import Stripe from 'stripe'
|
|
10
|
-
|
|
11
|
-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
12
|
-
|
|
13
|
-
export async function getInvoices(userId: string, limit = 10) {
|
|
14
|
-
const user = await prisma.user.findUnique({
|
|
15
|
-
where: { id: userId }
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
if (!user?.stripeCustomerId) {
|
|
19
|
-
return []
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const invoices = await stripe.invoices.list({
|
|
23
|
-
customer: user.stripeCustomerId,
|
|
24
|
-
limit,
|
|
25
|
-
expand: ['data.subscription']
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
return invoices.data.map(invoice => ({
|
|
29
|
-
id: invoice.id,
|
|
30
|
-
number: invoice.number,
|
|
31
|
-
status: invoice.status,
|
|
32
|
-
amount: invoice.amount_due,
|
|
33
|
-
currency: invoice.currency,
|
|
34
|
-
created: new Date(invoice.created * 1000),
|
|
35
|
-
dueDate: invoice.due_date ? new Date(invoice.due_date * 1000) : null,
|
|
36
|
-
paidAt: invoice.status_transitions.paid_at
|
|
37
|
-
? new Date(invoice.status_transitions.paid_at * 1000)
|
|
38
|
-
: null,
|
|
39
|
-
pdfUrl: invoice.invoice_pdf,
|
|
40
|
-
hostedUrl: invoice.hosted_invoice_url
|
|
41
|
-
}))
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Invoice API Route
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
// app/api/billing/invoices/route.ts
|
|
49
|
-
import { auth } from '@/auth'
|
|
50
|
-
import { getInvoices } from '@/lib/billing'
|
|
51
|
-
|
|
52
|
-
export async function GET(request: Request) {
|
|
53
|
-
const session = await auth()
|
|
54
|
-
|
|
55
|
-
if (!session?.user) {
|
|
56
|
-
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const { searchParams } = new URL(request.url)
|
|
60
|
-
const limit = parseInt(searchParams.get('limit') ?? '10')
|
|
61
|
-
|
|
62
|
-
const invoices = await getInvoices(session.user.id, limit)
|
|
63
|
-
|
|
64
|
-
return Response.json({ data: invoices })
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Invoice List Component
|
|
69
|
-
|
|
70
|
-
```tsx
|
|
71
|
-
// components/billing/InvoiceList.tsx
|
|
72
|
-
'use client'
|
|
73
|
-
|
|
74
|
-
import { useState, useEffect } from 'react'
|
|
75
|
-
import { formatCurrency, formatDate } from '@/lib/utils'
|
|
76
|
-
|
|
77
|
-
interface Invoice {
|
|
78
|
-
id: string
|
|
79
|
-
number: string | null
|
|
80
|
-
status: string | null
|
|
81
|
-
amount: number
|
|
82
|
-
currency: string
|
|
83
|
-
created: string
|
|
84
|
-
pdfUrl: string | null
|
|
85
|
-
hostedUrl: string | null
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function InvoiceList() {
|
|
89
|
-
const [invoices, setInvoices] = useState<Invoice[]>([])
|
|
90
|
-
const [loading, setLoading] = useState(true)
|
|
91
|
-
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
fetch('/api/billing/invoices')
|
|
94
|
-
.then(res => res.json())
|
|
95
|
-
.then(data => setInvoices(data.data))
|
|
96
|
-
.finally(() => setLoading(false))
|
|
97
|
-
}, [])
|
|
98
|
-
|
|
99
|
-
if (loading) {
|
|
100
|
-
return <div>Loading invoices...</div>
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (invoices.length === 0) {
|
|
104
|
-
return <p className="text-gray-500">No invoices yet</p>
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<div className="divide-y rounded-lg border">
|
|
109
|
-
{invoices.map(invoice => (
|
|
110
|
-
<div key={invoice.id} className="flex items-center justify-between p-4">
|
|
111
|
-
<div>
|
|
112
|
-
<p className="font-medium">
|
|
113
|
-
Invoice {invoice.number ?? invoice.id}
|
|
114
|
-
</p>
|
|
115
|
-
<p className="text-sm text-gray-500">
|
|
116
|
-
{formatDate(new Date(invoice.created))}
|
|
117
|
-
</p>
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<div className="flex items-center gap-4">
|
|
121
|
-
<span className={`rounded-full px-2 py-1 text-xs ${
|
|
122
|
-
invoice.status === 'paid' ? 'bg-green-100 text-green-800' :
|
|
123
|
-
invoice.status === 'open' ? 'bg-amber-100 text-amber-800' :
|
|
124
|
-
'bg-gray-100 text-gray-800'
|
|
125
|
-
}`}>
|
|
126
|
-
{invoice.status}
|
|
127
|
-
</span>
|
|
128
|
-
|
|
129
|
-
<span className="font-medium">
|
|
130
|
-
{formatCurrency(invoice.amount / 100, invoice.currency)}
|
|
131
|
-
</span>
|
|
132
|
-
|
|
133
|
-
{invoice.pdfUrl && (
|
|
134
|
-
<a
|
|
135
|
-
href={invoice.pdfUrl}
|
|
136
|
-
target="_blank"
|
|
137
|
-
rel="noopener noreferrer"
|
|
138
|
-
className="text-blue-600 hover:underline"
|
|
139
|
-
>
|
|
140
|
-
PDF
|
|
141
|
-
</a>
|
|
142
|
-
)}
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
))}
|
|
146
|
-
</div>
|
|
147
|
-
)
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## Handle Invoice Webhooks
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
// app/api/webhooks/stripe/route.ts
|
|
155
|
-
async function handleInvoiceEvents(event: Stripe.Event) {
|
|
156
|
-
switch (event.type) {
|
|
157
|
-
case 'invoice.paid': {
|
|
158
|
-
const invoice = event.data.object as Stripe.Invoice
|
|
159
|
-
await handleInvoicePaid(invoice)
|
|
160
|
-
break
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
case 'invoice.payment_failed': {
|
|
164
|
-
const invoice = event.data.object as Stripe.Invoice
|
|
165
|
-
await handleInvoicePaymentFailed(invoice)
|
|
166
|
-
break
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
case 'invoice.upcoming': {
|
|
170
|
-
const invoice = event.data.object as Stripe.Invoice
|
|
171
|
-
await handleUpcomingInvoice(invoice)
|
|
172
|
-
break
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function handleInvoicePaid(invoice: Stripe.Invoice) {
|
|
178
|
-
const customerId = invoice.customer as string
|
|
179
|
-
|
|
180
|
-
const user = await prisma.user.findFirst({
|
|
181
|
-
where: { stripeCustomerId: customerId }
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
if (!user) return
|
|
185
|
-
|
|
186
|
-
// Store invoice record
|
|
187
|
-
await prisma.invoice.create({
|
|
188
|
-
data: {
|
|
189
|
-
userId: user.id,
|
|
190
|
-
stripeInvoiceId: invoice.id,
|
|
191
|
-
amount: invoice.amount_paid,
|
|
192
|
-
currency: invoice.currency,
|
|
193
|
-
status: 'paid',
|
|
194
|
-
paidAt: new Date()
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
// Send receipt email
|
|
199
|
-
await emailService.sendInvoiceReceipt(user.email, {
|
|
200
|
-
invoiceNumber: invoice.number!,
|
|
201
|
-
amount: invoice.amount_paid,
|
|
202
|
-
currency: invoice.currency,
|
|
203
|
-
pdfUrl: invoice.invoice_pdf!
|
|
204
|
-
})
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
|
|
208
|
-
const customerId = invoice.customer as string
|
|
209
|
-
|
|
210
|
-
const user = await prisma.user.findFirst({
|
|
211
|
-
where: { stripeCustomerId: customerId }
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
if (!user) return
|
|
215
|
-
|
|
216
|
-
// Notify user
|
|
217
|
-
await emailService.sendPaymentFailed(user.email, {
|
|
218
|
-
amount: invoice.amount_due,
|
|
219
|
-
currency: invoice.currency,
|
|
220
|
-
updateUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async function handleUpcomingInvoice(invoice: Stripe.Invoice) {
|
|
225
|
-
// Send reminder for upcoming charge
|
|
226
|
-
const customerId = invoice.customer as string
|
|
227
|
-
|
|
228
|
-
const user = await prisma.user.findFirst({
|
|
229
|
-
where: { stripeCustomerId: customerId }
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
if (!user) return
|
|
233
|
-
|
|
234
|
-
await emailService.sendUpcomingInvoice(user.email, {
|
|
235
|
-
amount: invoice.amount_due,
|
|
236
|
-
currency: invoice.currency,
|
|
237
|
-
date: new Date(invoice.created * 1000)
|
|
238
|
-
})
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
## Create Custom Invoice
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
// lib/billing.ts
|
|
246
|
-
export async function createInvoice(
|
|
247
|
-
userId: string,
|
|
248
|
-
items: { description: string; amount: number }[]
|
|
249
|
-
) {
|
|
250
|
-
const user = await prisma.user.findUnique({
|
|
251
|
-
where: { id: userId }
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
if (!user?.stripeCustomerId) {
|
|
255
|
-
throw new Error('No billing account')
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Create invoice items
|
|
259
|
-
for (const item of items) {
|
|
260
|
-
await stripe.invoiceItems.create({
|
|
261
|
-
customer: user.stripeCustomerId,
|
|
262
|
-
amount: item.amount,
|
|
263
|
-
currency: 'usd',
|
|
264
|
-
description: item.description
|
|
265
|
-
})
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Create and send invoice
|
|
269
|
-
const invoice = await stripe.invoices.create({
|
|
270
|
-
customer: user.stripeCustomerId,
|
|
271
|
-
auto_advance: true, // Auto-finalize
|
|
272
|
-
collection_method: 'send_invoice',
|
|
273
|
-
days_until_due: 30
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
await stripe.invoices.sendInvoice(invoice.id)
|
|
277
|
-
|
|
278
|
-
return invoice
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
## When to Use
|
|
283
|
-
|
|
284
|
-
- Billing history
|
|
285
|
-
- Payment receipts
|
|
286
|
-
- Failed payment recovery
|
|
287
|
-
- Custom invoicing
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
# Stripe Customer Portal Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for self-service billing with Stripe Customer Portal.
|
|
4
|
-
|
|
5
|
-
## Create Portal Session
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/billing.ts
|
|
9
|
-
import Stripe from 'stripe'
|
|
10
|
-
import { prisma } from '@/lib/db'
|
|
11
|
-
|
|
12
|
-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
13
|
-
|
|
14
|
-
export async function createPortalSession(userId: string) {
|
|
15
|
-
const user = await prisma.user.findUnique({
|
|
16
|
-
where: { id: userId }
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
if (!user?.stripeCustomerId) {
|
|
20
|
-
throw new Error('No billing account found')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const session = await stripe.billingPortal.sessions.create({
|
|
24
|
-
customer: user.stripeCustomerId,
|
|
25
|
-
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
return session.url
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## API Route
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
// app/api/billing/portal/route.ts
|
|
36
|
-
import { auth } from '@/auth'
|
|
37
|
-
import { createPortalSession } from '@/lib/billing'
|
|
38
|
-
import { redirect } from 'next/navigation'
|
|
39
|
-
|
|
40
|
-
export async function POST() {
|
|
41
|
-
const session = await auth()
|
|
42
|
-
|
|
43
|
-
if (!session?.user) {
|
|
44
|
-
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const url = await createPortalSession(session.user.id)
|
|
49
|
-
return Response.json({ url })
|
|
50
|
-
} catch (error) {
|
|
51
|
-
return Response.json(
|
|
52
|
-
{ error: 'Failed to create portal session' },
|
|
53
|
-
{ status: 500 }
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Portal Button Component
|
|
60
|
-
|
|
61
|
-
```tsx
|
|
62
|
-
// components/billing/PortalButton.tsx
|
|
63
|
-
'use client'
|
|
64
|
-
|
|
65
|
-
import { useState } from 'react'
|
|
66
|
-
import { Button } from '@/components/ui/Button'
|
|
67
|
-
|
|
68
|
-
export function PortalButton() {
|
|
69
|
-
const [loading, setLoading] = useState(false)
|
|
70
|
-
|
|
71
|
-
async function handleClick() {
|
|
72
|
-
setLoading(true)
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const response = await fetch('/api/billing/portal', {
|
|
76
|
-
method: 'POST'
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const { url } = await response.json()
|
|
80
|
-
|
|
81
|
-
if (url) {
|
|
82
|
-
window.location.href = url
|
|
83
|
-
}
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.error('Failed to open billing portal:', error)
|
|
86
|
-
} finally {
|
|
87
|
-
setLoading(false)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<Button onClick={handleClick} disabled={loading}>
|
|
93
|
-
{loading ? 'Loading...' : 'Manage Billing'}
|
|
94
|
-
</Button>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Portal Configuration
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
// Configure portal in Stripe Dashboard or via API
|
|
103
|
-
const configuration = await stripe.billingPortal.configurations.create({
|
|
104
|
-
business_profile: {
|
|
105
|
-
headline: 'Manage your subscription'
|
|
106
|
-
},
|
|
107
|
-
features: {
|
|
108
|
-
subscription_cancel: {
|
|
109
|
-
enabled: true,
|
|
110
|
-
mode: 'at_period_end',
|
|
111
|
-
proration_behavior: 'none',
|
|
112
|
-
cancellation_reason: {
|
|
113
|
-
enabled: true,
|
|
114
|
-
options: [
|
|
115
|
-
'too_expensive',
|
|
116
|
-
'missing_features',
|
|
117
|
-
'switched_service',
|
|
118
|
-
'unused',
|
|
119
|
-
'other'
|
|
120
|
-
]
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
subscription_update: {
|
|
124
|
-
enabled: true,
|
|
125
|
-
default_allowed_updates: ['price'],
|
|
126
|
-
proration_behavior: 'create_prorations',
|
|
127
|
-
products: [
|
|
128
|
-
{
|
|
129
|
-
product: process.env.STRIPE_PRODUCT_ID!,
|
|
130
|
-
prices: [
|
|
131
|
-
process.env.STRIPE_PRO_PRICE_ID!,
|
|
132
|
-
process.env.STRIPE_ENTERPRISE_PRICE_ID!
|
|
133
|
-
]
|
|
134
|
-
}
|
|
135
|
-
]
|
|
136
|
-
},
|
|
137
|
-
payment_method_update: {
|
|
138
|
-
enabled: true
|
|
139
|
-
},
|
|
140
|
-
invoice_history: {
|
|
141
|
-
enabled: true
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
## Portal with Flow
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
// lib/billing.ts
|
|
151
|
-
export async function createPortalSessionWithFlow(
|
|
152
|
-
userId: string,
|
|
153
|
-
flow: 'subscription_cancel' | 'payment_method_update' | 'subscription_update'
|
|
154
|
-
) {
|
|
155
|
-
const user = await prisma.user.findUnique({
|
|
156
|
-
where: { id: userId }
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
if (!user?.stripeCustomerId) {
|
|
160
|
-
throw new Error('No billing account found')
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const subscription = await prisma.subscription.findUnique({
|
|
164
|
-
where: { userId }
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
const flowData: Stripe.BillingPortal.SessionCreateParams.FlowData = {
|
|
168
|
-
type: flow
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Add subscription ID for cancel/update flows
|
|
172
|
-
if (flow === 'subscription_cancel' || flow === 'subscription_update') {
|
|
173
|
-
flowData.subscription_cancel = {
|
|
174
|
-
subscription: subscription?.stripeSubscriptionId!
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const session = await stripe.billingPortal.sessions.create({
|
|
179
|
-
customer: user.stripeCustomerId,
|
|
180
|
-
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
|
|
181
|
-
flow_data: flowData
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
return session.url
|
|
185
|
-
}
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Billing Page
|
|
189
|
-
|
|
190
|
-
```tsx
|
|
191
|
-
// app/settings/billing/page.tsx
|
|
192
|
-
import { auth } from '@/auth'
|
|
193
|
-
import { getUserPlan, getSubscription } from '@/lib/billing'
|
|
194
|
-
import { PortalButton } from '@/components/billing/PortalButton'
|
|
195
|
-
import { PlanCard } from '@/components/billing/PlanCard'
|
|
196
|
-
import { PLANS } from '@/lib/plans'
|
|
197
|
-
|
|
198
|
-
export default async function BillingPage() {
|
|
199
|
-
const session = await auth()
|
|
200
|
-
const planId = await getUserPlan(session!.user.id)
|
|
201
|
-
const subscription = await getSubscription(session!.user.id)
|
|
202
|
-
const plan = PLANS[planId]
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<div className="space-y-8">
|
|
206
|
-
<div>
|
|
207
|
-
<h1 className="text-2xl font-bold">Billing</h1>
|
|
208
|
-
<p className="text-gray-600">Manage your subscription and billing</p>
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
<div className="rounded-lg border p-6">
|
|
212
|
-
<div className="flex items-center justify-between">
|
|
213
|
-
<div>
|
|
214
|
-
<h2 className="font-semibold">Current Plan</h2>
|
|
215
|
-
<p className="text-2xl font-bold">{plan.name}</p>
|
|
216
|
-
{subscription?.cancelAtPeriodEnd && (
|
|
217
|
-
<p className="text-sm text-amber-600">
|
|
218
|
-
Cancels on {subscription.currentPeriodEnd.toLocaleDateString()}
|
|
219
|
-
</p>
|
|
220
|
-
)}
|
|
221
|
-
</div>
|
|
222
|
-
<PortalButton />
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
<div className="grid gap-4 md:grid-cols-3">
|
|
227
|
-
{Object.entries(PLANS).map(([id, plan]) => (
|
|
228
|
-
<PlanCard
|
|
229
|
-
key={id}
|
|
230
|
-
plan={plan}
|
|
231
|
-
isCurrent={id === planId}
|
|
232
|
-
/>
|
|
233
|
-
))}
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
)
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## When to Use
|
|
241
|
-
|
|
242
|
-
- Subscription management
|
|
243
|
-
- Payment method updates
|
|
244
|
-
- Invoice history
|
|
245
|
-
- Plan changes
|