Dentolize · Balance Invoice Webhooks & API Config Cleanup Walkthrough
On this pageWhat to testEdge case discovered while preparing this documentationSilent failure mode (pre-existing, not new)

For Quality

What to test

Balance webhook firing (functional)

  • Add Balance → webhook fires. With apiEnabled on and a webhook row

subscribed to NEW_BALANCE_INVOICE + NEW_BALANCE_PAYMENT, use Add Balance on a patient. Confirm both events arrive, in that order (invoice call happens before payment call — packages/server/src/resolvers/mutations/actions/appointments/addPatientBalance.js:268-293).

  • Overpayment → webhook fires. Record a payment on an invoice for more

than the remaining balance. Confirm the same two events fire (packages/server/src/resolvers/mutations/mutationUtils/paymentUtils.js:1680-1708), gated on if (balanceInvoice) — i.e. only when there actually was an overage.

  • Exact payment → no balance webhook. Paying exactly what's owed should

fire the existing NEW_PAYMENT event only, not the balance events — balanceInvoice is null when balance is falsy.

  • Subscription granularity. A webhook row subscribed only to NEW_INVOICE

/ NEW_PAYMENT (not the balance variants) should NOT receive balance events, and vice versa — each handleCallApi call checks api.apis.includes(type) per event type independently (packages/server/src/apis/apiConfig/mutationsApiConfig.js:40).

Payload correctness

  • insurance_policy_data.name should now be the policy's holderName,

not blank/undefined, in NEW_PATIENT, NEW_INVOICE, and both new balance payloads — verify against a patient that actually has an insurance policy attached (a patient without one should still get insurance_policy_data: {}, the ternary's other branch).

  • Balance invoice reference numbers. For the overpayment path

specifically, confirm the original invoice keeps its own pre-existing reference number in the GraphQL response, and the new balance invoice gets its own freshly-assigned one — this was backwards before this PR (see Payload accuracy fixes). Easy to miss in testing because both invoices belong to the same patient and payment action, so it's tempting to only check "a reference number appeared."

  • New payload fields (email, gender, birthDate, referral_data,

tooth) should be present when the source patient/invoice-line/operation actually has that data, and cleanly absent/empty (not throw) when it doesn't.

API Config form regression

  • Type field is gone. Confirm the webhook config form no longer shows a

Type column, and that submitting a row doesn't send a type key at all (values.input?.map(({ id, url, secret, apis }) => ...) explicitly destructures only these four fields now).

  • Existing rows with a stored type value (from before this PR) should

still load and save correctly — the GraphQL input and Yup validation no longer reference type, so any stale type key in old stored JSON should simply be ignored, not cause a validation error. Worth an explicit regression test against a company that has pre-existing API Config data with type: 'NPHIES' or type: 'ZATCA' stored.

  • Row limits: UI caps new rows at 5 (disabled={fields.length >= 5}),

backend Yup validation caps the array at 10 (packages/server/src/permissions/inputRules.js, apiConfig: yup.array().max(10)) — worth confirming these two limits don't produce a confusing UX (e.g. what happens to a company that already has 6-10 rows from before the UI cap existed).

Edge case discovered while preparing this documentation

Attempting to exercise Add Balance end-to-end in the sandbox used for this walkthrough (sandbox company, owner user) failed server-side with:

PrismaClientValidationError: Argument `id` must not be null.

at tx.invoice.create()'s doctor: { connect: { id: company.createdById } } (packages/server/src/resolvers/mutations/actions/appointments/addPatientBalance.js:135, and the equivalent payment.create() at line 194). The same pattern exists in the overpayment path (packages/server/src/resolvers/mutations/mutationUtils/paymentUtils.js:1461 and :1481, using invoice.company.createdById).

This line is unchanged by this PR — it existed identically before the refactor, so this is not a regression introduced here. But it means: any company whose Company.createdById is null cannot use Add Balance or overpayment-to-balance conversion at all, and therefore can never produce the new webhook events either, regardless of configuration. Worth a regression/data-integrity check: does production have any company with apiEnabled = true and createdById = null? If so, this PR's new webhooks are unreachable for that company today, silently. This is a pre-existing latent bug surfaced by, but not caused by, this PR — flagging it here since QA testing this PR's webhooks against a similarly-misconfigured test company would produce a false negative that looks like a webhook bug but isn't one.

Silent failure mode (pre-existing, not new)

handleCallApi wraps its entire body in try { ... } catch (e) { /* Ignored */ }. There is no test coverage visible for delivery failure handling because there's no observable behavior to assert on — a failed webhook delivery produces no error, no log, no retry. This isn't new in this PR, but the two new event types inherit it. If retry/observability is ever added, this PR's event types will need the same treatment as the existing ones.