@mars-stack/cli 0.2.0 → 0.2.2
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/package.json +2 -2
- package/template/.cursor/rules/composition-patterns.mdc +186 -0
- package/template/.cursor/rules/data-access.mdc +29 -0
- package/template/.cursor/rules/project-structure.mdc +34 -0
- package/template/.cursor/rules/security.mdc +25 -0
- package/template/.cursor/rules/testing.mdc +24 -0
- package/template/.cursor/rules/ui-conventions.mdc +29 -0
- package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
- package/template/.cursor/skills/add-audit-log/SKILL.md +373 -0
- package/template/.cursor/skills/add-blog/SKILL.md +447 -0
- package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
- package/template/.cursor/skills/add-component/SKILL.md +158 -0
- package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
- package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
- package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
- package/template/.cursor/skills/add-feature/SKILL.md +174 -0
- package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
- package/template/.cursor/skills/add-page/SKILL.md +151 -0
- package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
- package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
- package/template/.cursor/skills/add-role/SKILL.md +156 -0
- package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
- package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
- package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
- package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
- package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
- package/template/.cursor/skills/build-form/SKILL.md +231 -0
- package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
- package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
- package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
- package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
- package/template/.cursor/skills/configure-email/SKILL.md +170 -0
- package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
- package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
- package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
- package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
- package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
- package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
- package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
- package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
- package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
- package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
- package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
- package/template/.cursor/skills/configure-search/SKILL.md +581 -0
- package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
- package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
- package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
- package/template/.cursor/skills/create-seed/SKILL.md +191 -0
- package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
- package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
- package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
- package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
- package/template/.cursor/skills/setup-project/SKILL.md +104 -0
- package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
- package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
- package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
- package/template/AGENTS.md +104 -0
- package/template/ARCHITECTURE.md +102 -0
- package/template/docs/QUALITY_SCORE.md +20 -0
- package/template/docs/design-docs/conversation-as-system-record.md +70 -0
- package/template/docs/design-docs/core-beliefs.md +43 -0
- package/template/docs/design-docs/index.md +8 -0
- package/template/docs/exec-plans/active/.gitkeep +0 -0
- package/template/docs/exec-plans/completed/.gitkeep +0 -0
- package/template/docs/exec-plans/tech-debt.md +7 -0
- package/template/docs/generated/.gitkeep +0 -0
- package/template/docs/product-specs/index.md +7 -0
- package/template/docs/references/index.md +18 -0
- package/template/e2e/api.spec.ts +20 -0
- package/template/e2e/auth.spec.ts +24 -0
- package/template/e2e/public.spec.ts +25 -0
- package/template/eslint.config.mjs +24 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.ts +45 -0
- package/template/package.json +80 -0
- package/template/playwright.config.ts +31 -0
- package/template/postcss.config.mjs +8 -0
- package/template/prisma/generated/prisma/browser.ts +49 -0
- package/template/prisma/generated/prisma/client.ts +73 -0
- package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
- package/template/prisma/generated/prisma/enums.ts +15 -0
- package/template/prisma/generated/prisma/internal/class.ts +254 -0
- package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
- package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
- package/template/prisma/generated/prisma/models/Account.ts +1543 -0
- package/template/prisma/generated/prisma/models/File.ts +1529 -0
- package/template/prisma/generated/prisma/models/Session.ts +1415 -0
- package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
- package/template/prisma/generated/prisma/models/User.ts +2235 -0
- package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
- package/template/prisma/generated/prisma/models.ts +17 -0
- package/template/prisma/schema/auth.prisma +69 -0
- package/template/prisma/schema/base.prisma +8 -0
- package/template/prisma/schema/file.prisma +15 -0
- package/template/prisma/schema/subscription.prisma +17 -0
- package/template/prisma.config.ts +13 -0
- package/template/scripts/check-architecture.ts +221 -0
- package/template/scripts/check-doc-freshness.ts +242 -0
- package/template/scripts/ensure-db.mjs +291 -0
- package/template/scripts/generate-docs.ts +143 -0
- package/template/scripts/generate-env-example.ts +89 -0
- package/template/scripts/seed.ts +56 -0
- package/template/scripts/update-quality-score.ts +263 -0
- package/template/src/__tests__/architecture.test.ts +114 -0
- package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
- package/template/src/app/(auth)/layout.tsx +11 -0
- package/template/src/app/(auth)/register/page.tsx +162 -0
- package/template/src/app/(auth)/reset-password/page.tsx +109 -0
- package/template/src/app/(auth)/sign-in/page.tsx +122 -0
- package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
- package/template/src/app/(auth)/verify/page.tsx +56 -0
- package/template/src/app/(protected)/admin/page.tsx +108 -0
- package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
- package/template/src/app/(protected)/dashboard/page.tsx +22 -0
- package/template/src/app/(protected)/layout.tsx +262 -0
- package/template/src/app/(protected)/settings/page.tsx +370 -0
- package/template/src/app/api/auth/forgot/route.ts +63 -0
- package/template/src/app/api/auth/login/route.ts +121 -0
- package/template/src/app/api/auth/logout/route.ts +19 -0
- package/template/src/app/api/auth/me/route.ts +30 -0
- package/template/src/app/api/auth/reset/route.ts +45 -0
- package/template/src/app/api/auth/signup/route.ts +85 -0
- package/template/src/app/api/auth/verify/route.ts +46 -0
- package/template/src/app/api/csrf/route.ts +12 -0
- package/template/src/app/api/health/route.ts +10 -0
- package/template/src/app/api/protected/admin/users/route.ts +24 -0
- package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
- package/template/src/app/api/protected/billing/portal/route.ts +39 -0
- package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
- package/template/src/app/api/protected/files/upload/route.ts +64 -0
- package/template/src/app/api/protected/user/password/route.ts +63 -0
- package/template/src/app/api/protected/user/profile/route.ts +35 -0
- package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
- package/template/src/app/api/protected/user/sessions/route.ts +22 -0
- package/template/src/app/api/readiness/route.ts +15 -0
- package/template/src/app/api/webhooks/stripe/route.ts +166 -0
- package/template/src/app/error.tsx +33 -0
- package/template/src/app/layout.tsx +29 -0
- package/template/src/app/not-found.tsx +20 -0
- package/template/src/app/page.tsx +136 -0
- package/template/src/app/privacy/page.tsx +178 -0
- package/template/src/app/providers.tsx +8 -0
- package/template/src/app/terms/page.tsx +139 -0
- package/template/src/config/app.config.ts +70 -0
- package/template/src/config/routes.ts +17 -0
- package/template/src/features/admin/index.ts +11 -0
- package/template/src/features/admin/permissions.ts +64 -0
- package/template/src/features/auth/context/AuthContext.tsx +96 -0
- package/template/src/features/auth/context/index.ts +2 -0
- package/template/src/features/auth/index.ts +3 -0
- package/template/src/features/auth/server/consent.ts +66 -0
- package/template/src/features/auth/server/session-revocation.ts +20 -0
- package/template/src/features/auth/server/sessions.ts +66 -0
- package/template/src/features/auth/server/user.ts +166 -0
- package/template/src/features/auth/types.ts +19 -0
- package/template/src/features/auth/validators.ts +29 -0
- package/template/src/features/billing/server/index.ts +66 -0
- package/template/src/features/billing/types.ts +43 -0
- package/template/src/features/uploads/server/index.ts +49 -0
- package/template/src/features/uploads/types.ts +26 -0
- package/template/src/lib/core/email/templates/base-layout.ts +122 -0
- package/template/src/lib/core/email/templates/index.ts +4 -0
- package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
- package/template/src/lib/core/email/templates/verification-email.ts +41 -0
- package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
- package/template/src/lib/mars.ts +56 -0
- package/template/src/lib/prisma.ts +19 -0
- package/template/src/proxy.ts +92 -0
- package/template/src/styles/brand.css +17 -0
- package/template/src/styles/globals.css +6 -0
- package/template/tsconfig.json +59 -0
- package/template/vitest.config.ts +41 -0
- package/template/vitest.setup.ts +24 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Skill: Configure Payments
|
|
2
|
+
|
|
3
|
+
Set up Stripe payments including checkout, subscriptions, and webhooks in a MARS application.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
Use this skill when the user asks to add payments, billing, subscriptions, Stripe, or checkout.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- A Stripe account at [stripe.com](https://stripe.com)
|
|
12
|
+
- `appConfig.features.billing` set to `true`
|
|
13
|
+
- `appConfig.services.payments.provider` set to `'stripe'`
|
|
14
|
+
|
|
15
|
+
## Step 1: Install Stripe SDK
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yarn add stripe
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Step 2: Environment Variables
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
STRIPE_SECRET_KEY="sk_test_..."
|
|
25
|
+
STRIPE_WEBHOOK_SECRET="whsec_..."
|
|
26
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Step 3: Create the Stripe Service
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// src/features/billing/server/stripe.ts
|
|
33
|
+
import 'server-only';
|
|
34
|
+
|
|
35
|
+
import Stripe from 'stripe';
|
|
36
|
+
|
|
37
|
+
let _stripe: Stripe | null = null;
|
|
38
|
+
|
|
39
|
+
export function getStripe(): Stripe {
|
|
40
|
+
if (_stripe) return _stripe;
|
|
41
|
+
|
|
42
|
+
const secretKey = process.env.STRIPE_SECRET_KEY;
|
|
43
|
+
if (!secretKey) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'STRIPE_SECRET_KEY is not set.\n'
|
|
46
|
+
+ ' → Get your key from https://dashboard.stripe.com/apikeys\n'
|
|
47
|
+
+ ' → Add it to your .env file',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_stripe = new Stripe(secretKey, { apiVersion: '2024-12-18.acacia' });
|
|
52
|
+
return _stripe;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Step 4: Prisma Schema for Subscriptions
|
|
57
|
+
|
|
58
|
+
```prisma
|
|
59
|
+
// prisma/schema/billing.prisma
|
|
60
|
+
model Subscription {
|
|
61
|
+
id String @id @default(cuid())
|
|
62
|
+
userId String @unique
|
|
63
|
+
stripeCustomerId String @unique
|
|
64
|
+
stripeSubscriptionId String? @unique
|
|
65
|
+
stripePriceId String?
|
|
66
|
+
status String @default("inactive")
|
|
67
|
+
currentPeriodEnd DateTime?
|
|
68
|
+
cancelAtPeriodEnd Boolean @default(false)
|
|
69
|
+
createdAt DateTime @default(now())
|
|
70
|
+
updatedAt DateTime @updatedAt
|
|
71
|
+
|
|
72
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
73
|
+
|
|
74
|
+
@@index([stripeCustomerId])
|
|
75
|
+
@@index([status])
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Step 5: Checkout API Route
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// src/app/api/protected/billing/checkout/route.ts
|
|
83
|
+
import { handleApiError, withAuthNoParams, type AuthenticatedRequest } from '@/lib/mars';
|
|
84
|
+
import { prisma } from '@/lib/prisma';
|
|
85
|
+
import { getStripe } from '@/features/billing/server';
|
|
86
|
+
import { NextResponse } from 'next/server';
|
|
87
|
+
import { z } from 'zod';
|
|
88
|
+
|
|
89
|
+
const checkoutSchema = z.object({
|
|
90
|
+
priceId: z.string().min(1),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const POST = withAuthNoParams(async (request: AuthenticatedRequest) => {
|
|
94
|
+
try {
|
|
95
|
+
const { priceId } = checkoutSchema.parse(await request.json());
|
|
96
|
+
const stripe = getStripe();
|
|
97
|
+
const userId = request.session.userId;
|
|
98
|
+
|
|
99
|
+
// Find or create Stripe customer
|
|
100
|
+
let subscription = await prisma.subscription.findUnique({ where: { userId } });
|
|
101
|
+
|
|
102
|
+
if (!subscription) {
|
|
103
|
+
const customer = await stripe.customers.create({
|
|
104
|
+
email: request.session.email,
|
|
105
|
+
metadata: { userId },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
subscription = await prisma.subscription.create({
|
|
109
|
+
data: { userId, stripeCustomerId: customer.id },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create checkout session
|
|
114
|
+
const session = await stripe.checkout.sessions.create({
|
|
115
|
+
customer: subscription.stripeCustomerId,
|
|
116
|
+
mode: 'subscription',
|
|
117
|
+
payment_method_types: ['card'],
|
|
118
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
119
|
+
success_url: `${process.env.APP_URL || 'http://localhost:3000'}/billing?success=true`,
|
|
120
|
+
cancel_url: `${process.env.APP_URL || 'http://localhost:3000'}/billing?canceled=true`,
|
|
121
|
+
metadata: { userId },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return NextResponse.json({ url: session.url });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return handleApiError(error, { endpoint: '/api/protected/billing/checkout' });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Step 6: Customer Portal Route
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// src/app/api/protected/billing/portal/route.ts
|
|
135
|
+
import { handleApiError, withAuthNoParams, type AuthenticatedRequest } from '@/lib/mars';
|
|
136
|
+
import { prisma } from '@/lib/prisma';
|
|
137
|
+
import { getStripe } from '@/features/billing/server';
|
|
138
|
+
import { NextResponse } from 'next/server';
|
|
139
|
+
|
|
140
|
+
export const POST = withAuthNoParams(async (request: AuthenticatedRequest) => {
|
|
141
|
+
try {
|
|
142
|
+
const stripe = getStripe();
|
|
143
|
+
const subscription = await prisma.subscription.findUnique({
|
|
144
|
+
where: { userId: request.session.userId },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!subscription) {
|
|
148
|
+
return NextResponse.json({ error: 'No subscription found' }, { status: 404 });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
152
|
+
customer: subscription.stripeCustomerId,
|
|
153
|
+
return_url: `${process.env.APP_URL || 'http://localhost:3000'}/billing`,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return NextResponse.json({ url: session.url });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return handleApiError(error, { endpoint: '/api/protected/billing/portal' });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Step 7: Stripe Webhook
|
|
164
|
+
|
|
165
|
+
See the `add-webhook` skill for the full pattern. Key events to handle:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
switch (event.type) {
|
|
169
|
+
case 'checkout.session.completed': {
|
|
170
|
+
const session = event.data.object;
|
|
171
|
+
await prisma.subscription.update({
|
|
172
|
+
where: { stripeCustomerId: session.customer as string },
|
|
173
|
+
data: {
|
|
174
|
+
stripeSubscriptionId: session.subscription as string,
|
|
175
|
+
status: 'active',
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case 'customer.subscription.updated': {
|
|
181
|
+
const sub = event.data.object;
|
|
182
|
+
await prisma.subscription.update({
|
|
183
|
+
where: { stripeSubscriptionId: sub.id },
|
|
184
|
+
data: {
|
|
185
|
+
status: sub.status,
|
|
186
|
+
stripePriceId: sub.items.data[0]?.price.id,
|
|
187
|
+
currentPeriodEnd: new Date(sub.current_period_end * 1000),
|
|
188
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'customer.subscription.deleted': {
|
|
194
|
+
const sub = event.data.object;
|
|
195
|
+
await prisma.subscription.update({
|
|
196
|
+
where: { stripeSubscriptionId: sub.id },
|
|
197
|
+
data: { status: 'canceled' },
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Step 8: Subscription Check Helper
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// src/features/billing/server/index.ts
|
|
208
|
+
import 'server-only';
|
|
209
|
+
|
|
210
|
+
import { prisma } from '@/lib/prisma';
|
|
211
|
+
|
|
212
|
+
export async function getUserSubscription(userId: string) {
|
|
213
|
+
return prisma.subscription.findUnique({ where: { userId } });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function isSubscribed(userId: string): Promise<boolean> {
|
|
217
|
+
const sub = await getUserSubscription(userId);
|
|
218
|
+
return sub?.status === 'active' || sub?.status === 'trialing';
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Local Development
|
|
223
|
+
|
|
224
|
+
Test webhooks locally with Stripe CLI:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
stripe listen --forward-to localhost:3000/api/webhooks/stripe
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
This prints a webhook signing secret -- use it as `STRIPE_WEBHOOK_SECRET`.
|
|
231
|
+
|
|
232
|
+
## Checklist
|
|
233
|
+
|
|
234
|
+
- [ ] Stripe SDK installed
|
|
235
|
+
- [ ] Environment variables set (`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`)
|
|
236
|
+
- [ ] Lazy Stripe client created (`src/features/billing/server/stripe.ts`)
|
|
237
|
+
- [ ] Subscription model in Prisma schema
|
|
238
|
+
- [ ] Checkout session API route
|
|
239
|
+
- [ ] Customer portal API route
|
|
240
|
+
- [ ] Webhook handler for subscription events
|
|
241
|
+
- [ ] Subscription check helper functions
|
|
242
|
+
- [ ] Webhook excluded from CSRF in middleware
|
|
243
|
+
- [ ] Feature flag checked (`appConfig.features.billing`)
|