Affinity Design
Agency Guide

Client Contracts & Invoicing

Create billing contracts, manage recurring items, and send invoices

Overview

Client contracts define the commercial terms between your agency and each client: what they pay, how often, and how payment is collected. The billing system supports Stripe-managed subscriptions, send-invoice flows, and fully manual invoicing.

Creating a Contract

  1. Open a client record.
  2. Go to the Billing tab.
  3. Create or update the billing contract.

A contract contains:

FieldDescription
Collection modecharge_automatically, send_invoice, or manual_only
Recurring itemsProducts/services billed every cycle
Scheduled additionsOne-time or future-dated line items
DiscountsPercentage or fixed-amount reductions
Billing DayThe anchor day (1–28) monthly items are charged on
Start DateWhen the contract's billing periods begin
Stripe subscriptionsOne entry per active Stripe subscription driving the contract — see below

Recurring Items

Recurring items are the core of the contract — services that renew every billing cycle. Each item references a product from your catalog and carries a quantity and unit price.

Scheduled Additions

Scheduled additions let you plan one-off charges in advance (e.g., a setup fee in month 1, a campaign boost in month 3). They attach to a specific billing cycle and do not renew.

Discounts

Apply discounts at the contract level to reduce the invoice total. Discounts can be a fixed amount or a percentage, and can be set to expire after a number of cycles.

Stripe-Managed Recurring Billing

For charge_automatically and some send_invoice contracts, you can convert the contract to Stripe subscriptions:

  1. On the client contract, use the Convert to Stripe Subscription action.
  2. The system creates Stripe subscriptions bucketed by cadence rather than a single subscription: one subscription covers all monthly recurring items, and each yearly item gets its own subscription (yearly items that share the same anniversary date bundle into one subscription/invoice together).
  3. Each subscription bucket is tracked as its own entry on the contract (subscription ID, schedule ID, its own billing anchor day, and status), so monthly and yearly items can be billed, cancelled, or re-created independently of each other.
  4. Stripe generates invoices automatically each cycle and fires webhook events.
  5. The webhook handler mirrors invoice and charge state into MongoDB and refreshes the lifetime ledger.

charge_automatically contracts rely on Stripe Smart Retries. The local reminder sweep skips these contracts so customers do not receive duplicate dunning messages.

Configure retry behavior in the Stripe Dashboard under Revenue Recovery.

Cancelling a Subscription Group

Because monthly and yearly items bill on separate Stripe subscriptions, you can cancel one cadence group without ending the whole contract:

  1. On the client contract's Services list, open the group header for the monthly bucket or a specific yearly-anniversary bucket.
  2. Use the Cancel action on that group.
  3. Only that group's Stripe subscription is cancelled and its items are deactivated — other active groups on the same contract keep billing normally.

This is useful when a client drops a yearly add-on but keeps their monthly plan, or vice versa.

Billing Timezone & Anchors

Monthly and yearly billing-cycle anchors are computed against your agency's configured billing timezone (see Billing Setup) so a charge lands on the intended calendar day locally, rather than shifting a day earlier under a UTC anchor.

Send-Invoice Flow

For send_invoice contracts, the billing scheduler materializes invoices locally and then you send them through Stripe:

  1. The daily scheduler runs materializeContractInvoiceForClient to create the local invoice record.
  2. From the admin UI, use the Send Invoice action to call sendStripeInvoice.
  3. Stripe creates a hosted invoice page and returns the URL.
  4. The local invoice record stores stripeInvoiceId and stripeHostedInvoiceUrl.
  5. The client receives an email from Stripe with a link to view and pay the invoice.

Hosted Invoice Page

Stripe hosts the payment page and handles payment collection. The portal can link clients directly to the hosted invoice page so they can view, pay, and download PDFs.

Reference: Stripe Hosted Invoices

Manual Invoices

For manual_only contracts:

  1. Create the invoice from the admin UI with the line items and amounts.
  2. Record payments manually when received.
  3. The lifetime ledger tracks all manual payments alongside Stripe payments.

Manual invoices do not go through Stripe — they exist only in MongoDB. Use this for clients who pay by check, bank transfer, or through an external billing system.

Reminder Sweep

The billing scheduler runs sweepOutstandingInvoiceReminders daily:

  • Only applies to send_invoice and manual_only contractscharge_automatically contracts are handled by Stripe's own retry logic.
  • Sends reminders based on the policy configured in Config → Billing (e.g., 3 days before due, 1 day after, 7 days after).
  • Each reminder is recorded on the invoice's reminderHistory so you can see exactly what was sent and when.

If you change the reminder policy, it only affects future reminders. Already-sent reminders are not retroactively updated.

Lifetime Ledger

Every client has a lifetime ledger on their BillingAccount document. It aggregates:

  • Total revenue from all invoices (Stripe and manual)
  • Total charges mirrored from Stripe
  • Total refunds
  • Current balance

The ledger is refreshed:

  • On every webhook event related to charges, refunds, and payments (refreshBillingAccountLedger)
  • By the daily charge reconciliation job, which repairs mirror and ledger drift

Charge Mirroring

Stripe charge.succeeded and charge.refunded events create or update BillingCharge records in MongoDB. These mirrors are the source for ledger reconciliation and reporting.

If webhooks or reconciliation detect drift, run:

node scripts/test-billing-engine.mjs --phase ledger

before modifying billing code.

Tax Handling

New Stripe invoices and subscriptions are created with automatic_tax: { enabled: true }. This means Stripe calculates tax based on the customer's location data.

For existing Stripe subscriptions created before automatic tax was enforced:

  1. Confirm the customer has sufficient tax location data in Stripe.
  2. Use the client contract Enable Tax action.

Local tax snapshots remain on invoice lines for historical reporting and preview consistency, even when Stripe is calculating tax automatically.

Verification

node --check utils/stripe-billing.js
node --check utils/billing-scheduler.js
node --check routes/admin-billing.js
node scripts/test-billing-engine.mjs

On this page