Skip to content

Doctor payout · one $180.00 visit Illustrative amounts · real states

A patient is refunded. Whether that costs your clinic anything depends on one thing only — whether you had already paid the doctor their share.

  1. 1 owed
  2. 2 settled
  3. 3 excluded
  4. 4 doctor owes clinic
  5. 5 cleared

The refund landed first, so the payout never included it. Nothing is owed in either direction — state 3, and it is excluded by construction rather than by a rule. The payout went first. $95.96 of it was the doctor’s share of money the patient no longer has — state 4, and the only one of the five that costs you anything.

The patient got their money back. You had already paid the doctor.

Every dollar of a doctor’s share sits in exactly one of five accounting states, and three of them exist only because money can come back. If the refund arrives before the payout, nothing happens at all — the payout query only ever looks at payments marked paid, so a refunded one is not skipped, it is simply never seen. If it arrives after, the money is already gone and somebody has to be told.

Most clinic software models a doctor’s pay as a running total, which cannot represent this at all: a total has no way of remembering that a particular $95.96 was handed over on the strength of a payment that has since been reversed. Qlynic tracks it per payment, so the amount is a real figure attached to a real visit on a real date, not a discrepancy someone notices at the end of the quarter.

The same $180.00 visit, refunded in full Illustrative amounts · real arithmetic

The patient has their money back. The doctor has given back their share. Everybody is square — and the books still do not close.

$5.52 Stripe’s fee. It is charged on the way in and it is not returned on the way out, so it is the one thing a full refund cannot put back.

  1. + $174.48 what Stripe landed in your account
  2. − $95.96 paid to the doctor as their 55 % share
  3. − $180.00 refunded to the patient in full
  4. + $95.96 recovered from the doctor — all of it, because their entitlement is now nothing
  5. − $5.52 where the clinic ends up

The recovery, as it is written

DrPayable − ROUND( MAX(0, StripeNet − Refunded ) × SharePct / 100, 0 )

Somebody has to be short by the fee. It is never your doctor.

On a full refund the arithmetic inside the recovery goes negative. The visit brought in $174.48 after Stripe’s cut and $180.00 went back out, so what is left of it is −$5.52. Left alone, that would make the doctor’s entitlement a negative number and the recovery would come out larger than the $95.96 they were ever paid. The MAX(0, …) stops it at zero, so the most that can ever come back is exactly what went out, and the shortfall stays where the fee was incurred.

The recovery is not a proportion of what the doctor was paid, either. It is worked out the way the original share was: take what is actually left in your account for that visit, apply the percentage that was frozen onto the payment the day it settled, and the difference between that and what they already have is what comes back. On a $60.00 refund of this visit that is $33.00 — a proportional method would have said $31.99, and the $1.01 gap is real money that would have quietly stayed with the wrong party.

None of it is a stored guess. The figure is recomputed from the payment’s own recorded columns every time it is asked for — what Stripe netted, what was refunded, and the share as it stood on the day — so it is reproducible months later and it does not drift when you change a rate.

Recording a payout · Dr Okafor Illustrative amounts · real refusal

This month the doctor is owed less than they owe back. Somebody presses the button anyway.

The batch, as assembled

unpaid payable $150.00

refund adjustments − $240.00

net to pay the doctor $0.00 floored

Rolled back — the batch row was never inserted

$90.00 of doctor debt would have disappeared. Not written off, not collected, not recorded anywhere — just gone, because a subtraction cannot go below zero and the adjustment would have been marked settled regardless.

What it says instead, word for word Refund adjustments (240.00) exceed the unpaid payable (150.00) for the selected payments. A residual doctor debt of 90.00 CAD would be silently forgiven. Collect it and record a debt settlement first, or include more unpaid payments in this payout.

The safest bug is the one nobody would ever have found. So it refuses the whole transaction.

The net a payout pays out cannot be negative, so it floors at zero. On its own that is correct — you do not hand a doctor a negative cheque. But the pass that runs immediately afterwards marks every pending refund adjustment as settled, and those two facts together are a hole: the $90.00 the doctor still owed would have been cleared from the ledger while nothing was collected and nothing was written down. No error, no warning, no row. It would simply have stopped being true.

So the guard does not adjust the number or log a note and carry on. It calls tx.Rollback() — the batch insert, the payment stamps and the adjustment clearing all go back as though the button had never been pressed — and returns a message that names both amounts, states the residual in your own currency, and gives you the two legitimate ways forward: collect the debt and record it as a settlement, or add more unpaid payments to the payout so there is enough to absorb it.

