Files
picpeak/backend/__tests__/services/billingRecipients.test.js
T
Luca 09c5110d2b feat(crm): route billing docs to billing_email when set
Wires customer_accounts.billing_email into the invoice, Storno, and
payment-reminder send paths. Previously the column existed on the
schema and the customer-detail page rendered an input for it, but no
send path read it — every outbound email landed on customer_accounts.email
regardless. That mismatch is the failure mode flagged in
feedback_data_driven_completeness: a UI field that promises behavior
the backend silently doesn't deliver.

Routing matrix:
  - invoice / Storno / payment reminder
      To: billing_email (fallback email when unset)
      CC: email (when billing_email took the To slot) + per-doc cc_pdf_email
  - quote / contract / event reminder / gallery share
      To: email (unchanged — decision-maker address)
  - payment-check / paid-notification
      To: admin contact (unchanged — internal flow)

A new resolveBillingRecipients helper centralises the rules:
prefer billing_email, dedupe addresses case-insensitively, keep
per-doc cc_pdf_email as a supplemental CC. Lives in its own file
(_billingRecipients.js) to match the _renderContext.js convention.
2026-05-27 14:04:15 +02:00

106 lines
3.5 KiB
JavaScript

/**
* Unit tests for the recipient resolver that routes invoice / Storno /
* reminder emails to a bookkeeper address when one is configured,
* while keeping the decision-maker (primary email) on CC.
*
* Pure helper, no DB, no side effects.
*/
const { resolveBillingRecipients } = require('../../src/services/_billingRecipients');
describe('resolveBillingRecipients', () => {
it('routes to the primary email when no billing_email is set', () => {
expect(resolveBillingRecipients({ email: 'bride@example.com' }, null))
.toEqual({ to: 'bride@example.com', cc: undefined });
});
it('routes to billing_email and CCs the primary when both are set', () => {
expect(resolveBillingRecipients({
email: 'bride@example.com',
billing_email: 'books@example.com',
}, null)).toEqual({
to: 'books@example.com',
cc: ['bride@example.com'],
});
});
it('folds the per-document cc_pdf_email into the CC list', () => {
expect(resolveBillingRecipients({
email: 'bride@example.com',
billing_email: 'books@example.com',
}, 'advisor@example.com')).toEqual({
to: 'books@example.com',
cc: ['bride@example.com', 'advisor@example.com'],
});
});
it('uses cc_pdf_email alone when there is no billing_email', () => {
expect(resolveBillingRecipients({
email: 'bride@example.com',
}, 'advisor@example.com')).toEqual({
to: 'bride@example.com',
cc: ['advisor@example.com'],
});
});
it('does not CC the primary onto itself when billing_email equals email', () => {
expect(resolveBillingRecipients({
email: 'same@example.com',
billing_email: 'same@example.com',
}, null)).toEqual({
to: 'same@example.com',
cc: undefined,
});
});
it('is case-insensitive when deduping addresses', () => {
// RFC 5321 says mailbox local-parts MAY be case sensitive, but in
// practice every mail server treats them as insensitive — and the
// admin entering "BRIDE@example.com" in one field and
// "bride@example.com" in another should not produce two copies.
expect(resolveBillingRecipients({
email: 'BRIDE@example.com',
billing_email: 'books@example.com',
}, 'bride@example.com')).toEqual({
to: 'books@example.com',
cc: ['BRIDE@example.com'],
});
});
it('trims whitespace around the addresses', () => {
expect(resolveBillingRecipients({
email: ' bride@example.com ',
billing_email: ' books@example.com\n',
}, '\tadvisor@example.com ')).toEqual({
to: 'books@example.com',
cc: ['bride@example.com', 'advisor@example.com'],
});
});
it('treats empty-string billing_email as not set', () => {
expect(resolveBillingRecipients({
email: 'bride@example.com',
billing_email: '',
}, null)).toEqual({
to: 'bride@example.com',
cc: undefined,
});
});
it('returns an empty To when neither email nor billing_email is set', () => {
// Caller is responsible for surfacing this — emailProcessor's own
// validation will reject the empty recipient. The helper just
// refuses to crash.
expect(resolveBillingRecipients({}, null))
.toEqual({ to: '', cc: undefined });
});
it('tolerates a null customer without throwing', () => {
// Per-doc cc alone is never promoted to To: — it stays
// supplemental. A missing customer is a caller bug; we just refuse
// to crash and let emailProcessor reject the empty recipient.
expect(resolveBillingRecipients(null, 'a@b.com'))
.toEqual({ to: '', cc: undefined });
});
});