mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
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
This commit is contained in:
33
admin/database_updates/3.5.6.php
Normal file
33
admin/database_updates/3.5.6.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 3.5.6 (from 3.5.5)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*
|
||||
* Clients can pay the card fee. On the Stripe payment provider:
|
||||
* payment_provider_card_fee turns it on, and payment_provider_card_fee_percent
|
||||
* and payment_provider_card_fee_fixed are what Stripe charges on a card
|
||||
* payment. On a client, client_card_fee_exempt lets them off it. All start
|
||||
* off, so nothing changes until it is turned on.
|
||||
*
|
||||
* Re-runnable: each column is added only where it isn't there yet.
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
$itflow_new_columns = [
|
||||
['payment_providers', 'payment_provider_card_fee', "TINYINT(1) NOT NULL DEFAULT 0 AFTER `payment_provider_expense_category`"],
|
||||
['payment_providers', 'payment_provider_card_fee_percent', "DECIMAL(5,2) NOT NULL DEFAULT 0.00 AFTER `payment_provider_card_fee`"],
|
||||
['payment_providers', 'payment_provider_card_fee_fixed', "DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER `payment_provider_card_fee_percent`"],
|
||||
['clients', 'client_card_fee_exempt', "TINYINT(1) NOT NULL DEFAULT 0 AFTER `client_tax_exempt_expires`"],
|
||||
];
|
||||
|
||||
foreach ($itflow_new_columns as [$itflow_table, $itflow_column, $itflow_definition]) {
|
||||
$itflow_column_exists = mysqli_query($mysqli, "SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$itflow_table' AND COLUMN_NAME = '$itflow_column' LIMIT 1");
|
||||
if (!$itflow_column_exists || mysqli_num_rows($itflow_column_exists) === 0) {
|
||||
mysqli_query($mysqli, "ALTER TABLE `$itflow_table` ADD `$itflow_column` $itflow_definition");
|
||||
}
|
||||
}
|
||||
|
||||
unset($itflow_new_columns, $itflow_table, $itflow_column, $itflow_definition, $itflow_column_exists);
|
||||
@@ -4,8 +4,8 @@
|
||||
* Admin > 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();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" name="card_fee" value="1" id="paymentProviderCardFee" <?= $card_fee ? 'checked' : '' ?> data-provider-card-fee-switch>
|
||||
<label class="form-check-label" for="paymentProviderCardFee">Clients pay the card fee</label>
|
||||
</div>
|
||||
<div class="form-text ps-5">Each card payment is made bigger by what <?= escapeHtml($name) ?> 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.</div>
|
||||
</div>
|
||||
|
||||
<div data-provider-card-fee>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 mb-3">
|
||||
<label class="form-label" for="paymentProviderCardFeePercent">Percent</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
|
||||
<input type="text" class="form-control" name="card_fee_percent" id="paymentProviderCardFeePercent" inputmode="decimal" maxlength="5" placeholder="2.9" value="<?= $card_fee_percent > 0 ? escapeHtml(rtrim(rtrim(number_format($card_fee_percent, 2, '.', ''), '0'), '.')) : '' ?>">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<?= $error_for('card_fee_percent') ?>
|
||||
</div>
|
||||
<div class="col-sm-6 mb-3">
|
||||
<label class="form-label" for="paymentProviderCardFeeFixed">Plus</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-coins"></i></span>
|
||||
<input type="text" class="form-control" name="card_fee_fixed" id="paymentProviderCardFeeFixed" inputmode="decimal" maxlength="6" placeholder="0.30" value="<?= $card_fee_fixed > 0 ? escapeHtml(number_format($card_fee_fixed, 2, '.', '')) : '' ?>">
|
||||
<?php if ($currency !== '') { ?><span class="input-group-text"><?= escapeHtml($currency) ?></span><?php } ?>
|
||||
</div>
|
||||
<?= $error_for('card_fee_fixed') ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text mt-n2 mb-3">What <?= escapeHtml($name) ?> 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.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="<?= $row ? 'edit_payment_provider' : 'add_payment_provider' ?>" class="btn btn-primary text-bold" data-provider-save <?= !$row && !$free_kinds ? 'disabled' : '' ?>>
|
||||
@@ -214,7 +249,7 @@ ob_start();
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Fees - their vendor and category only while they are recorded. Save - asked first, so what is wrong shows here
|
||||
// Fees - their vendor and category only while they are recorded, the card fee's rates only while clients pay it. Save - asked first, so what is wrong shows here
|
||||
(function () {
|
||||
var form = document.querySelector('[data-payment-provider-form]:not([data-ready])');
|
||||
if (!form) {
|
||||
@@ -223,6 +258,8 @@ ob_start();
|
||||
form.setAttribute('data-ready', '1');
|
||||
var feesSwitch = form.querySelector('[data-provider-fees-switch]');
|
||||
var fees = form.querySelector('[data-provider-fees]');
|
||||
var cardFeeSwitch = form.querySelector('[data-provider-card-fee-switch]');
|
||||
var cardFee = form.querySelector('[data-provider-card-fee]');
|
||||
var save = form.querySelector('[data-provider-save]');
|
||||
var accountNote = form.querySelector('[data-provider-account]');
|
||||
var checked = false;
|
||||
@@ -238,6 +275,17 @@ ob_start();
|
||||
feesSwitch.addEventListener('change', showFees);
|
||||
showFees();
|
||||
|
||||
// The card fee's rates only while clients pay it - off, they aren't sent and stay as saved
|
||||
function showCardFee() {
|
||||
var on = cardFeeSwitch.checked;
|
||||
cardFee.classList.toggle('opacity-50', !on);
|
||||
cardFee.querySelectorAll('input').forEach(function (el) {
|
||||
el.disabled = !on;
|
||||
});
|
||||
}
|
||||
cardFeeSwitch.addEventListener('change', showCardFee);
|
||||
showCardFee();
|
||||
|
||||
function clearErrors() {
|
||||
form.querySelectorAll('.is-invalid').forEach(function (el) { el.classList.remove('is-invalid'); });
|
||||
form.querySelectorAll('[data-error-for]').forEach(function (el) {
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
* through (functions/payment_provider.php). Each row says whether it is on,
|
||||
* live or test (as its keys say), the account its payments go into, its card
|
||||
* limit, whether its fees are recorded as expenses (and what they came to,
|
||||
* and how many are still to record), its saved cards and the autopay on
|
||||
* them, and its card payments in the last 30 days. Check connection asks
|
||||
* the provider whose account the key is; Record fees now does what the
|
||||
* Payment Provider Fees job does every few minutes. Add and Edit are one window
|
||||
* (modals/payment_provider/payment_provider_edit.php); the rest are
|
||||
* admin/post/payment_provider.php.
|
||||
* and how many are still to record) and the card fee clients pay, its saved
|
||||
* cards and the autopay on them, and its card payments in the last 30 days.
|
||||
* Check connection asks the provider whose account the key is; Record fees
|
||||
* now does what the Payment Provider Fees job does every few minutes. Add
|
||||
* and Edit are one window (modals/payment_provider/payment_provider_edit.php);
|
||||
* the rest are admin/post/payment_provider.php.
|
||||
*/
|
||||
|
||||
// Default Column Sortby Filter
|
||||
@@ -33,6 +33,7 @@ if (!isset($sortable_columns[$sort])) {
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS payment_provider_id, payment_provider_name, payment_provider_description, payment_provider_private_key,
|
||||
payment_provider_threshold, payment_provider_active, payment_provider_account, payment_provider_expense_vendor, payment_provider_expense_category,
|
||||
payment_provider_card_fee, payment_provider_card_fee_percent, payment_provider_card_fee_fixed,
|
||||
account_name, account_archived_at, vendor_name, category_name,
|
||||
COALESCE(cards.saved_cards, 0) AS saved_cards, COALESCE(cards.autopay, 0) AS autopay,
|
||||
(SELECT MAX(payment_date) FROM payments WHERE payment_archived_at IS NULL AND payment_reference LIKE CONCAT(payment_provider_name, ' - %')) AS last_paid
|
||||
@@ -160,6 +161,9 @@ $csrf = $_SESSION['csrf_token'];
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">Not recorded</span>
|
||||
<?php } ?>
|
||||
<?php if (billingCardFeeRates($row)) { ?>
|
||||
<div class="mt-1" data-provider-card-fee title="Clients pay it on each card payment, as a Card Fee line on the invoice"><i class="fas fa-fw fa-hand-holding-usd text-secondary me-1"></i>Clients pay <?= escapeHtml(paymentProviderCardFeeText($row)) ?></div>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell small text-nowrap">
|
||||
<?php if ($cards) { ?>
|
||||
|
||||
@@ -70,7 +70,7 @@ if (isset($_POST['check_payment_provider']) || isset($_POST['add_payment_provide
|
||||
$provider_id = paymentProviderWrite($after);
|
||||
$account_name = (string) (paymentProviderRow($provider_id)['account_name'] ?? '');
|
||||
logAudit("Payment Provider", "Create", "$session_name added payment provider " . escapeSql("$name ($mode mode), payments into $account_name, card limit " . paymentProviderLimitText($after['payment_provider_threshold'])
|
||||
. ', fees ' . ($after['payment_provider_expense_vendor'] ? 'recorded' : 'not recorded')), 0, $provider_id);
|
||||
. ', fees ' . ($after['payment_provider_expense_vendor'] ? 'recorded' : 'not recorded') . ', card fee clients pay ' . paymentProviderCardFeeText($after)), 0, $provider_id);
|
||||
flashAlert('<strong>' . escapeHtml($name) . '</strong> added - clients can pay invoices by card' . ($mode === 'test' ? '. Its keys are test keys, so no real money moves until they are live ones' : ''));
|
||||
redirect();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user