Skip to main content

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 via starting_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:

  1. Acquire Stripe client via stripeConfig(stripeBilling).
  2. Choose API path:
    • When search exists β†’ call stripe.customers.search.
    • Otherwise β†’ stripe.customers.list filtered by created range and expanded tax IDs.
  3. Fetch parent account currency defaults and CRM contacts with matching Stripe IDs or historical fields for linking indicators.
  4. 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.
  5. Return pagination metadata and results.

Key Business Rules:

  • CRM records are matched by stripe_customer_id and 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:

  1. Instantiate Stripe client.
  2. Create customer with provided fields (name, email, phone, address).
  3. 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:

  1. Fetch Stripe customer.
  2. When currency missing, fetch Stripe account defaults.
  3. Locate CRM contact with matching email + Stripe ID (or legacy fields).
  4. Lookup DashClicks account record that references the Stripe customer.
  5. 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.del and 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:

  1. Guard: require Stripe mappings or explicit contactType; otherwise throw forbidden.
  2. Validate that contact belongs to the account, matches expected type, and lacks existing Stripe linkage.
  3. Fetch Stripe customer for reference.
  4. Compute update object via getMapping helper (applies mapping rules to contact data).
  5. Update the contact document, storing both Stripe ID and previous values for rollback.

Error Handling:

  • Returns notFound if contact invalid; forbidden if mappings missing.

unlinkContact({ contactId, stripeConfig })​

Purpose: Removes Stripe linkage from a CRM contact, restoring original fields when available.

Steps:

  1. Validate that mappings exist and contact has old_field_values or stripe_customer_id.
  2. 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:

  1. Normalise mapping keys by replacing dots with dashes (Mongo keys cannot contain .).
  2. Update connected Stripe token document with mapping configuration.
  3. When type provided, enqueue a job in queues to 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:

  1. Convert date filters to Unix; parse status comma list into Set for quick filtering.
  2. Retrieve subscriptions (stripe.subscriptions.list) expanding product references.
  3. Memoise product fetches (fallback to getProduct) to avoid repeated Stripe calls.
  4. Reduce each subscription into DTO containing billing cycle info, nickname, amount, and product metadata.
  5. 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 getProduct helper for display data.

addSubscription({ subscriptionDetails, stripeBilling })​

Purpose: Creates a new subscription and handles invoice delivery based on collection method.

Business Logic:

  1. Create subscription via stripe.subscriptions.create.
  2. When collection_method === 'send_invoice', finalise and send invoice.
  3. 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:

  1. Create draft invoice with collection method, due date (relative days) and metadata.
  2. Iterate products array:
    • For price references β†’ add invoice item with price.
    • For custom items β†’ supply description, quantity, unit amounts.
  3. Finalise invoice.
  4. If sending manually (send_invoice), send via Stripe.
  5. 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 search provided.
  • 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.product to 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 search present.
  • 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:

  1. Fetch Stripe customer to identify default payment method.
  2. List payment methods with date filters and pagination cursors.
  3. 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:

  1. Retrieve Stripe token to read card fingerprint.
  2. List existing payment methods and ensure fingerprint not already present.
  3. Create source, update default payment method if none exists, and apply nickname metadata.

Error Handling:

  • Throws conflict when 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.list filtered 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​

FunctionPurpose
getContactsAggregates CRM contacts linked to the Stripe customer within the same account.
payInvoiceImmediately charges a finalised invoice.
voidInvoiceMarks invoices as void.
finalizeInvoiceFinalises drafts prior to payment or sending.
deleteInvoiceDeletes invoice drafts when necessary.
updateNote / deleteNoteMaintains user-authored billing notes.
updatePaymentMethodUpdates metadata on existing PaymentMethods.
defaultPaymentMethodSets default invoice payment method for the customer.
deletePaymentMethodDetaches PaymentMethod, protecting active subscriptions.
deletePaymentMethodTokenDeletes 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 – Provides getMapping and getKeysAndValues used 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) – axios call 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 conflict error.

Subscription Protection on Card Delete​

  • Condition: Attempt to delete last card with active subscriptions.
  • Handling: Throws notAllowed guard, 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 forbidden with 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.js cover representative data; extend these when altering return shapes.
πŸ’¬

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