Skip to content

Commit ea1086a

Browse files
committed
Add Stripe payment integration with subscription support and webhooks
1 parent 966bba8 commit ea1086a

18 files changed

+2918
-9
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ USDC_ADDRESS=0x402282c72a2f2b9f059C3b39Fa63932D6AA09f11
4141
STRIPE_ACCOUNT_ID=your-stripe-account-id
4242
STRIPE_PUBLISHABLE_KEY=your-stripe-publishable-key
4343
STRIPE_SECRET_KEY=your-stripe-secret-key
44+
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

bin/create-stripe-products.js

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Stripe Product Management Script
5+
*
6+
* This script manages Stripe products and prices for the PDF service.
7+
* It ensures that the necessary products and prices exist in Stripe.
8+
*
9+
* Usage:
10+
* node bin/create-stripe-products.js
11+
*
12+
* Environment variables:
13+
* STRIPE_SECRET_KEY - Stripe API secret key
14+
*/
15+
16+
import Stripe from 'stripe';
17+
import dotenv from 'dotenv-flow';
18+
import { fileURLToPath } from 'url';
19+
import { dirname, join } from 'path';
20+
import fs from 'fs';
21+
22+
// Get the directory name
23+
const __filename = fileURLToPath(import.meta.url);
24+
const __dirname = dirname(__filename);
25+
26+
// Load environment variables
27+
dotenv.config({ path: join(__dirname, '..') });
28+
29+
// Initialize Stripe with the secret key
30+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
31+
32+
// Product configuration
33+
const PRODUCTS = [
34+
{
35+
name: 'PDF Service Subscription',
36+
description: 'Subscription for PDF conversion service',
37+
prices: [
38+
{
39+
nickname: 'Monthly Subscription',
40+
unit_amount: 500, // $5.00
41+
currency: 'usd',
42+
recurring: {
43+
interval: 'month'
44+
},
45+
metadata: {
46+
plan: 'monthly'
47+
}
48+
},
49+
{
50+
nickname: 'Yearly Subscription',
51+
unit_amount: 3000, // $30.00
52+
currency: 'usd',
53+
recurring: {
54+
interval: 'year'
55+
},
56+
metadata: {
57+
plan: 'yearly'
58+
}
59+
}
60+
]
61+
}
62+
];
63+
64+
/**
65+
* Create or update a product in Stripe
66+
* @param {Object} productConfig - Product configuration
67+
* @returns {Promise<Object>} - Created or updated product
68+
*/
69+
async function createOrUpdateProduct(productConfig) {
70+
console.log(`Processing product: ${productConfig.name}`);
71+
72+
// Check if product already exists
73+
const existingProducts = await stripe.products.list({
74+
active: true,
75+
limit: 100
76+
});
77+
78+
const existingProduct = existingProducts.data.find(p => p.name === productConfig.name);
79+
80+
let product;
81+
82+
if (existingProduct) {
83+
console.log(`Product "${productConfig.name}" already exists with ID: ${existingProduct.id}`);
84+
product = existingProduct;
85+
86+
// Update product if needed
87+
if (existingProduct.description !== productConfig.description) {
88+
console.log(`Updating product "${productConfig.name}"`);
89+
product = await stripe.products.update(existingProduct.id, {
90+
description: productConfig.description
91+
});
92+
}
93+
} else {
94+
// Create new product
95+
console.log(`Creating new product "${productConfig.name}"`);
96+
product = await stripe.products.create({
97+
name: productConfig.name,
98+
description: productConfig.description,
99+
active: true
100+
});
101+
}
102+
103+
// Process prices
104+
for (const priceConfig of productConfig.prices) {
105+
await createOrUpdatePrice(product.id, priceConfig);
106+
}
107+
108+
return product;
109+
}
110+
111+
/**
112+
* Create or update a price in Stripe
113+
* @param {string} productId - Stripe product ID
114+
* @param {Object} priceConfig - Price configuration
115+
* @returns {Promise<Object>} - Created or updated price
116+
*/
117+
async function createOrUpdatePrice(productId, priceConfig) {
118+
console.log(`Processing price: ${priceConfig.nickname}`);
119+
120+
// Check if price already exists
121+
const existingPrices = await stripe.prices.list({
122+
product: productId,
123+
active: true,
124+
limit: 100
125+
});
126+
127+
const existingPrice = existingPrices.data.find(p =>
128+
p.nickname === priceConfig.nickname &&
129+
p.unit_amount === priceConfig.unit_amount &&
130+
p.currency === priceConfig.currency &&
131+
p.recurring?.interval === priceConfig.recurring?.interval
132+
);
133+
134+
if (existingPrice) {
135+
console.log(`Price "${priceConfig.nickname}" already exists with ID: ${existingPrice.id}`);
136+
137+
// Update metadata if needed
138+
if (JSON.stringify(existingPrice.metadata) !== JSON.stringify(priceConfig.metadata)) {
139+
console.log(`Updating metadata for price "${priceConfig.nickname}"`);
140+
return await stripe.prices.update(existingPrice.id, {
141+
metadata: priceConfig.metadata
142+
});
143+
}
144+
145+
return existingPrice;
146+
} else {
147+
// Create new price
148+
console.log(`Creating new price "${priceConfig.nickname}"`);
149+
return await stripe.prices.create({
150+
product: productId,
151+
nickname: priceConfig.nickname,
152+
unit_amount: priceConfig.unit_amount,
153+
currency: priceConfig.currency,
154+
recurring: priceConfig.recurring,
155+
metadata: priceConfig.metadata
156+
});
157+
}
158+
}
159+
160+
/**
161+
* Main function to process all products
162+
*/
163+
async function main() {
164+
try {
165+
console.log('Starting Stripe product management...');
166+
167+
// Check if Stripe API key is set
168+
if (!process.env.STRIPE_SECRET_KEY) {
169+
throw new Error('STRIPE_SECRET_KEY environment variable is not set');
170+
}
171+
172+
// Process all products
173+
for (const productConfig of PRODUCTS) {
174+
await createOrUpdateProduct(productConfig);
175+
}
176+
177+
console.log('Stripe product management completed successfully');
178+
} catch (error) {
179+
console.error('Error in Stripe product management:', error);
180+
process.exit(1);
181+
}
182+
}
183+
184+
// Run the main function
185+
main();

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"start": "node src/index.js",
99
"dev": "nodemon src/index.js",
1010
"test": "node test.js",
11-
"deploy": "./bin/deploy.sh"
11+
"deploy": "./bin/deploy.sh",
12+
"stripe:products": "node bin/create-stripe-products.js"
1213
},
1314
"keywords": [
1415
"pdf",
@@ -27,15 +28,19 @@
2728
"@hono/node-server": "^1.8.2",
2829
"@profullstack/spa-router": "^1.0.1",
2930
"@supabase/supabase-js": "^2.49.4",
31+
"chalk": "^5.3.0",
32+
"commander": "^12.0.0",
3033
"dotenv-flow": "^4.1.0",
3134
"form-data": "^4.0.2",
3235
"hono": "^4.1.3",
36+
"inquirer": "^9.2.15",
3337
"jsdom": "^26.1.0",
3438
"mailgun.js": "^12.0.1",
3539
"marked": "^15.0.8",
3640
"pptxgenjs": "^3.12.0",
3741
"puppeteer": "^22.5.0",
3842
"qrcode": "^1.5.4",
43+
"stripe": "^14.22.0",
3944
"turndown": "^7.2.0",
4045
"uuid": "^11.1.0",
4146
"ws": "^8.18.1",

0 commit comments

Comments
 (0)