Skip to content

Commit 66846d1

Browse files
committed
feat: Implement Stripe Connect integration for coinpayportal
- Added database tables: stripe_accounts, stripe_transactions, stripe_disputes, stripe_payouts, stripe_escrows, did_reputation_events - Installed Stripe SDK - Created API routes: * POST /api/stripe/connect/onboard - Create Express account + return onboarding link * GET /api/stripe/connect/status/:accountId - Check account status * POST /api/stripe/payments/create - Create card payment (destination charge for gateway, separate charge for escrow) * POST /api/stripe/webhooks - Handle Stripe webhooks (payment_intent.succeeded, charge.dispute.created, etc.) * POST /api/stripe/escrow/release - Transfer held funds to merchant * POST /api/stripe/escrow/refund - Refund held funds - Added SDK methods to client.js: * createCardPayment() - creates Stripe checkout session * getStripeAccountStatus() - check Connect onboarding status * createStripeOnboardingLink() - get onboarding URL * releaseCardEscrow() - release card escrow funds * refundCardPayment() - refund a card payment - Created packages/sdk/src/card-payments.js with convenience functions - Updated SDK docs page with Credit Card Payments section showing: * How to onboard merchants via Stripe Connect * How to create card payments via SDK * Card escrow flow * Webhook events for card payments * Example showing customer choosing crypto OR card - Added Credit Card Payments to SDK docs Quick Navigation grid - Bumped patch version to 0.6.4 - Added basic tests for new API routes - Platform fee logic: Free tier 1% / Pro tier 0.5% using application_fee_amount on destination charges - Environment variables: STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY Follows STRIPE-PAYMENTS-PRD.md closely. Ready for merchants to start accepting card payments alongside crypto.
1 parent cffe7fc commit 66846d1

File tree

14 files changed

+2341
-0
lines changed

14 files changed

+2341
-0
lines changed

docs/STRIPE-PAYMENTS-PRD.md

Lines changed: 413 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"qrcode": "^1.5.3",
5858
"react": "^19.0.0",
5959
"react-dom": "^19.0.0",
60+
"stripe": "^20.3.1",
6061
"tiny-secp256k1": "^2.2.4",
6162
"tweetnacl": "^1.0.3",
6263
"viem": "^2.40.3",

