There was an error verifying your payment. Please contact us for more information before attempting payment again.

"); // --- Get Stripe config from payment_providers table --- $stripe_provider = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT payment_provider_account, payment_provider_private_key, payment_provider_public_key, payment_provider_active FROM payment_providers WHERE payment_provider_name = 'Stripe' LIMIT 1")); $stripe_publishable = escapeHtml($stripe_provider['payment_provider_public_key'] ?? ''); $stripe_secret = escapeHtml($stripe_provider['payment_provider_private_key'] ?? ''); $stripe_account = intval($stripe_provider['payment_provider_account'] ?? 0); // Show payment form if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent'])) { // Stripe switched off (Admin > Payment Providers) takes no new payments, even from an old link. // A payment already made still comes back below and is booked. if (!$stripe_provider || !intval($stripe_provider['payment_provider_active'])) { echo "

Paying online isn't available just now. Please get in touch to pay this invoice another way.

"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php'; exit(); } $invoice_url_key = escapeSql($_GET['url_key']); $invoice_id = intval($_GET['invoice_id']); // Query invoice details $sql = mysqli_query( $mysqli, "SELECT client_id, client_name, invoice_amount, invoice_currency_code, invoice_date, invoice_discount_amount, invoice_due, invoice_id, invoice_number, invoice_prefix, invoice_status FROM invoices LEFT JOIN clients ON invoice_client_id = client_id WHERE invoice_id = $invoice_id AND invoice_url_key = '$invoice_url_key' AND invoice_status NOT IN ('Draft', 'Paid', 'Cancelled') LIMIT 1" ); // Ensure valid invoice if (!$sql || mysqli_num_rows($sql) !== 1) { echo "

Oops, something went wrong! Please ensure you have the correct URL and have not already paid this invoice.

"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php'; error_log("Stripe payment error - Invoice with ID $invoice_id not found or not eligible."); exit(); } $row = mysqli_fetch_assoc($sql); $invoice_id = intval($row['invoice_id']); $invoice_prefix = escapeHtml($row['invoice_prefix']); $invoice_number = intval($row['invoice_number']); $invoice_status = escapeHtml($row['invoice_status']); $invoice_date = escapeHtml($row['invoice_date']); $invoice_due = escapeHtml($row['invoice_due']); $invoice_discount = floatval($row['invoice_discount_amount']); $invoice_amount = floatval($row['invoice_amount']); $invoice_currency_code = escapeHtml($row['invoice_currency_code']); $client_id = intval($row['client_id']); $client_name = escapeHtml($row['client_name']); // Company info for currency formatting, etc $sql_company = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1"); $company_row = mysqli_fetch_assoc($sql_company); $company_locale = escapeHtml($company_row['company_locale']); // Add up all payments made to the invoice $sql_amount_paid = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id"); $amount_paid = floatval(mysqli_fetch_assoc($sql_amount_paid)['amount_paid']); $balance_to_pay = round($invoice_amount - $amount_paid, 2); // The card fee, when the client pays it - guest_ajax.php charges the same $fee_rates = billingCardFeeFor($client_id); $card_fee = billingCardFeeCents((int) round($balance_to_pay * 100), $fee_rates) / 100; // Get invoice items $sql_invoice_items = mysqli_query($mysqli, "SELECT item_name, item_quantity, item_total FROM invoice_items WHERE item_invoice_id = $invoice_id ORDER BY item_id ASC"); // Currency formatting $currency_format = numfmt_create($company_locale, NumberFormatter::CURRENCY); ?>

Payment for Invoice:

