Skip to content

How does the flow of money work for reimbursements on HCB?

Reimbursements are one of the more complicated parts of HCB! This isn’t a comprehensive guide to how they work, instead, this describes how money flows through the system.

To send the person being reimbursed money, we use standard HCB transfers such as an AchTransfer and a PaypalTransfer. However, we send these transfers from the “HCB Reimbursements Clearinghouse” organisation. On the organisation that is sending the reimbursement we have one transaction on the ledger per reimbursed expense (these are called ExpensePayouts). We took this approach to increase transparency and make it to understand what a reimbursement is for.

A Reimbursement::Report is a collection of Reimbursement::Expenses. No money moves until a report is approved by an admin and, if needed, an organiser.

When a Reimbursement::Report is marked as reimbursement_approved by an admin, we run Reimbursement::Report#reimburse! which creates one ExpensePayout per approved expense and one PayoutHolding for the report.

These transactions internal book transfers that move money from the organisation that is reimbursing someone to the clearinghouse organisation.

An ExpensePayout is a book transfer from “FS Main” to “FS Operating”. It comes in as a negative CanonicalTransaction.

An PayoutHolding is a book transfer from “FS Operating” to “FS Main”. It comes in as a positive CanonicalTransaction. A Reimbursement::Report can only have one PayoutHolding. It’s amount_cents will be the sum of the Reimbursement::Report’s ExpensePayouts’ amount_cents.

They are both created in Reimbursement::Report#reimburse!

After they are both created, they have after_create callbacks that create a CanonicalPendingTransaction. That means there will immediately be a transaction on the ledgers of both the reimbursing organisation and HCB Reimbursements Clearinghouse.

Reimbursement::ExpensePayoutService::Nightly is ran every five minutes and creates Column book transfers for each of these.

It also marks any ExpensePayout with a CanonicalTransaction as settled.

And a PayoutHolding with either a CanonicalTransaction or a CanonicalPendingTransaction as settled. You’ll notice that every PayoutHolding should meet this condition. This is true. It used to wait for a CanonicalTransaction but we started fronting the CanonicalPendingTransaction so it would immediately increase HCB Reimbursement Clearinghouse’s balance. That’s because these transactions are guaranteed to end up on our bank statement.

Reimbursement::PayoutHoldingService::Nightly goes through each settled PayoutHolding and sends the money to the user based on their User::PayoutMethod which is a polymorphic relationship on the User model.

And for 99% of reports, that’s the end of it!

But for that 1%…

Failed PayoutHoldings

If we attempt to send an AchTransfer or a PaypalTransfer and it fails (for ACHs, this means it errors and for PayPal transfers, this means a human tried to send it but something didn’t work), we mark the PayoutHolding as failed.

Users receive an email asking them to update their payout information. In the meantime, the PayoutHolding just sits in a holding state with the money staying in the HCB Reimbursements Clearinghouse.

Once a user updates their payout information, we mark all of their failed PayoutHoldings as settled and try the process all over again.

Reversed PayoutHoldings

In a very rare set of circumstances, we “reverse” a PayoutHolding. That means that the money leaves the HCB Reimbursements Clearinghouse organisation and goes back to the organisation which the report was on. This has to be manually triggered by an engineer by calling PayoutHolding#reverse! in the production console.

We perform the following sanity checks:

raise ArgumentError, "must be a reimbursed report" unless report.reimbursed?

raise ArgumentError, "must be a failed payout holding" unless failed?

raise ArgumentError, "ACH must have been rejected / failed" unless ach_transfer.nil? || ach_transfer.failed? || ach_transfer.rejected?

raise ArgumentError, "PayPal transfer must have been rejected" unless paypal_transfer.nil? || paypal_transfer.rejected?

raise ArgumentError, "a check is present" if increase_check.present?

And if it passes, we create a set of Column book transfers that are essentially the reverse of what we created above.

After this the report will never be reimbursed.

- @sampoder