packages/sdk/src/card-payments.js

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
/**
2+
* CoinPay Card Payments - Convenience functions for Stripe integration
3+
*
4+
* This module provides high-level convenience functions for working with
5+
* card payments through Stripe Connect, similar to the payments.js module.
6+
*/
7+
8+
/**
9+
* Quick card payment creation with sensible defaults
10+
*
11+
* @param {import('./client.js').CoinPayClient} client - CoinPay client instance
12+
* @param {string} businessId - Business ID
13+
* @param {number} amountUSD - Amount in USD (will be converted to cents)
14+
* @param {string} description - Payment description
15+
* @param {Object} [options] - Additional options
16+
* @param {Object} [options.metadata] - Custom metadata
17+
* @param {string} [options.successUrl] - Success redirect URL
18+
* @param {string} [options.cancelUrl] - Cancel redirect URL
19+
* @param {boolean} [options.escrowMode=false] - Enable escrow mode
20+
* @returns {Promise<Object>} Payment session with checkout URL
21+
*
22+
* @example
23+
* import { createQuickCardPayment } from '@profullstack/coinpay/card-payments';
24+
*
25+
* const payment = await createQuickCardPayment(client, 'business-id', 50, 'Order #123', {
26+
* metadata: { orderId: '123' },
27+
* escrowMode: true
28+
* });
29+
*
30+
* // Redirect customer to: payment.checkout_url
31+
*/
32+
export async function createQuickCardPayment(client, businessId, amountUSD, description, options = {}) {
33+
const {
34+
metadata = {},
35+
successUrl,
36+
cancelUrl,
37+
escrowMode = false,
38+
} = options;
39+
40+
// Convert USD to cents
41+
const amountCents = Math.round(amountUSD * 100);
42+
43+
return client.createCardPayment({
44+
businessId,
45+
amount: amountCents,
46+
currency: 'usd',
47+
description,
48+
metadata,
49+
successUrl,
50+
cancelUrl,
51+
escrowMode,
52+
});
53+
}
54+
55+
/**
56+
* Wait for merchant to complete Stripe onboarding
57+
*
58+
* Polls the Stripe account status until onboarding is complete.
59+
* Useful for integration flows where you need to wait for merchant setup.
60+
*
61+
* @param {import('./client.js').CoinPayClient} client - CoinPay client instance
62+
* @param {string} businessId - Business ID
63+
* @param {Object} [options] - Polling options
64+
* @param {number} [options.intervalMs=5000] - Polling interval in ms
65+
* @param {number} [options.timeoutMs=300000] - Timeout in ms (default: 5 minutes)
66+
* @param {Function} [options.onStatusChange] - Callback for status changes
67+
* @returns {Promise<Object>} Final account status when onboarding complete
68+
*
69+
* @example
70+
* const accountStatus = await waitForStripeOnboarding(client, 'business-id', {
71+
* onStatusChange: (status) => {
72+
* console.log(`Onboarding status: ${JSON.stringify(status)}`);
73+
* }
74+
* });
75+
*/
76+
export async function waitForStripeOnboarding(client, businessId, options = {}) {
77+
const {
78+
intervalMs = 5000,
79+
timeoutMs = 300000, // 5 minutes
80+
onStatusChange,
81+
} = options;
82+
83+
const startTime = Date.now();
84+
85+
while (Date.now() - startTime < timeoutMs) {
86+
const status = await client.getStripeAccountStatus(businessId);
87+
88+
if (onStatusChange) {
89+
onStatusChange(status);
90+
}
91+
92+
if (status.onboarding_complete) {
93+
return status;
94+
}
95+
96+
// Wait before next poll
97+
await new Promise(resolve => setTimeout(resolve, intervalMs));
98+
}
99+
100+
throw new Error(`Stripe onboarding timeout after ${timeoutMs}ms`);
101+
}
102+
103+
/**
104+
* Create payment with automatic merchant onboarding check
105+
*
106+
* Checks if merchant has completed Stripe onboarding first, and provides
107+
* helpful error messages if not. Prevents failed payment attempts.
108+
*
109+
* @param {import('./client.js').CoinPayClient} client - CoinPay client instance
110+
* @param {Object} params - Payment parameters (same as createCardPayment)
111+
* @returns {Promise<Object>} Payment session or onboarding info if incomplete
112+
*
113+
* @example
114+
* const result = await createCardPaymentWithOnboardingCheck(client, {
115+
* businessId: 'business-id',
116+
* amount: 5000,
117+
* description: 'Order #123'
118+
* });
119+
*
120+
* if (result.requires_onboarding) {
121+
* // Redirect merchant to result.onboarding_url
122+
* } else {
123+
* // Redirect customer to result.checkout_url
124+
* }
125+
*/
126+
export async function createCardPaymentWithOnboardingCheck(client, params) {
127+
try {
128+
// Check onboarding status first
129+
const status = await client.getStripeAccountStatus(params.businessId);
130+
131+
if (!status.onboarding_complete) {
132+
// Generate onboarding link if needed
133+
const onboarding = await client.createStripeOnboardingLink(params.businessId);
134+
135+
return {
136+
requires_onboarding: true,
137+
onboarding_url: onboarding.onboarding_url,
138+
status: status,
139+
message: 'Merchant must complete Stripe onboarding before accepting card payments',
140+
};
141+
}
142+
143+
// Onboarding complete, create payment
144+
const payment = await client.createCardPayment(params);
145+
return {
146+
requires_onboarding: false,
147+
...payment,
148+
};
149+
150+
} catch (error) {
151+
if (error.status === 404) {
152+
// No Stripe account exists, need onboarding
153+
const onboarding = await client.createStripeOnboardingLink(params.businessId);
154+
155+
return {
156+
requires_onboarding: true,
157+
onboarding_url: onboarding.onboarding_url,
158+
status: null,
159+
message: 'Merchant needs to complete Stripe onboarding',
160+
};
161+
}
162+
throw error;
163+
}
164+
}
165+
166+
/**
167+
* Get payment method support status
168+
*
169+
* Returns which payment methods are available for a merchant.
170+
* Helps with conditional UI rendering.
171+
*
172+
* @param {import('./client.js').CoinPayClient} client - CoinPay client instance
173+
* @param {string} businessId - Business ID
174+
* @returns {Promise<Object>} Available payment methods
175+
*
176+
* @example
177+
* const support = await getPaymentMethodSupport(client, 'business-id');
178+
* console.log(support);
179+
* // {
180+
* // crypto: true,
181+
* // cards: true,
182+
* // escrow: true,
183+
* // stripe_onboarding_complete: true
184+
* // }
185+
*/
186+
export async function getPaymentMethodSupport(client, businessId) {
187+
try {
188+
// Check if business exists (crypto payments always work)
189+
await client.getBusiness(businessId);
190+
191+
let cardSupport = false;
192+
let stripeOnboardingComplete = false;
193+
194+
// Check Stripe status
195+
try {
196+
const stripeStatus = await client.getStripeAccountStatus(businessId);
197+
cardSupport = stripeStatus.onboarding_complete;
198+
stripeOnboardingComplete = stripeStatus.onboarding_complete;
199+
} catch (error) {
200+
// No Stripe account = no card support yet
201+
cardSupport = false;
202+
stripeOnboardingComplete = false;
203+
}
204+
205+
return {
206+
crypto: true, // Always available
207+
cards: cardSupport,
208+
escrow: cardSupport, // Card escrow requires Stripe
209+
stripe_onboarding_complete: stripeOnboardingComplete,
210+
};
211+
212+
} catch (error) {
213+
if (error.status === 404) {
214+
return {
215+
crypto: false,
216+
cards: false,
217+
escrow: false,
218+
stripe_onboarding_complete: false,
219+
error: 'Business not found',
220+
};
221+
}
222+
throw error;
223+
}
224+
}
225+
226+
/**
227+
* Format amount for display
228+
*
229+
* Converts cents to dollar amount with proper formatting.
230+
*
231+
* @param {number} amountCents - Amount in cents
232+
* @param {string} [currency='USD'] - Currency code
233+
* @returns {string} Formatted amount string
234+
*
235+
* @example
236+
* formatCardAmount(5000); // "$50.00"
237+
* formatCardAmount(5050); // "$50.50"
238+
* formatCardAmount(500, 'EUR'); // "€5.00"
239+
*/
240+
export function formatCardAmount(amountCents, currency = 'USD') {
241+
const amount = amountCents / 100;
242+
243+
const formatters = {
244+
USD: (amt) => `$${amt.toFixed(2)}`,
245+
EUR: (amt) => `€${amt.toFixed(2)}`,
246+
GBP: (amt) => ${amt.toFixed(2)}`,
247+
CAD: (amt) => `C$${amt.toFixed(2)}`,
248+
};
249+
250+
const formatter = formatters[currency.toUpperCase()];
251+
if (formatter) {
252+
return formatter(amount);
253+
}
254+
255+
// Fallback for unknown currencies
256+
return `${amount.toFixed(2)} ${currency.toUpperCase()}`;
257+
}
258+
259+
/**
260+
* Calculate platform fees for card payments
261+
*
262+
* @param {number} amountCents - Payment amount in cents
263+
* @param {string} tier - Merchant tier ('free' or 'pro')
264+
* @returns {Object} Fee breakdown
265+
*
266+
* @example
267+
* const fees = calculateCardPaymentFees(5000, 'free');
268+
* console.log(fees);
269+
* // {
270+
* // amount: 5000,
271+
* // platformFee: 50,
272+
* // platformFeePercent: 1,
273+
* // merchantReceives: 4950 // before Stripe fees
274+
* // }
275+
*/
276+
export function calculateCardPaymentFees(amountCents, tier = 'free') {
277+
const platformFeePercent = tier === 'pro' ? 0.5 : 1.0; // 0.5% or 1%
278+
const platformFeeCents = Math.round(amountCents * (platformFeePercent / 100));
279+
280+
return {
281+
amount: amountCents,
282+
platformFee: platformFeeCents,
283+
platformFeePercent,
284+
merchantReceives: amountCents - platformFeeCents, // Before Stripe processing fees
285+
};
286+
}
287+
288+
/**
289+
* Default export with all convenience functions
290+
*/
291+
export default {
292+
createQuickCardPayment,
293+
waitForStripeOnboarding,
294+
createCardPaymentWithOnboardingCheck,
295+
getPaymentMethodSupport,
296+
formatCardAmount,
297+
calculateCardPaymentFees,
298+
};

0 commit comments

Comments
 (0)