Skip to main content

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_on flag
  • Multi-Tenant Support: Platform vs connected account invoices

MongoDB Collections

CollectionOperationsPurpose
_store.invoicesCRUDInvoice storage and tracking
_accountsRead, UpdateCustomer account verification, flags
stripe.keysReadConnected 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:

  1. Resolve Customer & Stripe Account:

    • If account provided: Verify parent_account relationship
    • Create Stripe customer if not exists for sub-account
    • Resolve connected account Stripe keys
  2. Create Invoice Items:

    • Loop through prices array
    • Create stripe.invoiceItems.create() for each price
    • Store invoice item IDs for cleanup on failure
  3. Create Invoice:

    • stripe.invoices.create({ customer, metadata })
    • Metadata includes account_id for tracking
  4. 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:

  1. Find invoice in MongoDB with access check
  2. Update in Stripe: stripe.invoices.update(stripe_id, body)
  3. Convert metadata.account_id to ObjectId
  4. Update in MongoDB with Stripe response
  5. 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 invoice
  • uncollectible: Mark as uncollectible
  • finalize: Finalize draft invoice
  • pay: Attempt payment with optional body params
  • send: 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:

  1. Verify invoice access
  2. Check status is 'draft'
  3. Delete from Stripe: stripe.invoices.del()
  4. Delete from MongoDB: Invoice.deleteOne()
  5. 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_REQUEST with Stripe error message

Redirects to invoice PDF hosted on Stripe.

Endpoint: GET /store/invoices/:id/pdf

Response: 302 Redirect to invoice_pdf URL from Stripe

Business Logic:

  1. Find invoice by MongoDB ID
  2. Retrieve from Stripe: stripe.invoices.retrieve(stripe_id)
  3. 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 ID
  • apiOptions: { stripeAccount } for connected accounts
  • invoiceItems: Array of created invoice item objects
  • next: Express next() function
  • error: 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:

  1. Order Generation: generate_order flag in prices array
  2. Application Fee Calculation: Automatic fee calculation for connected accounts
  3. 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

💬

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