Payment Methods
Overview
The Payment Methods controller manages payment method CRUD operations for both platform accounts and connected accounts (white-label stores). It handles card creation, updates, retrieval, deletion with built-in duplicate detection and subscription protection.
Source File: internal/api/v1/store/Controllers/paymentMethods.js (422 lines)
Key Capabilities
- Card Management: Add, update, retrieve, delete payment methods
- Duplicate Detection: Fingerprint-based card duplicate prevention
- Default Source Management: Set and update default payment method
- Subscription Protection: Prevent deletion of last card with active subscriptions
- Multi-Tenant Support: Platform vs connected account handling
- Payment Method Types: Support for both sources (
src_*) and payment methods (pm_*)
🗄️ MongoDB Collections
Read Operations
| Collection | Purpose |
|---|---|
_accounts | Verify stripe_customer existence |
stripe.keys | Load connected account Stripe credentials |
Note: This controller primarily interacts with Stripe API, not MongoDB directly.
🎯 Core Methods
1. newMethod() - Add Payment Method
Purpose: Create new payment method from token with duplicate detection and auto-default assignment.
Business Logic
flowchart TD
A[Receive Token] --> B{stripe_customer Exists?}
B -->|No| C[Error: STRIPE_CUSTOMER_NOT_FOUND]
B -->|Yes| D[Retrieve Token Details]
D --> E[Extract Card Fingerprint]
E --> F{Main Account?}
F -->|Yes| G[List Platform Payment Methods]
F -->|No| H[List Connected Account Methods]
G --> I{Duplicate Fingerprint?}
H --> I
I -->|Yes| J[Error: CARD ALREADY EXIST]
I -->|No| K[Create Payment Source]
K --> L[Set as Default Source]
L --> M[Return New Method]
Key Operations
1. Duplicate Detection
const requestTokenDetails = await stripe.tokens.retrieve(token);
const fingerprint = requestTokenDetails?.card?.fingerprint;
const stripeMethods = await stripe.paymentMethods.list({
customer: req.auth.account.stripe_customer,
type: 'card',
});
const isDuplicate = ((stripeMethods && stripeMethods.data) || []).find(
c => c?.card?.fingerprint === fingerprint,
);
if (isDuplicate) {
return next(new Error('CARD ALREADY EXIST'));
}
2. Source Creation (Platform)
if (req.auth.account.main) {
newMethod = await stripe.customers.createSource(req.auth.account.stripe_customer, {
source: token,
});
// Set as default
await stripe.customers.update(req.auth.account.stripe_customer, {
default_source: newMethod.id,
invoice_settings: {
default_payment_method: '',
},
});
}
3. Source Creation (Connected Account)
else {
if (!req.auth.default_config.stripe_connected_account) {
return next(new Error('STORE MISCONFIGURED'));
}
newMethod = await stripe.customers.createSource(
req.auth.account.stripe_customer,
{ source: token },
{
stripeAccount: req.auth.default_config.stripe_connected_account
}
);
await stripe.customers.update(
req.auth.account.stripe_customer,
{
default_source: newMethod.id
},
{
stripeAccount: req.auth.default_config.stripe_connected_account
}
);
}
Important Notes
- Auto-Default: New cards automatically set as default payment method
- Fingerprint Matching: Uses Stripe's card fingerprint for duplicate detection
- Connected Account Check: Sub-accounts require parent's
stripe_connected_account - Error Handling: Logs but doesn't fail on default assignment errors
2. updateDefaultSource() - Change Default Payment Method
Purpose: Update the default payment source for a customer's future charges.
Key Operations
1. Payment Method Type Detection
const updateCustomerParams = {
default_source: card_id,
invoice_settings: {
default_payment_method: '',
},
};
// Handle new-style PaymentMethods (pm_*) vs old-style Sources (src_*)
if (card_id.startsWith('pm_')) {
delete updateCustomerParams.default_source;
updateCustomerParams.invoice_settings = {
default_payment_method: card_id,
};
}
2. Connected Account Handling
let stripeOptions = {};
if (!req.auth.account.main) {
stripeOptions.stripeAccount = req.auth.default_config.stripe_connected_account;
if (!stripeOptions.stripeAccount) {
return next(new Error('STORE MISCONFIGURED'));
}
}
if (stripeOptions.stripeAccount) {
await stripe.customers.update(req.auth.account.stripe_customer, {
...updateCustomerParams,
stripeOptions,
});
} else {
await stripe.customers.update(req.auth.account.stripe_customer, updateCustomerParams);
}
Important Notes
- PaymentMethod vs Source: Handles both
pm_*(PaymentMethods) andsrc_*/card_*(Sources) - Invoice Settings: Clears
default_payment_methodwhen using sources - Error Logging: Logs failures but returns error to client
3. updateMethod() - Update Payment Method Details
Purpose: Update payment method metadata (e.g., billing address, cardholder name).
const paymentMethodId = req.params.id;
const paymentMethodDetails = req.body;
if (account.main) {
paymentMethodData = await stripe.paymentMethods.update(paymentMethodId, paymentMethodDetails);
} else {
if (!defaultConfig.stripe_connected_account) {
throw internalError('STORE MISCONFIGURED');
}
paymentMethodData = await stripe.paymentMethods.update(paymentMethodId, paymentMethodDetails, {
stripeAccount: defaultConfig.stripe_connected_account,
});
}
Important Notes
- Metadata Only: Cannot change card number, expiry, or CVC
- Typical Updates: Billing address, cardholder name
- Uses catch-errors: Standardized error handling with
notFound()andinternalError()
4. getMethods() - List Payment Methods
Purpose: Retrieve all payment methods for a customer with default flag.
Business Logic
flowchart TD
A[Start] --> B{stripe_customer Exists?}
B -->|No| C[Error: STRIPE_CUSTOMER_NOT_FOUND]
B -->|Yes| D[Initialize Empty Methods Array]
D --> E{Main Account?}
E -->|Yes| F[List Platform Methods]
E -->|No| G{Connected Account Exists?}
G -->|No| H[Error: STORE MISCONFIGURED]
G -->|Yes| I[List Connected Account Methods]
F --> J[Paginate with limit=100]
I --> J
J --> K[Find Default Method]
K --> L{Has More?}
L -->|Yes| M[Get Next Page]
M --> J
L -->|No| N[Return All Methods]
Key Operations
1. Pagination with 100-item Limit
let methods = [];
let hasMore = false;
let last;
do {
let stripeMethods;
if (req.auth.account.main) {
stripeMethods = await stripe.paymentMethods.list({
customer: req.auth.account.stripe_customer,
type: 'card',
expand: ['data.customer'],
limit: 100, // Stripe max limit
starting_after: last,
});
} else {
stripeMethods = await stripe.paymentMethods.list(
{
customer: req.auth.account.stripe_customer,
type: 'card',
expand: ['data.customer'],
limit: 100,
},
{
stripeAccount: req.auth.default_config.stripe_connected_account,
},
);
}
if (stripeMethods.data?.length) {
methods = methods.concat(stripeMethods.data);
last = stripeMethods.data[stripeMethods.data.length - 1].id;
}
hasMore = stripeMethods.has_more;
} while (hasMore);
2. Default Method Detection
if (stripeMethods?.data) {
const defaultMethod = stripeMethods.data.find(method => {
const cus = method?.customer;
return (
cus?.invoice_settings?.default_payment_method === method?.id ||
cus?.default_source === method?.id
);
});
if (defaultMethod) {
defaultMethod.default_source = true;
}
}
Important Notes
- Pagination Required: Loops until all pages retrieved (max 100 per page)
- Expanded Customer: Includes customer object for default detection
- Default Flag: Adds
default_source: trueto default method - Multi-Tenant: Automatically scopes to correct Stripe account
5. deleteMethod() - Remove Payment Method
Purpose: Delete payment method with subscription protection.
Business Logic
flowchart TD
A[Delete Request] --> B{stripe_customer Exists?}
B -->|No| C[Error: STRIPE_CUSTOMER_NOT_FOUND]
B -->|Yes| D[Parallel: List Methods & Subscriptions]
D --> E{Only 1 Method?}
E -->|No| F[Delete Payment Method]
E -->|Yes| G{Has Active Subscriptions?}
G -->|No| F
G -->|Yes| H[Error: CANNOT_DELETE_LAST_PAYMENT_METHOD]
F --> I{PaymentMethod or Source?}
I -->|pm_*| J[Detach PaymentMethod]
I -->|Other| K[Delete Source]
J --> L[Return Deleted Card]
K --> L
Key Operations
1. Subscription Protection Check
if (req.auth.account.main) {
const [stripeMethods, subscriptions] = await Promise.all([
stripe.paymentMethods.list({
customer: req.auth.account.stripe_customer,
type: 'card',
}),
stripe.subscriptions.list({
customer: req.auth.account.stripe_customer,
}),
]);
// Prevent deletion of last card with active subscriptions
if (stripeMethods.data.length == 1 && subscriptions.data.length >= 1) {
let error = new Error('CANNOT_DELETE_LAST_PAYMENT_METHOD');
error.additional_info =
'Cannot delete the last payment method on the account while you have an active subscription. ' +
'Please add a new payment method before removing this one';
return next(error);
}
}
2. Payment Method Type Handling
let deletedCard;
// New-style PaymentMethods (pm_*)
if (req.params.id.startsWith('pm_')) {
deletedCard = await stripe.paymentMethods.detach(req.params.id);
}
// Old-style Sources (src_*, card_*)
else {
deletedCard = await stripe.customers.deleteSource(
req.auth.account.stripe_customer,
req.params.id,
);
}
3. Connected Account Deletion
else {
if (!req.auth.default_config.stripe_connected_account) {
return next(new Error('STORE MISCONFIGURED'));
}
const [stripeMethods, subscriptions] = await Promise.all([
stripe.paymentMethods.list(
{
customer: req.auth.account.stripe_customer,
type: 'card'
},
{
stripeAccount: req.auth.default_config.stripe_connected_account
}
),
stripe.subscriptions.list(
{
customer: req.auth.account.stripe_customer
},
{ stripeAccount: req.auth.default_config.stripe_connected_account }
)
]);
// Same protection check...
if (req.params.id.startsWith('pm_')) {
deletedCard = await stripe.paymentMethods.detach(
req.params.id,
{
stripeAccount: req.auth.default_config.stripe_connected_account
}
);
} else {
deletedCard = await stripe.customers.deleteSource(
req.auth.account.stripe_customer,
req.params.id,
{
stripeAccount: req.auth.default_config.stripe_connected_account
}
);
}
}
Important Notes
- Subscription Protection: Cannot delete last payment method with active subscriptions
- User-Friendly Error: Provides clear guidance on adding new method first
- Parallel Queries: Uses
Promise.allfor efficiency - Type Detection: Automatically handles PaymentMethods vs Sources
🔐 Configuration
Environment Variables
| Variable | Description | Required |
|---|---|---|
STRIPE_SECRET_KEY | Platform Stripe secret key | ✅ |
Authentication Requirements
- All Methods: Require authenticated account with
stripe_customer - Connected Accounts: Require parent account with
stripe_connected_account
⚠️ Important Notes
Critical Business Rules
- Stripe Customer Required: All operations require
stripe_customeron account - Auto-Default: New payment methods automatically set as default
- Fingerprint Duplicates: Card fingerprint used for duplicate detection (same card, different tokens)
- Subscription Protection: Last payment method cannot be deleted with active subscriptions
- Connected Account Validation: Sub-accounts must have parent with
stripe_connected_account - Payment Method Types: Supports both PaymentMethods (
pm_*) and Sources (src_*,card_*)
Error Messages
| Error | Cause | Resolution |
|---|---|---|
STRIPE_CUSTOMER_NOT_FOUND | Account has no stripe_customer | Create Stripe customer first |
CARD ALREADY EXIST | Card fingerprint already exists | Use existing card or try different card |
STORE MISCONFIGURED | Missing stripe_connected_account | Configure parent account Stripe Connect |
CANNOT_DELETE_LAST_PAYMENT_METHOD | Trying to delete last card with active subscriptions | Add new payment method before deleting |
Common Patterns
Main Account (Platform):
if (req.auth.account.main) {
// Use platform Stripe credentials
await stripe.paymentMethods.list({
customer: req.auth.account.stripe_customer,
});
}
Sub-Account (Connected):
else {
// Use parent's connected account
await stripe.paymentMethods.list(
{
customer: req.auth.account.stripe_customer
},
{
stripeAccount: req.auth.default_config.stripe_connected_account
}
);
}
🔗 Related Documentation
- Customer Management - Stripe customer operations
- Payment Intent - Payment processing
- Subscriptions - Subscription payment methods
- Charge - Direct charge operations
Last Updated: October 8, 2025
Status: Production-Ready ✅