0) { ?> 0) { ?> 0) { ?>
Product Qty Total
Discount
Paid
Balance
Card fee

Payment Total:

0) { ?>

With a card fee for paying by card, which goes on the invoice as a line.


client_secret !== $pi_cs) { error_log("Stripe payment error - Payment intent ID/Secret mismatch for $pi_id"); exit(WORDING_PAYMENT_FAILED); } elseif ($pi_obj->status !== "succeeded") { exit(WORDING_PAYMENT_FAILED); } elseif ($pi_obj->amount !== $pi_obj->amount_received) { error_log("Stripe payment error - payment amount does not match amount paid for $pi_id"); exit(WORDING_PAYMENT_FAILED); } // PI details $pi_date = date('Y-m-d', $pi_obj->created); $pi_invoice_id = intval($pi_obj->metadata->itflow_invoice_id); $pi_client_id = intval($pi_obj->metadata->itflow_client_id); $pi_amount_paid = floatval(($pi_obj->amount_received / 100)); $pi_currency = strtoupper(escapeSql($pi_obj->currency)); $pi_livemode = $pi_obj->livemode; // Get/Check invoice (& client/primary contact) $invoice_sql = mysqli_query( $mysqli, "SELECT client_id, client_name, contact_email, contact_name, invoice_amount, invoice_currency_code, invoice_id, invoice_number, invoice_prefix, invoice_url_key FROM invoices LEFT JOIN clients ON invoice_client_id = client_id LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1 WHERE invoice_id = $pi_invoice_id AND invoice_status NOT IN ('Draft', 'Paid', 'Cancelled') LIMIT 1" ); if (!$invoice_sql || mysqli_num_rows($invoice_sql) !== 1) { error_log("Stripe payment error - Invoice with ID $pi_invoice_id is unknown/not eligible. PI $pi_id"); exit(WORDING_PAYMENT_FAILED); } $row = mysqli_fetch_assoc($invoice_sql); $invoice_id = intval($row['invoice_id']); $invoice_prefix = escapeSql($row['invoice_prefix']); $invoice_number = intval($row['invoice_number']); $invoice_amount = floatval($row['invoice_amount']); $invoice_currency_code = escapeSql($row['invoice_currency_code']); $invoice_url_key = escapeSql($row['invoice_url_key']); $client_id = intval($row['client_id']); $client_name = escapeSql($row['client_name']); $contact_name = escapeSql($row['contact_name']); $contact_email = escapeSql($row['contact_email']); $sql_company = mysqli_query($mysqli, "SELECT company_locale, company_name, company_phone FROM companies WHERE company_id = 1"); $row = mysqli_fetch_assoc($sql_company); $company_name = escapeSql($row['company_name']); $company_phone = escapeSql(formatPhoneNumber($row['company_phone'])); $company_locale = escapeSql($row['company_locale']); $currency_format = numfmt_create($company_locale, NumberFormatter::CURRENCY); $sql_amount_paid_previously = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id"); $amount_paid_previously = floatval(mysqli_fetch_assoc($sql_amount_paid_previously)['amount_paid']); $balance_to_pay = $invoice_amount - $amount_paid_previously; // The card fee it carried, if the client pays one (guest_ajax.php) - only for this invoice $pi_fees = billingCardFeeSplit($pi_obj->metadata->toArray()); $pi_fee_cents = intval($pi_fees[$invoice_id] ?? 0); if (array_diff_key($pi_fees, [$invoice_id => true])) { error_log("Stripe payment error - card fee is not for invoice $invoice_id on $pi_id"); exit(WORDING_PAYMENT_FAILED); } // Compare in whole cents - intval() truncates, which silently accepted an underpayment // of up to 99c as payment in full while rejecting a 1c overpayment. if ((int) round($balance_to_pay * 100) + $pi_fee_cents !== (int) round($pi_amount_paid * 100)) { error_log("Stripe payment error - Invoice balance does not match amount paid for $pi_id"); exit(WORDING_PAYMENT_FAILED); } // A payment already booked - here, or in the client portal - is never booked again, even // when its invoice has been reopened since (an edit, a payment deleted) and owes it again if (mysqli_num_rows(mysqli_query($mysqli, "SELECT payment_id FROM payments WHERE payment_reference = 'Stripe - $pi_id' LIMIT 1"))) { header('Location: //' . $config_base_url . '/guest/guest_view_invoice.php?invoice_id=' . $invoice_id . '&url_key=' . $invoice_url_key); exit(); } // Claim the invoice - the conditional UPDATE is the lock, and the row lock it takes // is what serialises concurrent requests carrying the same payment intent. A request // that loses the race matches 0 rows and must not book the payment a second time. mysqli_query($mysqli, "UPDATE invoices SET invoice_status = 'Paid' WHERE invoice_id = $invoice_id AND invoice_status NOT IN ('Draft', 'Paid', 'Cancelled')"); if (mysqli_affected_rows($mysqli) !== 1) { error_log("Stripe payment - invoice $invoice_id was already settled by a concurrent request; skipping duplicate booking of $pi_id"); header('Location: //' . $config_base_url . '/guest/guest_view_invoice.php?invoice_id=' . $invoice_id . '&url_key=' . $invoice_url_key); exit(); } // The card fee goes on the invoice first, so the payment - balance and fee - pays it to the cent billingCardFeeAdd($invoice_id, $pi_fee_cents / 100, $pi_date); // Add Payment to History mysqli_query($mysqli, "INSERT INTO payments SET payment_date = '$pi_date', payment_amount = $pi_amount_paid, payment_currency_code = '$pi_currency', payment_account_id = $stripe_account, payment_method = 'Stripe', payment_reference = 'Stripe - $pi_id', payment_invoice_id = $invoice_id"); mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Paid', history_description = 'Online Payment added (client)" . ($pi_fee_cents > 0 ? ', with a ' . escapeSql(billingMoney($pi_fee_cents / 100, $pi_currency)) . ' card fee' : '') . " - $ip - $os - $browser', history_invoice_id = $invoice_id"); // Notify appNotify("Invoice Paid", "Invoice $invoice_prefix$invoice_number has been paid by $client_name - $ip - $os - $browser", "/agent/invoice.php?invoice_id=$invoice_id", $pi_client_id); triggerCustomAction('invoice_pay', $invoice_id); $extended_log_desc = ''; if (!$pi_livemode) { $extended_log_desc = '(DEV MODE)'; } mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Payment', log_action = 'Create', log_description = 'Stripe payment of $pi_currency $pi_amount_paid against invoice $invoice_prefix$invoice_number - $pi_id $extended_log_desc', log_ip = '$ip', log_user_agent = '$user_agent', log_client_id = $pi_client_id"); logForwardQueue('audit', mysqli_insert_id($mysqli)); // The client's receipt, and the paid notification address - functions/billing_email.php $config_smtp_host = mysqli_fetch_row(mysqli_query($mysqli, "SELECT config_smtp_host FROM settings WHERE company_id = 1"))[0] ?? ''; if (!empty($config_smtp_host)) { billingPaymentSend($invoice_id, $pi_amount_paid, ['date' => $pi_date, 'method' => 'Card', 'reference' => "Stripe - $pi_id", 'how' => 'Online card'], true); } // Redirect user to invoice header('Location: //' . $config_base_url . '/guest/guest_view_invoice.php?invoice_id=' . $invoice_id . '&url_key=' . $invoice_url_key); } else { exit(WORDING_PAYMENT_FAILED); } require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php';