Invoice Management
Source: internal/api/v1/store/Controllers/invoice.js
Overview
The Invoice controller manages Stripe invoice operations with full CRUD capabilities, invoice actions (void, finalize, pay, send), and PDF retrieval. It handles multi-tenant invoice management with platform and connected account support.
Key Capabilities
- Invoice Creation: Create draft invoices with line items
- Invoice CRUD: List, retrieve, update, delete invoices
- Invoice Actions: Void, finalize, pay, send, mark uncollectible
- Invoice Retrieval: Get invoice details and PDF links
- Customer Tracking: Auto-set
became_customer_onflag - Multi-Tenant Support: Platform vs connected account invoices
MongoDB Collections
| Collection | Operations | Purpose |
|---|---|---|
_store.invoices | CRUD | Invoice storage and tracking |
_accounts | Read, Update | Customer account verification, flags |
stripe.keys | Read | Connected account Stripe key resolution |
Service Methods
newInvoice
Creates a new draft invoice with invoice items for specified prices.
Endpoint: POST /store/invoices
Request Body:
{
account: string, // Optional: buyer account ObjectId
prices: [{
price: string, // Stripe price_id
generate_order: boolean // NOT IMPLEMENTED (commented out)
}]
}
Response: Created invoice object from Stripe
Business Logic:
-
Resolve Customer & Stripe Account:
- If
accountprovided: Verify parent_account relationship - Create Stripe customer if not exists for sub-account
- Resolve connected account Stripe keys
- If
-
Create Invoice Items:
- Loop through
pricesarray - Create
stripe.invoiceItems.create()for each price - Store invoice item IDs for cleanup on failure
- Loop through
-
Create Invoice:
stripe.invoices.create({ customer, metadata })- Metadata includes
account_idfor tracking
-
Cleanup on Failure:
- If any step fails, delete created invoice items
- Call
cleanupInvoiceItems()helper
Edge Cases:
- Creates Stripe customer on-the-fly if missing for sub-account
- SMS notification sent if database save fails
- Order generation logic commented out (not currently implemented)
- Application fee calculation logic commented out
getInvoices
Lists invoices with pagination and filtering.
Endpoint: GET /store/invoices
Query Parameters:
{
page: number,
limit: number,
billing: boolean, // NOT USED
account: string, // Filter by account_id
status: string, // Filter by invoice status
subscription: string // Filter by subscription_id
}
Response: Paginated invoice list
Filtering Logic:
// Viewing own invoices as main account
if (account === self && main) {
options.connected_account = 'platform';
options.customer = stripe_customer;
}
// Viewing own invoices as sub-account
if (account === self && !main) {
options.connected_account = parent_stripe_connected_account;
options.customer = stripe_customer;
}
// Viewing client invoices as agency
if (account !== self && main) {
options.connected_account = payments_enabled ? stripe_connected_account : 'platform';
if (!account) {
options['metadata.account_id'] = { $ne: req.auth.account_id };
}
}
Multi-Tenant Rules:
- Main accounts can view their own platform invoices
- Main accounts can view client invoices from their connected account
- Sub-accounts can only view their own invoices
- Cannot view customer-filtered invoices when requesting client list
getInvoice
Retrieves a single invoice by MongoDB _id.
Endpoint: GET /store/invoices/:id
Authorization: Uses generateInvoiceLookupOptions() utility
Response: Single invoice object
updateInvoice
Updates an existing invoice in Stripe and MongoDB.
Endpoint: PATCH /store/invoices/:id
Request Body: Any Stripe-supported invoice update fields
Authorization:
- Customer matches authenticated account's customer
- OR connected_account matches authenticated account's connected_account
Business Logic:
- Find invoice in MongoDB with access check
- Update in Stripe:
stripe.invoices.update(stripe_id, body) - Convert
metadata.account_idto ObjectId - Update in MongoDB with Stripe response
- SMS notification on database save failure
performInvoiceAction
Executes Stripe invoice actions (void, finalize, pay, send, uncollectible).
Endpoint: POST /store/invoices/:id/actions/:action
Supported Actions:
void: Cancel an invoiceuncollectible: Mark as uncollectiblefinalize: Finalize draft invoicepay: Attempt payment with optional body paramssend: Email invoice to customer
Request Body (for finalize/pay): Optional Stripe action parameters
Response: Updated invoice object
Special Logic for pay Action:
if (action === 'pay' && apiOptions.stripeAccount) {
// Check if first payment for customer
const existingPaidInvoice = await Invoice.findOne({
customer: dbInvoice.customer,
connected_account: apiOptions.stripeAccount,
_id: { $ne: dbInvoice.id },
paid: true,
});
if (!existingPaidInvoice) {
// Set became_customer_on flag
await Account.findOneAndUpdate(
{
parent_account: req.auth.default_config.id,
stripe_customer: dbInvoice.customer,
},
{ became_customer_on: new Date() },
);
}
}
Customer Tracking: First paid invoice sets became_customer_on for account analytics
deleteInvoice
Deletes a draft invoice from Stripe and MongoDB.
Endpoint: DELETE /store/invoices/:id
Validation: Invoice must have status: 'draft'
Response:
{
success: true,
message: "SUCCESS",
data: {
id: "mongodb_invoice_id",
deleted: true
}
}
Business Logic:
- Verify invoice access
- Check status is 'draft'
- Delete from Stripe:
stripe.invoices.del() - Delete from MongoDB:
Invoice.deleteOne() - SMS notification on database delete failure
retriveInvoice
Fetches fresh invoice data from Stripe API.
Endpoint: GET /store/invoices/:id/retrieve
Purpose: Get real-time invoice data directly from Stripe (bypasses MongoDB cache)
Response: Live Stripe invoice object
Error Handling:
- Invoice not found in MongoDB:
404 NOT_FOUND - Stripe API error:
400 BAD_REQUESTwith Stripe error message
retriveInvoiceLink
Redirects to invoice PDF hosted on Stripe.
Endpoint: GET /store/invoices/:id/pdf
Response: 302 Redirect to invoice_pdf URL from Stripe
Business Logic:
- Find invoice by MongoDB ID
- Retrieve from Stripe:
stripe.invoices.retrieve(stripe_id) - Redirect to
stripe_invoice.invoice_pdf
Use Case: Direct PDF download links for customer portals
Helper Functions
cleanupInvoiceItems
Cleans up invoice items if invoice creation fails.
cleanupInvoiceItems(customer, apiOptions, invoiceItems, next, error);
Purpose: Delete all created invoice items when invoice creation fails
Parameters:
customer: Stripe customer IDapiOptions: { stripeAccount } for connected accountsinvoiceItems: Array of created invoice item objectsnext: Express next() functionerror: Original error to pass to next()
Returns: Calls next(error) after cleanup
Business Logic Flows
Invoice Creation Flow
graph TD
A[POST /store/invoices] --> B{Account Provided?}
B -->|Yes| C[Verify Account Access]
B -->|No| D[Use Auth Account]
C --> E{Stripe Customer Exists?}
E -->|No| F[Create Stripe Customer]
E -->|Yes| G[Resolve Stripe Keys]
F --> G
D --> G
G --> H[Create Invoice Items]
H --> I{All Items Created?}
I -->|No| J[Cleanup Items]
I -->|Yes| K[Create Invoice]
K --> L{Invoice Created?}
L -->|No| J
L -->|Yes| M[Return Invoice]
J --> N[Return Error]
Invoice Action Flow (pay)
graph TD
A[POST /invoices/:id/actions/pay] --> B[Find Invoice]
B --> C{Access Authorized?}
C -->|No| D[Return 403]
C -->|Yes| E[Execute stripe.invoices.pay]
E --> F{Payment Successful?}
F -->|No| G[Return Error]
F -->|Yes| H{Connected Account?}
H -->|No| I[Update MongoDB]
H -->|Yes| J[Check First Payment]
J --> K{First Paid Invoice?}
K -->|Yes| L[Set became_customer_on]
K -->|No| I
L --> I
I --> M[Return Updated Invoice]
Edge Cases & Business Rules
1. Multi-Tenant Invoice Access
Platform Invoices (connected_account = 'platform'):
- Main account purchasing from DashClicks
- Customer is main account's Stripe customer
- No connected account fees
Connected Account Invoices:
- Agency selling to sub-accounts
- Customer is sub-account's Stripe customer
- Application fees apply (commented out logic)
2. Customer Creation
Automatically creates Stripe customer for sub-accounts on first invoice:
if (!accountVerify.stripe_customer) {
const cus = await stripe.customers.create(
{
name: accountVerify.name,
email: accountVerify.email,
description: accountVerify.name,
},
apiOptions,
);
await Account.findOneAndUpdate({ _id: accountVerify.id }, { stripe_customer: cus.id });
}
3. Draft-Only Deletion
Cannot delete finalized invoices:
if (dbInvoice.status !== 'draft') {
throw new Error('OPERATION_NOT_PERMITTED');
}
Rationale: Finalized invoices are legal documents and must be voided instead
4. First Payment Tracking
The became_customer_on flag is set when:
- Invoice action is
pay - Invoice is from connected account
- No other paid invoices exist for this customer
Query:
const op = {
customer: dbInvoice.customer,
connected_account: apiOptions.stripeAccount,
_id: { $ne: dbInvoice.id },
paid: true,
};
const invCheck = await Invoice.findOne(op);
if (!invCheck) {
acc_update.became_customer_on = new Date();
}
5. Metadata Conversion
MongoDB requires ObjectId type for account references:
if (invoice.metadata.account_id) {
invoice.metadata.account_id = new mongoose.Types.ObjectId(invoice.metadata.account_id);
}
6. Error Notifications
SMS alerts sent to admins when database operations fail:
await sendSMS({
text: `Failed to update invoice in database.\n\nInvoice ID: ${invoice.id}${
apiOptions.stripeAccount ? `\n\nConnected Account: ${apiOptions.stripeAccount}` : ''
}`,
}).catch();
7. Invoice Item Cleanup
If invoice creation fails after items are created, all items are deleted:
for (ii of invoiceItems) {
if (apiOptions.stripeAccount) {
await stripe.invoiceItems.del(ii.id, apiOptions);
} else {
await stripe.invoiceItems.del(ii.id);
}
}
Authorization Patterns
generateInvoiceLookupOptions Utility
Used by get/update/delete/action methods to ensure proper access control:
const options = { null: true }; // Safeguard
await generateInvoiceLookupOptions(req, options, req.params.id || req.body.invoice);
// Utility sets options based on:
// - If viewing own invoices
// - If main or sub-account
// - Payments enabled status
// - Connected account configuration
Prevents:
- Sub-accounts viewing other sub-accounts' invoices
- Agencies viewing platform invoices
- Unauthorized cross-account access
Important Notes
Commented-Out Features
Several features are commented out in the code:
- Order Generation:
generate_orderflag in prices array - Application Fee Calculation: Automatic fee calculation for connected accounts
- Expanded Retrievals: Expanding customer/subscription/charge/default_source
Reason: Likely moved to webhook synchronization for consistency
Stripe Account Resolution
Connected accounts require Stripe key lookup:
const stripe_keys = await StripeKey.findOne({
account_id: req.auth.account.parent_account,
});
const key = stripe_keys?.token?.access_token;
stripe = Stripe(key);
Invoice Status Lifecycle
draft → open → paid
↓
void / uncollectible
- draft: Can be edited or deleted
- open: Finalized, awaiting payment
- paid: Payment successful
- void: Cancelled
- uncollectible: Marked as bad debt
Related Documentation
- Order Management - Order creation from invoices
- Webhooks - Invoice update synchronization
- Subscriptions - Recurring invoice generation
- Reporting - Invoice-based revenue analytics
- Payment Methods - Payment source management