It is a small amount of money and an unlikely month. That is rather the point — it is exactly the kind of case that is never noticed, never reported and never fixed, because the only person who could have spotted it is the doctor, and they would have had to be checking.

Payroll run · April 2026 Three states · two doors

A run is created as a draft, checked, locked, and paid. Each of those moves closes the one before it, and nothing in the software opens it again.

  1. Draft lines editable · run deletable
  2. Finalized totals recomputed from the line items and locked
  3. Paid IsPaid = 1, PaidAtUtc stamped
  1. What the closed doors say, word for word
  2. PR_UpdateLineItem Can only edit draft payroll runs.
  3. PR_FinalizeRun Only draft runs can be finalized.
  4. PR_DeleteRun Only draft runs can be deleted.
  5. PR_MarkAllPaid Run must be finalized before marking as paid.
  6. PR_MarkLinePaid Already paid or not found.

And there is no other door. A search of the payroll file for reopen, unfinalize, void, reverse, undo and rollback returns nothing at all — not a disabled button, not an admin-only path. The methods to go backwards were never written.

A payroll run goes one way. There is no button that puts it back.

Exactly three words are ever written to a run’s status, and each transition shuts the one behind it. Once a run is finalized its line items stop accepting edits and the run stops being deletable; once it is marked paid it can never be marked paid again, because the method that does it will only accept a run that is still finalized. Those are not settings. They are the only code paths that exist.

The same care runs one level down. Both statements that flag staff as paid carry AND IsPaid=0, so a double-click never re-stamps somebody’s payment date or overwrites how they were paid, and the single-line version reports “Already paid or not found.” rather than quietly succeeding. Creating a run checks for an existing one for that clinic and that month before it opens a transaction, so two April runs cannot exist.

And the run keeps its own copy of who was on it. Each line takes the staff member’s name, role, employment type and compensation type at the moment the draft is created, so renaming somebody in June does not quietly rewrite what April said. The audit entry for an edit goes further still: it reads the net before the change and reads it again afterwards, so what gets logged is what the database actually holds rather than what the code believed it had written.

Staff & payroll · what it will not do Six ports · all of them empty

Everything above is Qlynic writing down what happened to money. Here is the other half of that sentence — the things it deliberately has no mechanism for at all.

  1. Pay anybody “Mark as paid” writes IsPaid=1 and a timestamp. There is no Stripe transfer, no EFT, no bank file and no notification in either file.
  2. Calculate tax TaxCents arrives on the request and is added straight to the deductions. No rate, no table, no jurisdiction, no CPP, no EI.
  3. Run weekly or bi-weekly The only period key is PeriodMonth, parsed as yyyy-MM and normalised to the 1st. The word weekly does not appear.
  4. Treat a staff record as a login ClinicStaff and ClinicAdmins are never joined in either direction, and a staff row carries no user id at all.
  5. Carry a debt forward on its own debt_collected_deduction records that you intend to deduct it. Nothing schedules the deduction and nothing applies it later.
  6. Un-record a payout No delete, no edit and no reversal exists for a payout batch, and no code path ever puts a batch id back to NULL.

Qlynic is the ledger. It is not the bank, it is not your accountant and it is not a payroll bureau. What it does is keep the record straight enough, and dated accurately enough, that the people who are those things can do their work from it without having to reconstruct your month first.

Six covers, and nothing under any of them. That is the honest inventory.

The two that surprise people most are tax and payment. Payroll here is a record of what you decided to pay and what you decided to deduct — the deduction figures are numbers a person enters, not amounts the software worked out, and it has no opinion about whether they are right. And “mark as paid” is a bookkeeping flag: it records that you paid somebody, by whatever means you actually used. Nothing in this system moves a dollar.

A staff record is not a login either, and the separation is deliberate rather than an omission. The person who cleans the clinic on Thursdays belongs on a payroll run and has no business having an account; the locum who covers a weekend may need an account for two days and never appear on a run at all. Tying the two together forces every clinic to invent one of those problems.

None of this is a roadmap in disguise. Some of it may change — sub-monthly periods are an obvious candidate — but what is on this page is what the software does today, and the page is written from the source rather than from the plan, so if you find something here that is no longer true, that is a bug in the page and worth telling us about.