Skip to main content

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

CollectionOperationsPurpose
onebalanceUpdateIncrement balance after successful payment
onebalance.usage_logsCreateLog refill event for audit trail
stripe.keysQueryLoad 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_CURRENCY for 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=false query 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_session to support recurring billing
  • Automatic Payment Methods: Enables Apple Pay, Google Pay, etc. automatically
  • Client-Side Confirmation: Frontend uses client_secret with 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

VariableDescriptionRequired
STRIPE_SECRET_KEYPlatform Stripe secret key
BASE_CURRENCYBase currency for OneBalance (e.g., USD)

Dependencies

  • CurrencyUtil: utilities/currency.js for currency conversion
  • StripeKey Model: Connected account Stripe credentials
  • Onebalance Models: Balance and usage log tracking

⚠️ Important Notes

Critical Business Rules

  1. Stripe Customer Required: All operations require stripe_customer on account
  2. Connected Account Keys: Sub-accounts use parent's Stripe access token from stripe.keys
  3. Currency Conversion: OneBalance always stored in BASE_CURRENCY
  4. Auto-Confirmation: Payment intents auto-confirmed (no async payment flow)
  5. Balance Atomicity: Uses $inc for thread-safe balance updates
  6. Retry Cleanup: Successful reloads clear previous retry state
  7. Off-Session Setup: Setup intents configured for recurring billing use

Error Messages

ErrorCauseResolution
Parent account stripe key does not existMissing StripeKey for parentConfigure parent Stripe Connect
Stripe Customer not foundAccount has no stripe_customerCreate Stripe customer first
Payment source is not providedNo source and no defaultAdd payment method or provide source
Customer stripe is not connectedMissing stripe_customerLink Stripe customer to account

OneBalance Integration

Balance Update Flow:

  1. Payment intent confirmed
  2. Amount converted to BASE_CURRENCY
  3. Balance incremented atomically
  4. Usage log created for audit
  5. 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:

  1. Backend creates setup intent
  2. Frontend receives clientSecret
  3. User completes setup with Stripe.js
  4. Payment method saved to customer
  5. 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,
});


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