From cf2c9c2d9b250eab9ab2abb2d23862f5ef82eaf7 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 5 Nov 2026 12:47:06 -0500 Subject: [PATCH] Let clients pay the card fee - turn on Clients pay the card fee on Stripe in Admin > Payment Providers with Stripe's percent and fixed rate, and each card payment in the client portal, through an invoice's link, by autopay or from Charge Saved Card is grossed up so Stripe's fee comes out of it, with one fee per payment shared over its invoices; the fee is shown before paying (Pay invoices, checkout, the guest pay page, invoice pages, autopay and the Charge Saved Card window) and goes on each invoice as a Card Fee line with no tax when the payment is booked, carried in the PaymentIntent's itflow_fees metadata and checked against the amount received to the cent; a client can be let off it with No card fee on their Billing tab, refunding a card payment takes its Card Fee line back off, autopay and Charge Saved Card now compare cents instead of whole dollars, database 3.5.6 adds the provider and client columns, changelog updated --- CHANGELOG.md | 9 +- admin/database_updates/3.5.6.php | 33 +++ .../payment_provider_edit.php | 54 +++- admin/payment_providers.php | 16 +- admin/post/payment_provider.php | 2 +- agent/modals/client/client_add.php | 6 + agent/modals/client/client_card_fee_field.php | 30 +++ agent/modals/client/client_edit.php | 8 +- .../payment/payment_saved_method_add.php | 19 +- agent/post/client.php | 4 +- agent/post/client_model.php | 2 + agent/post/payment.php | 50 +++- agent/recurring_invoice.php | 4 +- client/invoice.php | 8 +- client/pay.php | 11 +- client/post.php | 4 +- client/recurring_invoice.php | 3 +- client/saved_payment_methods.php | 9 +- client/unpaid_invoices.php | 37 ++- cron/nightly_tasks.php | 33 ++- db.sql | 4 + functions/billing_pay.php | 233 ++++++++++++++++-- functions/payment_provider.php | 44 +++- guest/guest_ajax.php | 25 +- guest/guest_pay_invoice_stripe.php | 34 ++- guest/guest_view_invoice.php | 4 +- js/portal.js | 23 +- 27 files changed, 615 insertions(+), 94 deletions(-) create mode 100644 admin/database_updates/3.5.6.php create mode 100644 agent/modals/client/client_card_fee_field.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd887c94..64c731141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ This file documents all notable changes made to ITFlow. ### Upgrading -Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's one database change (3.5.4 to 3.5.5) in the same pass. Nothing else is required. Everyone is signed out once after the update, agents and client portal contacts alike - see Security. The new Payment Provider Fees job in Maintenance > Cron is set up by itself. +Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's two database changes (3.5.4 to 3.5.6) in the same pass. Nothing else is required. Everyone is signed out once after the update, agents and client portal contacts alike - see Security. The new Payment Provider Fees job in Maintenance > Cron is set up by itself. ### Security @@ -21,6 +21,7 @@ Update from Maintenance > Update — Queue Update hands the job to cron, which u - API: products can be updated through the API (`products/update`). Only the fields you send are changed, and the key's user needs edit access to Sales, the same as editing a product in ITFlow. - API keys: agents are notified in ITFlow a week before an API key expires, so an integration doesn't stop without warning. - Payment Providers: Stripe's fee on a card payment is now recorded as an expense about 15 minutes after the payment, instead of the next morning. A new Payment Provider Fees job in Maintenance > Cron does it, and you can run it more often there. Record fees now still works any time. +- Payment Providers: clients can pay the card fee. Turn on Clients pay the card fee on Stripe in Admin > Payment Providers and enter what Stripe charges you, as 2.9% plus 0.30. Each card payment is then made bigger by Stripe's fee on it, so you get the full invoice. The fee goes on the invoice as a Card Fee line, with no tax, when the payment goes through. Clients see it before they pay: on Pay invoices and the checkout page in the client portal, on the page an invoice's link opens, and next to autopay. It applies to payments in the client portal, through an invoice's link, by autopay, and when you charge a saved card, where the Charge Saved Card window shows it too. One payment for several invoices has one fee, shared over them. A client can be let off it with No card fee on their Billing tab. Refunding a card payment from ITFlow takes its Card Fee line back off the invoice. It is off until you turn it on. Card surcharges have rules where you and your clients are - in the US, for one, the card networks cap them and don't allow them on debit cards - so check them before turning it on. ### Bug Fixes @@ -41,6 +42,12 @@ Update from Maintenance > Update — Queue Update hands the job to cron, which u - New `requestIsMfaSetup($allowed)` in `functions/auth.php` decides what a session that still has to set up MFA may request: the script really running (`realpath($_SERVER['SCRIPT_FILENAME'])` under the app root, never `PHP_SELF`, which a path added after the script name changes), and every GET and POST key inside one of the shapes allowed for that script. `includes/load_user_session.php` applies it when `user_config_force_mfa` is 1 and `user_token` is empty, allowing `agent/user/mfa_enforcement.php` with no parameters and `agent/user/post.php` with only `csrf_token`, `enable_mfa` and `verify_code`, or `?logout` - everything else redirects to the set up page, modals and AJAX included. The client portal gate in `client/includes/check_login.php` now uses it too (`profile.php?mfa`, and `post.php` with `portal_mfa_enable` or `?logout`), replacing its `basename($_SERVER['PHP_SELF'])` test; the separate gate in `client/post.php`, which let any request through that carried `portal_mfa_enable` or `logout` alongside other actions, is gone. `portalViewAsAccess()` refuses an agent still owing MFA set up. The `enable_mfa` handler sends `userNotifyChange()`. - `accountLedgerShows($all_accounts)`: across every account, the `in` and `out` cuts leave out `txn_kind` `transfer_in` and `transfer_out`. `agent/transactions.php` does the same for its Money in and Money out totals, except on the Transfers pill, and notes it in the table footer when the listed lines include transfers the totals leave out. The transactions export, which takes its cuts from the same function, follows. - Stripe fees are recorded by a new job, `cron/payment_provider_fees.php` (`payment_provider_fees` in `includes/cron_jobs.php`, every 15 minutes, interval-safe), instead of `cron/nightly_tasks.php`. The dispatcher seeds its row the first time it sees it. `paymentProviderRecordFees()` now holds `GET_LOCK('itflow_payment_provider_fees', 0)` around the work, which moved to `paymentProviderRecordFeesLocked()`, and returns `'busy' => true` when another run has it - "has no fee yet" is read before the fee is written, so two runs at once would each record it. Record fees now says when a run is in progress. +- Database 3.5.6 adds `payment_providers.payment_provider_card_fee`, `payment_provider_card_fee_percent` (decimal 5,2) and `payment_provider_card_fee_fixed`, and `clients.client_card_fee_exempt`, all defaulting to 0. Like 3.5.4 and 3.5.5 it is safe to run again, and `db.sql` has the columns. +- The card fee lives in `functions/billing_pay.php`. `billingCardFeeRates()` reads it off the Stripe provider, `billingCardFeeFor($client_id)` adds the client's exemption, and `billingCardFeeCents($cents, $rates)` grosses up in whole numbers: `ceil((cents + fixed) * 10000 / (10000 - percent * 100)) - cents`, so the fee is the percent of everything charged plus the fixed part. `js/portal.js` works it the same way from `data-pay-fee-percent` (hundredths of a percent) and `data-pay-fee-fixed` (cents) on the Pay invoices form. `billingCardFeeShares()` splits one payment's fee over its invoices in proportion, by largest remainder. +- `billingPaySelection()` now also returns `fees` ([invoice_id => cents]), `fee`, `charge` and `fee_rates`; the card limit is still checked on what the invoices come to, without the fee. `billingStripeIntentParams()` charges the fee and puts it in the PaymentIntent's metadata as `itflow_fees`, in the same `id:cents,...` form as `itflow_invoices`, which still carries what each invoice was owed. The guest Payment Element (`guest/guest_ajax.php`), autopay in `cron/nightly_tasks.php` and Charge Saved Card (`agent/post/payment.php`) add `itflow_fees` for their one invoice. +- Booking: `billingStripeBook()` checks that `itflow_invoices` and `itflow_fees` add up to `amount_received` and refuses an `itflow_fees` that doesn't read. For each invoice it calls `billingCardFeeAdd($invoice_id, $fee, $date)` before the payment, which inserts a Card Fee line (`BILLING_CARD_FEE_NAME`, no tax, no product) and raises `invoice_amount` by the fee directly rather than through `billingRecalc()`, so the invoice's other lines aren't re-taxed and the payment pays it to the cent. The payment is what was owed plus the fee. A PaymentIntent without `itflow_fees`, from before, books as it did. The guest return, autopay and Charge Saved Card do the same, and now compare the amount received with the balance and fee in whole cents (autopay and Charge Saved Card compared whole dollars with `intval()`). +- Refunding a Stripe payment (`refund_payment_stripe`) retrieves its PaymentIntent and calls `billingCardFeeRemove($invoice_id, $fee)`, which deletes the latest Card Fee line of that amount and lowers `invoice_amount` to match, before the invoice's status is worked out again. +- `paymentProviderCheck()` takes `card_fee`, `card_fee_percent` (0 to 15) and `card_fee_fixed` (0 to 100), needs at least one rate above 0 when it is on, and keeps the saved rates when it is off. `paymentProviderChangeLines()` and the Create audit line include it, through the new `paymentProviderCardFeeText()`. The client's switch is `agent/modals/client/client_card_fee_field.php`, shown on the Billing tab of Add and Edit Client while Stripe is set up, and saved through `card_fee_field` and `card_fee_exempt` in `agent/post/client_model.php`. ## [26.11.2] diff --git a/admin/database_updates/3.5.6.php b/admin/database_updates/3.5.6.php new file mode 100644 index 000000000..4ad269014 --- /dev/null +++ b/admin/database_updates/3.5.6.php @@ -0,0 +1,33 @@ + Payment Providers - one provider's window: Add (no id, or from * payment_provider_add.php) or Edit. Its keys - the secret one never put back * into the page, so left empty it stays - whether it is on, the account its - * payments go into, the card limit, and whether its fees are recorded as - * expenses. Save asks first (check_payment_provider, + * payments go into, the card limit, whether its fees are recorded as + * expenses, and whether clients pay a card fee to cover them. Save asks first (check_payment_provider, * admin/post/payment_provider.php): what is wrong shows beside each field and * the window stays open; once all is right, it saves. */ @@ -35,6 +35,9 @@ $vendor_id = intval($row['payment_provider_expense_vendor'] ?? 0); $category_id = intval($row['payment_provider_expense_category'] ?? 0); $threshold = round(floatval($row['payment_provider_threshold'] ?? 0), 2); $records_fees = $row ? ($vendor_id > 0 && $category_id > 0) : true; +$card_fee = intval($row['payment_provider_card_fee'] ?? 0) === 1; +$card_fee_percent = round(floatval($row['payment_provider_card_fee_percent'] ?? 0), 2); +$card_fee_fixed = round(floatval($row['payment_provider_card_fee_fixed'] ?? 0), 2); // A new one starts on an account and a vendor named for it, and Processing Fee, where there are those if (!$row) { @@ -204,6 +207,38 @@ ob_start(); +
+
+ data-provider-card-fee-switch> + +
+
Each card payment is made bigger by what takes of it, so you get what the invoice is for. The fee goes on the invoice as a Card Fee line, and clients see it before they pay - in the client portal, through an invoice's link, by autopay, or when you charge a saved card. A client can be let off it on their Billing tab.
+
+ +
+
+
+ +
+ + + % +
+ +
+
+ +
+ + + +
+ +
+
+
What charges you on a card payment - your account's rate, as 2.9% plus 0.30. Card surcharges have rules where you and your clients are: in the US, for one, the card networks cap them and don't allow them on debit cards, and some states limit them further.
+
+