mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
Payment Providers stage 3: the providers list shows card payments in the last 30 days with their total and the last payment date, and fees recorded in 30 days with a count of payments whose fee is still to record, flagged once more than two days old; adds Check connection, which reports the Stripe account the saved key belongs to or why it is not connected, and Record fees now, which runs the nightly fee reconciliation on demand through the same shared function the nightly job now uses; Settings > Invoices and billing shows how card payments stand under Paid online, and the Settings overview flags test keys, card payments going into an archived account, and Stripe fees not being recorded, with the Invoices and billing tile showing whether cards are on; overview warnings keep their button beside the text where there is room
This commit is contained in:
@@ -4,10 +4,13 @@
|
||||
* Admin > Payment Providers - the services clients pay invoices by card
|
||||
* 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, its saved cards and the
|
||||
* autopay on them, and when a card payment last came through it. Add and
|
||||
* Edit are one window (modals/payment_provider/payment_provider_edit.php);
|
||||
* turning one off, on and deleting it are admin/post/payment_provider.php.
|
||||
* 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
|
||||
* nightly run does. 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
|
||||
@@ -94,7 +97,7 @@ $csrf = $_SESSION['csrf_token'];
|
||||
<th class="d-none d-lg-table-cell"><?= $sort_link('payment_provider_threshold', 'Card limit') ?></th>
|
||||
<th class="d-none d-lg-table-cell">Fees</th>
|
||||
<th class="d-none d-md-table-cell"><?= $sort_link('saved_cards', 'Saved cards') ?></th>
|
||||
<th class="d-none d-md-table-cell"><?= $sort_link('last_paid', 'Last card payment') ?></th>
|
||||
<th class="d-none d-md-table-cell"><?= $sort_link('last_paid', 'Card payments') ?></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -110,6 +113,7 @@ $csrf = $_SESSION['csrf_token'];
|
||||
$autopay = intval($row['autopay']);
|
||||
$fees_on = intval($row['payment_provider_expense_vendor']) > 0 && intval($row['payment_provider_expense_category']) > 0;
|
||||
$ties = paymentProviderTiesText(paymentProviderTies($provider_id));
|
||||
$activity = paymentProviderActivity($row);
|
||||
?>
|
||||
<tr data-payment-provider-id="<?= $provider_id ?>" class="<?= $on ? '' : 'text-body-secondary' ?>">
|
||||
<td class="ps-3">
|
||||
@@ -146,6 +150,13 @@ $csrf = $_SESSION['csrf_token'];
|
||||
<?php if ($fees_on) { ?>
|
||||
<div><i class="fas fa-fw fa-receipt text-secondary me-1"></i>Recorded</div>
|
||||
<div class="text-body-secondary"><?= escapeHtml(trim($row['vendor_name'] . ' · ' . $row['category_name'], ' ·')) ?></div>
|
||||
<?php if ($activity['fees']) { ?>
|
||||
<div><a class="text-body-secondary" href="/agent/expenses.php?q=<?= urlencode($name . ' fee') ?>" data-provider-fees-total><?= escapeHtml(paymentProviderMoneyText($activity['fees'])) ?> in 30 days</a></div>
|
||||
<?php } ?>
|
||||
<?php if ($activity['waiting']) { ?>
|
||||
<span class="badge <?= $activity['stale'] ? 'bg-warning-subtle text-warning-emphasis border border-warning-subtle' : 'text-bg-light border' ?> fw-normal mt-1" data-provider-fees-waiting="<?= $activity['waiting'] ?>"
|
||||
title="<?= $activity['stale'] ? 'Some are more than two days old - the nightly run should have had them' : 'The nightly run records them once Stripe has settled them' ?>"><?= $activity['waiting'] ?> to record</span>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">Not recorded</span>
|
||||
<?php } ?>
|
||||
@@ -158,7 +169,13 @@ $csrf = $_SESSION['csrf_token'];
|
||||
<span class="text-body-secondary">None yet</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell small text-body-secondary text-nowrap"<?= $row['last_paid'] ? ' title="' . escapeHtml($row['last_paid']) . '"' : '' ?>><?= $row['last_paid'] ? escapeHtml(date('M j, Y', strtotime($row['last_paid']))) : 'None yet' ?></td>
|
||||
<td class="d-none d-md-table-cell small text-nowrap" data-provider-payments="<?= $activity['payments'] ?>">
|
||||
<?php if ($activity['payments']) { ?>
|
||||
<div><?= $activity['payments'] ?> in 30 days</div>
|
||||
<div class="text-body-secondary"><?= escapeHtml(paymentProviderMoneyText($activity['paid'])) ?></div>
|
||||
<?php } ?>
|
||||
<div class="text-body-secondary"<?= $row['last_paid'] ? ' title="' . escapeHtml($row['last_paid']) . '"' : '' ?>><?= $row['last_paid'] ? 'Last ' . escapeHtml(date('M j, Y', strtotime($row['last_paid']))) : 'None yet' ?></div>
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<div class="dropdown dropstart">
|
||||
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-v"></i></button>
|
||||
@@ -166,6 +183,14 @@ $csrf = $_SESSION['csrf_token'];
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="<?= $edit_url ?>">
|
||||
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
|
||||
</a>
|
||||
<a class="dropdown-item" href="post.php?check_payment_provider_connection=<?= $provider_id ?>&csrf_token=<?= $csrf ?>" title="Whether <?= escapeHtml($name) ?> still takes its secret key, and whose account it is">
|
||||
<i class="fas fa-fw fa-plug text-secondary me-2"></i>Check connection
|
||||
</a>
|
||||
<?php if ($fees_on && $activity['waiting']) { ?>
|
||||
<a class="dropdown-item" href="post.php?record_payment_provider_fees=<?= $provider_id ?>&csrf_token=<?= $csrf ?>" title="What the nightly run does - for the card payments whose fee is still to record">
|
||||
<i class="fas fa-fw fa-receipt text-secondary me-2"></i>Record fees now
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php if ($on) { ?>
|
||||
<a class="dropdown-item confirm-link" href="post.php?turn_payment_provider_off=<?= $provider_id ?>&csrf_token=<?= $csrf ?>"
|
||||
data-confirm-title="Turn <?= escapeHtml($name) ?> off?" data-confirm-text="Clients won't be able to pay by card, and autopay with its saved cards is skipped until it is on again. Its cards are kept." data-confirm-button="Turn off">
|
||||
|
||||
@@ -139,3 +139,65 @@ if (isset($_GET['delete_payment_provider'])) {
|
||||
redirect('payment_providers.php');
|
||||
|
||||
}
|
||||
|
||||
// Check connection - is the provider still taking the secret key, and whose account is it?
|
||||
if (isset($_GET['check_payment_provider_connection'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$row = paymentProviderRow($_GET['check_payment_provider_connection']);
|
||||
if (!$row) {
|
||||
flashAlert('That payment provider no longer exists.', 'error');
|
||||
redirect('payment_providers.php');
|
||||
}
|
||||
$name = $row['payment_provider_name'];
|
||||
$asked = paymentProviderAsk($name, $row['payment_provider_private_key']);
|
||||
if (!$asked['ok']) {
|
||||
logApp($name, 'warning', "Check connection - $name turned the saved key away: " . $asked['error']);
|
||||
flashAlert('<strong>' . escapeHtml($name) . '</strong> isn\'t connected - ' . escapeHtml($asked['error']), 'error');
|
||||
redirect();
|
||||
}
|
||||
$account = $asked['account'];
|
||||
$who = trim(($account['name'] ?? '') . (($account['email'] ?? '') !== '' ? ' (' . $account['email'] . ')' : ''));
|
||||
flashAlert('<strong>' . escapeHtml($name) . '</strong> is connected' . ($who !== '' ? ' to ' . escapeHtml($who) : '') . ', with '
|
||||
. (paymentProviderMode($row['payment_provider_private_key']) === 'live' ? 'live keys' : 'test keys - no real money moves'));
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
// Record fees now - what the nightly run does, for the payments whose fee is still to record
|
||||
if (isset($_GET['record_payment_provider_fees'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$row = paymentProviderRow($_GET['record_payment_provider_fees']);
|
||||
if (!$row) {
|
||||
flashAlert('That payment provider no longer exists.', 'error');
|
||||
redirect('payment_providers.php');
|
||||
}
|
||||
$name = $row['payment_provider_name'];
|
||||
if (!intval($row['payment_provider_expense_vendor']) || !intval($row['payment_provider_expense_category'])) {
|
||||
flashAlert('<strong>' . escapeHtml($name) . '</strong>\'s fees aren\'t recorded - switch Record its fees as expenses on in Edit first', 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$done = paymentProviderRecordFees($row);
|
||||
$said = [];
|
||||
if ($done['recorded']) {
|
||||
$said[] = 'Recorded its fee on ' . $done['recorded'] . ' payment' . ($done['recorded'] === 1 ? '' : 's') . ' - ' . paymentProviderMoneyText($done['amount']);
|
||||
logAudit("Payment Provider", "Edit", escapeSql(stripslashes($session_name) . " recorded $name's fees on " . $done['recorded'] . ' card payment' . ($done['recorded'] === 1 ? '' : 's') . ' - ' . paymentProviderMoneyText($done['amount'])), 0, intval($row['payment_provider_id']));
|
||||
}
|
||||
if ($done['waiting']) {
|
||||
$said[] = $done['waiting'] . ' ' . ($done['waiting'] === 1 ? 'isn\'t' : 'aren\'t') . ' settled yet - ' . ($done['waiting'] === 1 ? 'it is' : 'they are') . ' recorded once ' . ($done['waiting'] === 1 ? 'it has' : 'they have');
|
||||
}
|
||||
if ($done['failed']) {
|
||||
$said[] = $name . ' wouldn\'t say for ' . $done['failed'] . ' - ' . $done['error'];
|
||||
}
|
||||
if (!$said) {
|
||||
flashAlert('There are no fees to record - every card payment of the last 30 days has its fee', 'info');
|
||||
redirect();
|
||||
}
|
||||
flashAlert(escapeHtml(implode('. ', $said)), $done['failed'] ? 'warning' : ($done['recorded'] ? 'success' : 'info'));
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ $summaries = [
|
||||
'ticket' => 'Next ' . escapeHtml($config_ticket_prefix . $config_ticket_next_number) . ' · email to ticket ' . $on_off($config_ticket_email_parse),
|
||||
'project' => 'Next ' . escapeHtml($config_project_prefix . $config_project_next_number),
|
||||
'quote' => 'Next ' . escapeHtml($config_quote_prefix . $config_quote_next_number),
|
||||
'invoice' => 'Next ' . escapeHtml($config_invoice_prefix . $config_invoice_next_number) . ' · reminders ' . $on_off($config_send_invoice_reminders) . ' · late fees ' . $on_off($config_invoice_late_fee_enable),
|
||||
'invoice' => 'Next ' . escapeHtml($config_invoice_prefix . $config_invoice_next_number) . ' · reminders ' . $on_off($config_send_invoice_reminders) . ' · late fees ' . $on_off($config_invoice_late_fee_enable)
|
||||
. ' · cards ' . $on_off(billingStripeProvider()),
|
||||
'mail' => 'Sending ' . ($config_smtp_provider ? 'on' : '<span class="text-danger-emphasis">off</span>') . ' · receiving ' . $on_off($config_imap_provider)
|
||||
. (mailQueueSummary()['failed_week'] ? ' · <span class="text-danger-emphasis">' . mailQueueSummary()['failed_week'] . ' failed this week</span>' : ''),
|
||||
'notification' => 'Domain alerts ' . $on_off($config_enable_alert_domain_expire) . ' · ticket emails to clients ' . $on_off($config_ticket_client_general_notifications),
|
||||
@@ -73,6 +74,12 @@ $mail_queue_health = mailQueueHealth();
|
||||
if (!empty($config_smtp_provider) && in_array($mail_queue_health['key'], ['cron_off', 'cron_stopped', 'job_off', 'slow'], true)) {
|
||||
$attention[] = [$mail_queue_health['level'], 'fa-inbox', $mail_queue_health['title'], $mail_queue_health['text'], 'mail_queue.php', 'Mail Queue'];
|
||||
}
|
||||
// Taking card payments - test keys, an account that is archived or gone, fees not being recorded
|
||||
if ($config_module_enable_accounting) {
|
||||
foreach (paymentProviderProblems() as $problem) {
|
||||
$attention[] = $problem;
|
||||
}
|
||||
}
|
||||
if (trim((string) $config_timezone) === '') {
|
||||
$attention[] = ['warning', 'fa-clock', 'No timezone', 'Dates and times are in the server\'s timezone until one is picked.', 'settings_localization.php#setting=config_timezone', 'Pick one'];
|
||||
}
|
||||
@@ -107,7 +114,7 @@ $groups = settingsGroups();
|
||||
<?php foreach ($attention as [$tone, $icon, $title, $text, $url, $action]) { ?>
|
||||
<div class="alert alert-<?= $tone ?> d-flex flex-wrap align-items-center gap-3 py-2 mb-2">
|
||||
<i class="fas fa-fw <?= $icon ?>"></i>
|
||||
<div class="flex-grow-1 min-w-0"><strong><?= escapeHtml($title) ?></strong> - <?= escapeHtml($text) ?></div>
|
||||
<div class="itflow-settings-attention-text"><strong><?= escapeHtml($title) ?></strong> - <?= escapeHtml($text) ?></div>
|
||||
<a href="<?= escapeHtml($url) ?>" class="btn btn-sm btn-<?= $tone ?>"><?= escapeHtml($action) ?><i class="fas fa-arrow-right ms-2"></i></a>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
@@ -68,6 +68,9 @@ echo settingsPageStart('invoice');
|
||||
<?= settingsSectionEnd() ?>
|
||||
|
||||
<?= settingsSectionStart('paid', 'Paid online', 'When a client pays an invoice by card - in the client portal or from the link in its email - through a provider in <a href="payment_providers.php">Payment providers</a>.') ?>
|
||||
<div class="itflow-settings-field">
|
||||
<div class="itflow-settings-note" data-card-payments><?= paymentProviderSummaryHtml() ?></div>
|
||||
</div>
|
||||
<?= settingsInput('config_invoice_paid_notification_email', $config_invoice_paid_notification_email, ['icon' => 'fa-bell', 'type' => 'email', 'maxlength' => 200,
|
||||
'placeholder' => 'billing@company.com', 'help' => 'A shared mailbox or list works best. Empty, nobody is emailed - the payment is still recorded.']) ?>
|
||||
<?= settingsSectionEnd() ?>
|
||||
|
||||
Reference in New Issue
Block a user