Customer Operations
π Overviewβ
internal/api/v1/billing/services/customer.service.js is the largest surface in the billing domain. It normalises Stripe customer APIs, manages invoices and subscriptions, links contacts inside DashClicks CRM, and offers helper utilities for exporting notes or connecting queued background jobs. Controllers remain thin wrappers around these functions, deferring every business rule (lookups, validation, queueing) to the service layer.
File Path: internal/api/v1/billing/services/customer.service.js
ποΈ Collections Usedβ
π Full Schema: See Database Collections Documentation
billing.customersβ
- Operations: Read/Write through Stripe webhooks; this service reads via aggregations.
- Model:
shared/models/billing-customer.js - Usage Context: Identifies linked CRM contacts and new customer growth.
billing.notesβ
- Operations: CRUD + CSV export.
- Model:
shared/models/billing-notes.js - Usage Context: Stores account-scoped billing notes with author metadata.
crm.contactsβ
- Operations: Aggregations, updates, linking/unlinking.
- Model:
shared/models/contact.js - Usage Context: Maps Stripe customers to CRM contacts, or creates new contacts when linking is enabled.
queuesβ
- Operations: Inserts queue jobs for asynchronous linking (
linkCustomers). - Model:
shared/models/queues.js - Usage Context: Persists background tasks consumed by Queue Manager for customer/contact linkage.
_store.prices, _store.subscriptions, _store.cartβ
- Operations: Reads to enrich subscription metadata; writes created through charge/payment-intent services.
- Usage Context: Pricing data for invoice creation and subscription displays.
_account, userβ
- Operations: Reads for parent-account context, owner information.
- Usage Context: Required for linking, queue records, and invoice configuration.
π Data Flowβ
flowchart TD
A[HTTP Request] --> B[customer.controller]
B --> C[customer.service]
C -->|Stripe SDK| D[Stripe Connected Account]
C -->|Mongo| E[(MongoDB)]
C -->|Queues| F[Queue Manager]
C -->|CRM| G[crm.contacts]
D --> H[Webhook] --> E
π§ Business Logic & Functionsβ
Customer Portfolioβ
getCustomers({ startDate, endDate, search, limit, lastCustomerId, nextPage, stripeBilling })β
Purpose: Lists Stripe customers for the connected account with optional time-range filtering and search.
Parameters:
startDate/endDate(String) β Optional ISO strings for creation filters (converted to Unix).search(String) β When provided, uses Stripe search (`name~"term" OR email~"term").limit(Number) β Page size.lastCustomerId(String) β Cursor for pagination viastarting_after.nextPage(String) β Stripe search pagination token.stripeBilling(Object) β Connected account tokens + Stripe user ID.
Returns: Promise<{ customers, has_more, next_page, last_customer_id }>
Business Logic Flow:
- Acquire Stripe client via
stripeConfig(stripeBilling). - Choose API path:
- When
searchexists β callstripe.customers.search. - Otherwise β
stripe.customers.listfiltered bycreatedrange and expanded tax IDs.
- When
- Fetch parent account currency defaults and CRM contacts with matching Stripe IDs or historical fields for linking indicators.
- Map each Stripe customer to DTO with:
- Normalised balance to dollars.
- Link metadata (contact name/id, type) when CRM match found.
- Currency fallback to account default when Stripe record lacks one.
- Return pagination metadata and results.
Key Business Rules:
- CRM records are matched by
stripe_customer_idand email to avoid false positives. - Search path bypasses date filters (Stripe limitation); controllers combine filters accordingly.
Error Handling:
- Stripe connection timeouts map to human-readable message via
custom().
Example Usage:
const { customers } = await customerService.getCustomers({
limit: 25,
search: 'Acme',
stripeBilling,
});
Side Effects: None (read-only).
addCustomer({ customerDetails, stripeBilling })β
Purpose: Creates a Stripe customer on behalf of the connected account.
Business Logic Flow:
- Instantiate Stripe client.
- Create customer with provided fields (name, email, phone, address).
- Return Stripe response.
Side Effects: β οΈ External Stripe API call.
getCustomer({ customerId, stripeBilling })β
Purpose: Retrieves a single Stripe customer enriched with CRM and account data.
Key Steps:
- Fetch Stripe customer.
- When
currencymissing, fetch Stripe account defaults. - Locate CRM contact with matching email + Stripe ID (or legacy fields).
- Lookup DashClicks account record that references the Stripe customer.
- Compose DTO including link status, account summary, and normalised balance.
Error Handling: Stripe connection issues surfaced via custom helper.
updateCustomer({ customerId, customerDetails, stripeBilling })β
Purpose: Updates Stripe customer fields.
Side Effects: β οΈ External Stripe API call.
deleteCustomer({ customerId, stripeBilling })β
Purpose: Deletes the Stripe customer (hard delete) for the connected account.
Behaviour:
- Issues
stripe.customers.deland returns response. - Stripe may block deletion when subscriptions exist; the service relays Stripeβs error.
CRM Linkingβ
getContacts({ customerId, accountId })β
Purpose: Lists CRM contacts linked to the billing customer within a DashClicks account.
Business Logic:
- Aggregates
crm.contacts, allowing both person and business relationships. - Normalises result to
{ id, name, phone, email, type }.
linkContact({ customerId, accountId, contactId, stripeConfigs, contactType })β
Purpose: Maps a CRM contact to a Stripe customer using configured field mappings.
Business Logic Flow:
- Guard: require Stripe mappings or explicit
contactType; otherwise throwforbidden. - Validate that contact belongs to the account, matches expected type, and lacks existing Stripe linkage.
- Fetch Stripe customer for reference.
- Compute update object via
getMappinghelper (applies mapping rules to contact data). - Update the contact document, storing both Stripe ID and previous values for rollback.
Error Handling:
- Returns
notFoundif contact invalid;forbiddenif mappings missing.
unlinkContact({ contactId, stripeConfig })β
Purpose: Removes Stripe linkage from a CRM contact, restoring original fields when available.
Steps:
- Validate that mappings exist and contact has
old_field_valuesorstripe_customer_id. - Restore fields captured during linking, unset stored references.
linkCustomers({ linkingDetails, accountId, stripeBilling })β
Purpose: Persists Stripe mapping settings and optionally queues a background job to link contacts in bulk.
Key Steps:
- Normalise mapping keys by replacing dots with dashes (Mongo keys cannot contain
.). - Update connected Stripe token document with mapping configuration.
- When
typeprovided, enqueue a job inqueuesto process linking asynchronously.
Side Effects:
- β οΈ Inserts queue record when triggered.
Subscriptions & Schedulesβ
getSubscriptions({ customerId, startDate, endDate, status, limit, lastSubscriptionId, stripeBilling })β
Purpose: Lists subscriptions for a customer with status filtering and product enrichment.
Business Logic Flow:
- Convert date filters to Unix; parse
statuscomma list intoSetfor quick filtering. - Retrieve subscriptions (
stripe.subscriptions.list) expanding product references. - Memoise product fetches (fallback to
getProduct) to avoid repeated Stripe calls. - Reduce each subscription into DTO containing billing cycle info, nickname, amount, and product metadata.
- Filter results to statuses requested; set pagination flags.
Edge Cases:
- Subscriptions with multiple items sum plan amounts for total price.
getSubscriptionSchedules({ customerId, startDate, endDate, limit, lastSubscriptionId, stripeBilling })β
Purpose: Lists scheduled subscription changes.
Highlights:
- Expands price data for each phase.
- Uses memoised
getProducthelper for display data.
addSubscription({ subscriptionDetails, stripeBilling })β
Purpose: Creates a new subscription and handles invoice delivery based on collection method.
Business Logic:
- Create subscription via
stripe.subscriptions.create. - When
collection_method === 'send_invoice', finalise and send invoice. - Return Stripe response.
Side Effects:
- β οΈ Stripe subscription creation.
- β οΈ Optionally triggers invoice finalisation + email send.
updateSubscription({ subscriptionId, subscriptionDetails, stripeBilling })β
Purpose: Updates existing subscriptions (items, metadata, proration).
deleteSubscription({ subscriptionId, stripeBilling })β
Purpose: Cancels the subscription immediately by calling stripe.subscriptions.del.
addSubscriptionSchedules({ subscriptionSchedulesDetails, stripeBilling })β
Purpose: Creates a subscription schedule with optional delayed start.
Special Handling:
- Converts ISO start dates to Unix unless
'now'.
canceSubscriptionSchedules({ subscriptionScheduleId, stripeBilling })β
Purpose: Cancels an existing subscription schedule.
Invoices & Chargesβ
addInvoice({ invoiceDetails, stripeBilling })β
Purpose: Builds a draft invoice, attaches line items (by price or custom amounts), and dispatches or charges it depending on collection method.
Business Logic Flow:
- Create draft invoice with collection method, due date (relative days) and metadata.
- Iterate
productsarray:- For price references β add invoice item with
price. - For custom items β supply description, quantity, unit amounts.
- For price references β add invoice item with
- Finalise invoice.
- If sending manually (
send_invoice), send via Stripe. - If automatic charge (
charge_automatically), pay immediately.
Edge Cases:
- If any invoice item creation fails, delete the invoice and bubble error to caller.
Side Effects:
- β οΈ Multiple Stripe API calls (invoice + invoice items + optional payment).
getInvoices({ customerId, startDate, endDate, status, limit, lastInvoiceId, search, nextPage, subscription, stripeBilling })β
Purpose: Fetches invoices with optional search and subscription filtering.
Key Behaviours:
- Uses list endpoint (with expand) when not searching; search endpoint when
searchprovided. - Returns totals in dollars and status for UI display.
getInvoice({ invoiceId, stripeBilling })β
Purpose: Retrieves a single invoice with related subscription and transaction data.
Highlights:
- Expands
lines.data.price.productto provide product metadata. - Calculates product total amount.
- Fetches related transactions (other invoices) when subscription present.
payInvoice, voidInvoice, finalizeInvoice, deleteInvoiceβ
- Update invoice state via Stripe APIs (
pay,voidInvoice,finalizeInvoice,del). - Wrap Stripe errors with friendly messages when connection issues occur.
getCharges({ customerId, startDate, endDate, limit, lastChargeId, search, nextPage, stripeBilling })β
Purpose: Lists Stripe charges with optional search.
Behaviour:
- Similar to invoices: list when not searching, search when
searchpresent. - Returns DTO with amount, status, invoice reference, and payment method ID.
getCharge({ chargeId, stripeBilling })β
Purpose: Retrieves detailed charge information with balance transaction and related subscription data.
Highlights:
- Expands invoice and balance transaction.
- If associated with subscription, fetches product metadata via helper.
- Includes related transactions for context.
Payment Methodsβ
getPaymentMethods({ customerId, startDate, endDate, type, limit, firstPaymentMethodId, lastPaymentMethodId, stripeBilling })β
Purpose: Paginates payment methods for a customer, prioritising the default method.
Business Logic Flow:
- Fetch Stripe customer to identify default payment method.
- List payment methods with date filters and pagination cursors.
- Move default payment method to the front of the array for UI display.
addPaymentMethod({ paymentMethodDetails, stripeBilling })β
Purpose: Creates and attaches a new PaymentMethod to the customer.
Special Handling:
- Accepts
customer_stripe_id; attaches payment method immediately after creation.
addPaymentMethodToken({ paymentMethodDetails, stripeBilling })β
Purpose: Supports legacy token-based card addition while preventing duplicates.
Business Logic:
- Retrieve Stripe token to read card fingerprint.
- List existing payment methods and ensure fingerprint not already present.
- Create source, update default payment method if none exists, and apply nickname metadata.
Error Handling:
- Throws
conflictwhen duplicate detected.
updatePaymentMethod, defaultPaymentMethod, deletePaymentMethod, deletePaymentMethodTokenβ
- Update metadata, set default invoice settings, or detach/delete as required.
- Guard against deleting the last card when subscriptions exist.
Activity & Notesβ
getActivity({ customerId, startDate, endDate, limit, lastEventId, stripeBilling })β
Purpose: Lists Stripe events (currently subscription created/deleted) relevant to the customer.
Behaviour:
- Uses
stripe.events.listfiltered by type and related object. - Memoises product names within each event for readability.
getNotes, addNote, updateNote, deleteNoteβ
- CRUD operations on
billing.notes, scoped to account/user and customer ID.
exportNotes({ startDate, endDate, accountId, stripeBilling })β
Purpose: Generates CSV of notes via json2csv.
Behaviour:
- Formats output columns (
Customer Id,Content,Created By, etc.).
Summary Table of Additional Helpersβ
| Function | Purpose |
|---|---|
getContacts | Aggregates CRM contacts linked to the Stripe customer within the same account. |
payInvoice | Immediately charges a finalised invoice. |
voidInvoice | Marks invoices as void. |
finalizeInvoice | Finalises drafts prior to payment or sending. |
deleteInvoice | Deletes invoice drafts when necessary. |
updateNote / deleteNote | Maintains user-authored billing notes. |
updatePaymentMethod | Updates metadata on existing PaymentMethods. |
defaultPaymentMethod | Sets default invoice payment method for the customer. |
deletePaymentMethod | Detaches PaymentMethod, protecting active subscriptions. |
deletePaymentMethodToken | Deletes token-based cards while forbidding removal of the last card. |
π Integration Pointsβ
Internal Dependenciesβ
../utils/stripe-configβ All Stripe calls go through this connected-account aware factory.../utils/add-mappingβ ProvidesgetMappingandgetKeysAndValuesused during CRM link/unlink.shared/utilities/catch-errorsβ Standardised error helpers (custom,notAllowed,conflict, etc.).shared/models/*β Numerous shared models for CRM, queues, account context, and billing notes.
External Servicesβ
- Stripe API β Customers, subscriptions, invoices, payments, events, payment methods, files, payout references.
- DashClicks Router (internal) β
axioscall in payment-link service leverages the same base URL; customer service indirectly supports that flow.
π§ͺ Edge Cases & Special Handlingβ
Duplicate Payment Methodsβ
- Condition: Adding card via token that already exists.
- Handling: Fingerprint comparison with existing PaymentMethods, raising
conflicterror.
Subscription Protection on Card Deleteβ
- Condition: Attempt to delete last card with active subscriptions.
- Handling: Throws
notAllowedguard, instructing user to add replacement first.
Invoice Item Failureβ
- Condition: Creating manual invoice items fails mid-loop.
- Handling: Deletes invoice and rethrows error, ensuring no orphan drafts remain.
Contact Linking without Mappingsβ
- Condition: Attempt to link contacts before Stripe mappings configured.
- Handling: Throws
forbiddenwith explicit message.
β οΈ Important Notesβ
- π¨ Stripe Rate Limits: Functions that fetch subscriptions or invoices can invoke multiple Stripe endpoints (list + product lookups). Batch requests appropriately in controllers or implement caching where possible.
- π‘ Timezone Conversions: Date filters are converted to Unix seconds using
moment; ensure clients send ISO strings for accurate filtering. - π§ͺ Testing Hooks: Fixtures under
tests/fixtures/customer.jscover representative data; extend these when altering return shapes.