Payment Intent
Overview
The Payment Intent controller handles one-time payment processing for OneBalance wallet reloads and setup intents for digital wallet subscription payments (Apple Pay, Google Pay). It supports both platform and connected account payments with currency conversion.
Source File: internal/api/v1/store/Controllers/payment-intent.js (121 lines)
Key Capabilities
- OneBalance Reloads: Manual wallet balance top-ups with payment confirmation
- Currency Conversion: Automatic conversion to base currency for balance tracking
- Digital Wallet Setup: Setup intents for saving payment methods for recurring charges
- Multi-Tenant Support: Platform vs connected account payment handling
- Payment Confirmation: Auto-confirm payment intents for immediate processing
🗄️ MongoDB Collections
| Collection | Operations | Purpose |
|---|---|---|
onebalance | Update | Increment balance after successful payment |
onebalance.usage_logs | Create | Log refill event for audit trail |
stripe.keys | Query | Load connected account Stripe credentials |
🎯 Core Methods
1. payment() - OneBalance Reload
Purpose: Process one-time payment to reload account OneBalance wallet.
Business Logic
flowchart TD
A[Payment Request] --> B{Main Account?}
B -->|Yes| C[Use Platform Stripe Key]
B -->|No| D[Load Parent Stripe Key]
D --> E{Parent Key Exists?}
E -->|No| F[Error: Parent Key Missing]
E -->|Yes| G[Calculate Total Amount × 100]
C --> G
G --> H{Payment Source Provided?}
H -->|No| I[Retrieve Customer Default Source]
H -->|Yes| J[Use Provided Source]
I --> K{Has Default Source?}
K -->|No| L[Error: Payment Source Required]
K -->|Yes| M[Create PaymentIntent]
J --> M
M --> N[Confirm Payment]
N --> O{Update OneBalance?}
O -->|Yes| P[Convert Currency to BASE_CURRENCY]
O -->|No| Q[Return Success]
P --> R[Update Balance]
R --> S[Create Usage Log]
S --> Q
Key Operations
1. Stripe Instance Creation
let stripe;
if (account.main) {
stripe = Stripe(STRIPE_KEY);
} else {
const stripe_keys = await StripeKey.findOne({
account_id: new mongoose.Types.ObjectId(
req.auth.account.parent_account?.id || req.auth.account.parent_account,
),
});
const key = stripe_keys?._doc?.token?.access_token;
if (!key) {
throw notFound('Parent account stripe key does not exist.');
}
stripe = Stripe(key);
}
2. Payment Source Resolution
const total_amount = amount * 100; // Convert to cents
const stripe_customer = account.stripe_customer;
if (!payment_source) {
const customer = await stripe.customers.retrieve(stripe_customer);
if (customer.default_source) {
payment_source = customer.default_source;
} else {
throw badRequest('Payment source is not provided.');
}
}
3. Payment Intent Creation & Confirmation
await stripe.paymentIntents.create({
amount: total_amount,
currency: currency,
payment_method: payment_source,
description: 'OneBalance reload (manual)',
customer: stripe_customer,
confirm: true, // Auto-confirm immediately
});
4. Currency Conversion & Balance Update
if (onebalance !== false) {
const currUtil = new CurrencyUtil();
// Convert payment currency to base currency
const currData = await currUtil.convert(
account.id || account._id,
total_amount,
currency?.toUpperCase() || BASE_CURRENCY,
BASE_CURRENCY,
);
// Update balance
await Onebalance.updateOne(
{ account_id: account.id },
{
$inc: { balance: currData.amount },
$set: {
lock: false,
balance_reload_queue_in_progress: false,
retry: false,
retries: [],
},
},
);
// Log refill event
await new OnebalanceLogs({
account_id: account.id,
event: 'refill',
cost: currData.amount,
status: 'success',
type: 'credit',
}).save();
}
Request Parameters
{
"amount": 100,
"currency": "USD",
"payment_source": "pm_1234567890", // Optional
"onebalance": true // Optional, defaults to true
}
Response
{
"success": true,
"message": "Balance updated"
}
Important Notes
- Amount in Dollars: API accepts dollars, converts to cents internally (
amount * 100) - Currency Conversion: Automatically converts to
BASE_CURRENCYfor balance tracking - Auto-Confirm: Payment intents confirmed immediately (no 3D Secure support)
- Default Source Fallback: Uses customer's default payment method if not provided
- Retry State Cleanup: Clears any previous retry flags on successful reload
- Optional Balance Update: Can skip OneBalance update with
onebalance=falsequery param
2. createDigitalWalletSetup() - Setup Intent for Digital Wallets
Purpose: Create setup intent for saving digital wallet payment methods (Apple Pay, Google Pay) for future subscription charges.
Business Logic
flowchart TD
A[Setup Request] --> B{Main Account?}
B -->|Yes| C[Use Platform Stripe Key]
B -->|No| D[Load Parent Stripe Key]
D --> E{Parent Key Exists?}
E -->|No| F[Error: Parent Key Missing]
E -->|Yes| G{stripe_customer Exists?}
C --> G
G -->|No| H[Error: Customer Not Found]
G -->|Yes| I[Create SetupIntent]
I --> J[Enable Automatic Payment Methods]
J --> K[Return Client Secret]
Key Operations
1. Setup Intent Creation
let customerId = account.stripe_customer;
if (!customerId) {
throw notFound('Stripe Customer not found.');
}
const setupIntentParams = {
customer: customerId,
usage: 'off_session', // Required for recurring billing
automatic_payment_methods: { enabled: true },
};
const setupIntent = await stripe.setupIntents.create(setupIntentParams);
2. Client Secret Response
res.json({
clientSecret: setupIntent.client_secret,
success: true,
message: 'Setup intent created successfully',
});
Response
{
"clientSecret": "seti_1ABC...secret_XYZ",
"success": true,
"message": "Setup intent created successfully"
}
Important Notes
- Off-Session Usage: Setup intents configured for
off_sessionto support recurring billing - Automatic Payment Methods: Enables Apple Pay, Google Pay, etc. automatically
- Client-Side Confirmation: Frontend uses
client_secretwith Stripe.js to complete setup - Subscription-Ready: Saved payment methods can be used for future subscription charges
- No Immediate Charge: Setup intents don't charge the customer, only save the payment method
🔐 Configuration
Environment Variables
| Variable | Description | Required |
|---|---|---|
STRIPE_SECRET_KEY | Platform Stripe secret key | ✅ |
BASE_CURRENCY | Base currency for OneBalance (e.g., USD) | ✅ |
Dependencies
- CurrencyUtil:
utilities/currency.jsfor currency conversion - StripeKey Model: Connected account Stripe credentials
- Onebalance Models: Balance and usage log tracking
⚠️ Important Notes
Critical Business Rules
- Stripe Customer Required: All operations require
stripe_customeron account - Connected Account Keys: Sub-accounts use parent's Stripe access token from
stripe.keys - Currency Conversion: OneBalance always stored in
BASE_CURRENCY - Auto-Confirmation: Payment intents auto-confirmed (no async payment flow)
- Balance Atomicity: Uses
$incfor thread-safe balance updates - Retry Cleanup: Successful reloads clear previous retry state
- Off-Session Setup: Setup intents configured for recurring billing use
Error Messages
| Error | Cause | Resolution |
|---|---|---|
Parent account stripe key does not exist | Missing StripeKey for parent | Configure parent Stripe Connect |
Stripe Customer not found | Account has no stripe_customer | Create Stripe customer first |
Payment source is not provided | No source and no default | Add payment method or provide source |
Customer stripe is not connected | Missing stripe_customer | Link Stripe customer to account |
OneBalance Integration
Balance Update Flow:
- Payment intent confirmed
- Amount converted to BASE_CURRENCY
- Balance incremented atomically
- Usage log created for audit
- Retry flags cleared
Usage Log Entry:
{
account_id: account.id,
event: 'refill',
cost: currData.amount, // In BASE_CURRENCY
status: 'success',
type: 'credit'
}
Digital Wallet Setup Flow
Frontend Integration:
- Backend creates setup intent
- Frontend receives
clientSecret - User completes setup with Stripe.js
- Payment method saved to customer
- Can be used for subscriptions
Subscription Usage:
// Later, when creating subscription
await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
default_payment_method: 'pm_...', // From setup intent
off_session: true,
});
🔗 Related Documentation
- Payment Methods - Payment method CRUD
- Customer Management - Stripe customer operations
- Charge - Alternative charge methods
- Subscriptions - Recurring payment setup
Last Updated: October 8, 2025
Status: Production-Ready ✅