Skip to main content

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

CollectionPurpose
_accountsVerify stripe_customer existence
stripe.keysLoad 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) and src_*/card_* (Sources)
  • Invoice Settings: Clears default_payment_method when 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() and internalError()

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: true to 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.all for efficiency
  • Type Detection: Automatically handles PaymentMethods vs Sources

🔐 Configuration

Environment Variables

VariableDescriptionRequired
STRIPE_SECRET_KEYPlatform 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

  1. Stripe Customer Required: All operations require stripe_customer on account
  2. Auto-Default: New payment methods automatically set as default
  3. Fingerprint Duplicates: Card fingerprint used for duplicate detection (same card, different tokens)
  4. Subscription Protection: Last payment method cannot be deleted with active subscriptions
  5. Connected Account Validation: Sub-accounts must have parent with stripe_connected_account
  6. Payment Method Types: Supports both PaymentMethods (pm_*) and Sources (src_*, card_*)

Error Messages

ErrorCauseResolution
STRIPE_CUSTOMER_NOT_FOUNDAccount has no stripe_customerCreate Stripe customer first
CARD ALREADY EXISTCard fingerprint already existsUse existing card or try different card
STORE MISCONFIGUREDMissing stripe_connected_accountConfigure parent account Stripe Connect
CANNOT_DELETE_LAST_PAYMENT_METHODTrying to delete last card with active subscriptionsAdd 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
}
);
}


Last Updated: October 8, 2025
Status: Production-Ready ✅

💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM