mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
697 lines
31 KiB
PHP
697 lines
31 KiB
PHP
<?php
|
|
|
|
/*
|
|
* ITFlow - GET/POST request handler for credits & payments
|
|
*
|
|
* Payments are accounting work - Financial, with Sales at Read to see the
|
|
* invoices (billingMoneyCan()). Credits keep their old checks until the
|
|
* feature is finished.
|
|
*/
|
|
|
|
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
|
|
|
if (isset($_POST['add_payment'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
enforceBillingMoney('take');
|
|
|
|
$invoice_id = intval($_POST['invoice_id']);
|
|
$balance = floatval($_POST['balance']);
|
|
$date = escapeSql($_POST['date']);
|
|
$amount = floatval($_POST['amount']);
|
|
$account = intval($_POST['account']);
|
|
$currency_code = escapeSql($_POST['currency_code']);
|
|
$payment_method = escapeSql($_POST['payment_method']);
|
|
$reference = escapeSql($_POST['reference']);
|
|
$email_receipt = intval($_POST['email_receipt']);
|
|
|
|
$client_id = intval(getFieldById('invoices', $invoice_id, 'invoice_client_id'));
|
|
|
|
enforceClientAccess();
|
|
|
|
//Check to see if amount entered is greater than the balance of the invoice
|
|
if ($amount > $balance) {
|
|
flashAlert("Payment can not be more than the balance", 'error');
|
|
redirect();
|
|
} else {
|
|
mysqli_query($mysqli,"INSERT INTO payments SET payment_date = '$date', payment_amount = $amount, payment_currency_code = '$currency_code', payment_account_id = $account, payment_method = '$payment_method', payment_reference = '$reference', payment_invoice_id = $invoice_id");
|
|
|
|
// Get Payment ID for reference
|
|
$payment_id = mysqli_insert_id($mysqli);
|
|
|
|
//Add up all the payments for the invoice and get the total amount paid to the invoice
|
|
$sql_total_payments_amount = mysqli_query($mysqli,"SELECT SUM(payment_amount) AS payments_amount FROM payments WHERE payment_invoice_id = $invoice_id");
|
|
$row = mysqli_fetch_assoc($sql_total_payments_amount);
|
|
$total_payments_amount = floatval($row['payments_amount']);
|
|
|
|
//Get the invoice total
|
|
$sql = mysqli_query($mysqli,"SELECT invoice_amount, invoice_currency_code, invoice_number, invoice_prefix FROM invoices WHERE invoice_id = $invoice_id");
|
|
$row = mysqli_fetch_assoc($sql);
|
|
$invoice_amount = floatval($row['invoice_amount']);
|
|
$invoice_prefix = escapeSql($row['invoice_prefix']);
|
|
$invoice_number = intval($row['invoice_number']);
|
|
$invoice_currency_code = escapeSql($row['invoice_currency_code']);
|
|
|
|
//Calculate the Invoice balance
|
|
$invoice_balance = $invoice_amount - $total_payments_amount;
|
|
|
|
//Determine if invoice has been paid then set the status accordingly
|
|
$invoice_status = $invoice_balance == 0 ? "Paid" : "Partial";
|
|
|
|
//Update Invoice Status
|
|
mysqli_query($mysqli,"UPDATE invoices SET invoice_status = '$invoice_status' WHERE invoice_id = $invoice_id");
|
|
|
|
//Add Payment to History
|
|
mysqli_query($mysqli,"INSERT INTO history SET history_status = '$invoice_status', history_description = 'Payment added', history_invoice_id = $invoice_id");
|
|
|
|
logAudit("Invoice", "Payment", "Payment amount of " . numfmt_format_currency($currency_format, $amount, $invoice_currency_code) . " added to invoice $invoice_prefix$invoice_number", $client_id, $invoice_id);
|
|
|
|
// The client's receipt - functions/billing_email.php
|
|
if ($email_receipt == 1) {
|
|
$receipt_to = billingPaymentSend($invoice_id, $amount, ['date' => $date, 'method' => stripslashes($payment_method), 'reference' => stripslashes($reference)]);
|
|
if ($receipt_to) {
|
|
logAudit("Invoice", "Payment", "Payment receipt for invoice $invoice_prefix$invoice_number queued to " . escapeSql(implode(', ', $receipt_to)), $client_id, $invoice_id);
|
|
}
|
|
}
|
|
|
|
triggerCustomAction('invoice_pay', $invoice_id);
|
|
|
|
flashAlert("Payment amount <strong>" . numfmt_format_currency($currency_format, $amount, $invoice_currency_code) . "</strong> added");
|
|
|
|
redirect();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($_POST['edit_payment'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
enforceBillingMoney('edit');
|
|
|
|
$payment_id = intval($_POST['payment_id']);
|
|
$date = escapeSql($_POST['date']);
|
|
$amount = floatval($_POST['amount']);
|
|
$account = intval($_POST['account']);
|
|
$payment_method = escapeSql($_POST['payment_method']);
|
|
$reference = escapeSql($_POST['reference']);
|
|
|
|
// payments has no client column - the client comes from the invoice the payment sits on
|
|
$client_id = intval(getFieldById('invoices', intval(getFieldById('payments', $payment_id, 'payment_invoice_id')), 'invoice_client_id'));
|
|
|
|
enforceClientAccess();
|
|
|
|
// Cleared on a reconciled statement - its date, amount and account stay
|
|
if (!reconcileCheckChange('payment', $payment_id, $date, $amount, $account, $error)) {
|
|
flashAlert(escapeHtml($error), 'error');
|
|
redirect();
|
|
}
|
|
// Moved to another account - out of the deposit it was in
|
|
transferDepositRelease('payment', $payment_id, $account);
|
|
|
|
mysqli_query($mysqli,"UPDATE payments SET payment_date = '$date', payment_amount = $amount, payment_account_id = $account, payment_method = '$payment_method', payment_reference = '$reference' WHERE payment_id = $payment_id");
|
|
|
|
// A changed amount can leave the invoice paid, part paid or owing again
|
|
updateInvoiceStatusFromPayments(intval(getFieldById('payments', $payment_id, 'payment_invoice_id')));
|
|
|
|
logAudit("Payment", "Edit", "Payment edited amount of " . numfmt_format_currency($currency_format, $amount, $session_company_currency));
|
|
|
|
flashAlert("Payment edited to amount <strong>" . numfmt_format_currency($currency_format, $amount, $session_company_currency) . "</strong> added");
|
|
|
|
redirect();
|
|
|
|
}
|
|
|
|
/*
|
|
Apply Credit Not ready for use 2025-08-27 - JQ
|
|
|
|
if (isset($_POST['apply_credit'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
enforceUserPermission('module_sales', 2);
|
|
enforceUserPermission('module_financial', 2);
|
|
|
|
$invoice_id = intval($_POST['invoice_id']);
|
|
$credit_amount_applied = floatval($_POST['credit_amount_applied']);
|
|
|
|
$sql = mysqli_query($mysqli, "SELECT invoice_client_id, invoice_credit_amount, invoice_number, invoice_prefix, invoice_status FROM invoices LEFT JOIN clients ON invoice_client_id = client_id WHERE invoice_id = $invoice_id");
|
|
$row = mysqli_fetch_assoc($sql);
|
|
|
|
$invoice_prefix = escapeSql($row['invoice_prefix']);
|
|
$invoice_number = intval($row['invoice_number']);
|
|
$invoice_status = escapeSql($row['invoice_status']);
|
|
$invoice_credit_amount = floatval($row['invoice_credit_amount']);
|
|
$invoice_amount = floatval('invoice_amount');
|
|
$client_id = intval($row['invoice_client_id']);
|
|
|
|
enforceClientAccess();
|
|
|
|
// Get Credit Balance
|
|
$sql_credit_balance = mysqli_query($mysqli, "SELECT SUM(credit_amount) AS credit_balance FROM credits WHERE credit_client_id = $client_id");
|
|
$row = mysqli_fetch_assoc($sql_credit_balance);
|
|
|
|
$credit_balance = floatval($row['credit_balance']);
|
|
|
|
// Get Invoice Balance
|
|
$sql_amount_paid = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id");
|
|
$row = mysqli_fetch_assoc($sql_amount_paid);
|
|
$amount_paid = floatval($row['amount_paid']);
|
|
|
|
$invoice_balance = $invoice_amount - $amount_paid;
|
|
|
|
// Get Credit Tally applied to invoice
|
|
$sql_credit_tally = mysqli_query($mysqli, "SELECT SUM(credit_tally) AS credit_balance FROM credits WHERE credit_invoice_id = $invoice_id");
|
|
$row = mysqli_fetch_assoc($sql_credit_tally);
|
|
|
|
$credit_tally = floatval($row['credit_tally']);
|
|
|
|
// Check to see if amount entered is greater than the balance of the invoice
|
|
if ($credit_amount_applied > $invoice_balance) {
|
|
flashAlert("Credit can not be more than the balance", 'alert');
|
|
redirect();
|
|
}
|
|
|
|
// Check to see if amount entered is greater than the credit balance
|
|
if ($credit_amount_applied > $credit_balance) {
|
|
flashAlert("Credit can not be more than the available credit", 'alert');
|
|
redirect();
|
|
}
|
|
|
|
// Insert a new credit usage record linked to the invoice
|
|
mysqli_query($mysqli, "
|
|
INSERT INTO credits SET
|
|
credit_amount = -$amount,
|
|
credit_type = 'usage',
|
|
credit_created_by = $session_user_id,
|
|
credit_client_id = $client_id,
|
|
credit_invoice_id = $invoice_id
|
|
");
|
|
|
|
$new_invoice_amount = $invoice_amount - $credit_amount_applied;
|
|
|
|
// Calculate updated invoice credit sum
|
|
$result = mysqli_query($mysqli, "
|
|
SELECT SUM(credit_amount) AS credit_total
|
|
FROM credits
|
|
WHERE credit_invoice_id = $invoice_id
|
|
");
|
|
$total_credit_applied = floatval(mysqli_fetch_assoc($result)['credit_total']);
|
|
|
|
// Get invoice amount
|
|
$invoice_amount = floatval(getFieldByID('invoices', $invoice_id, 'invoice_amount'));
|
|
|
|
// Determine new status
|
|
$invoice_due = $invoice_amount + $total_credit_applied;
|
|
$invoice_status = ($invoice_due <= 0) ? 'Paid' : 'Partial';
|
|
|
|
// Update the invoice credit amount
|
|
mysqli_query($mysqli, "
|
|
UPDATE invoices
|
|
SET invoice_credit_amount = $total_credit_applied
|
|
WHERE invoice_id = $invoice_id
|
|
");
|
|
|
|
// Update invoice status only (not invoice_credit_amount)
|
|
mysqli_query($mysqli, "UPDATE invoices SET invoice_status = '$invoice_status' WHERE invoice_id = $invoice_id");
|
|
|
|
// Log credit application in history
|
|
mysqli_query($mysqli, "
|
|
INSERT INTO history SET
|
|
history_status = '$invoice_status',
|
|
history_description = 'Credit applied',
|
|
history_invoice_id = $invoice_id
|
|
");
|
|
|
|
logAudit("Invoice", "Payment", "Credit " . numfmt_format_currency($currency_format, $amount, $session_company_currency) . " applied to invoice $invoice_prefix$invoice_number", $client_id, $invoice_id);
|
|
|
|
triggerCustomAction('invoice_pay', $invoice_id);
|
|
|
|
flashAlert("Credit amount <strong>" . numfmt_format_currency($currency_format, $amount, $session_company_currency) . "</strong> applied");
|
|
|
|
redirect();
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
if (isset($_POST['add_payment_stripe'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
enforceBillingMoney('take');
|
|
|
|
$invoice_id = intval($_POST['invoice_id']);
|
|
$saved_payment_id = intval($_POST['saved_payment_id']);
|
|
|
|
// Get invoice details
|
|
$sql = mysqli_query($mysqli,"SELECT client_id, client_name, contact_email, contact_extension, contact_mobile,
|
|
contact_mobile_country_code, contact_name, contact_phone, contact_phone_country_code,
|
|
invoice_amount, invoice_currency_code, invoice_number, invoice_prefix, invoice_status,
|
|
invoice_url_key FROM invoices
|
|
LEFT JOIN clients ON invoice_client_id = client_id
|
|
LEFT JOIN contacts ON client_id = contact_client_id AND contact_primary = 1
|
|
WHERE invoice_id = $invoice_id"
|
|
);
|
|
$row = mysqli_fetch_assoc($sql);
|
|
$invoice_number = intval($row['invoice_number']);
|
|
$invoice_status = escapeSql($row['invoice_status']);
|
|
$invoice_amount = floatval($row['invoice_amount']);
|
|
$invoice_prefix = escapeSql($row['invoice_prefix']);
|
|
$invoice_number = intval($row['invoice_number']);
|
|
$invoice_url_key = escapeSql($row['invoice_url_key']);
|
|
$invoice_currency_code = escapeSql($row['invoice_currency_code']);
|
|
$client_id = intval($row['client_id']);
|
|
$client_name = escapeSql($row['client_name']);
|
|
$contact_name = escapeSql($row['contact_name']);
|
|
$contact_email = escapeSql($row['contact_email']);
|
|
$contact_phone = escapeSql(formatPhoneNumber($row['contact_phone'], $row['contact_phone_country_code']));
|
|
$contact_extension = preg_replace("/[^0-9]/", '',$row['contact_extension']);
|
|
$contact_mobile = escapeSql(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code']));
|
|
|
|
enforceClientAccess();
|
|
|
|
// Get ITFlow company details
|
|
$sql = mysqli_query($mysqli,"SELECT company_address, company_city, company_country, company_email, company_name, company_phone,
|
|
company_phone_country_code, company_state, company_website, company_zip FROM companies WHERE company_id = 1");
|
|
$row = mysqli_fetch_assoc($sql);
|
|
$company_name = escapeSql($row['company_name']);
|
|
$company_country = escapeSql($row['company_country']);
|
|
$company_address = escapeSql($row['company_address']);
|
|
$company_city = escapeSql($row['company_city']);
|
|
$company_state = escapeSql($row['company_state']);
|
|
$company_zip = escapeSql($row['company_zip']);
|
|
$company_phone = escapeSql(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
|
$company_email = escapeSql($row['company_email']);
|
|
$company_website = escapeSql($row['company_website']);
|
|
|
|
// Sanitize Config vars from get_settings.php
|
|
$config_invoice_from_name = escapeSql($config_invoice_from_name);
|
|
$config_invoice_from_email = escapeSql($config_invoice_from_email);
|
|
|
|
// Get Client Payment Details
|
|
$sql = mysqli_query($mysqli, "SELECT payment_provider_account, payment_provider_client, payment_provider_private_key,
|
|
payment_provider_public_key, saved_payment_description, saved_payment_provider_method FROM client_saved_payment_methods LEFT JOIN payment_providers ON saved_payment_provider_id = payment_provider_id LEFT JOIN client_payment_provider ON saved_payment_client_id = client_id WHERE saved_payment_id = $saved_payment_id LIMIT 1");
|
|
$row = mysqli_fetch_assoc($sql);
|
|
|
|
$public_key = escapeSql($row['payment_provider_public_key']);
|
|
$private_key = escapeSql($row['payment_provider_private_key']);
|
|
$account_id = intval($row['payment_provider_account']);
|
|
$payment_provider_client = escapeSql($row['payment_provider_client']);
|
|
$saved_payment_method = escapeSql($row['saved_payment_provider_method']);
|
|
$saved_payment_description = escapeSql($row['saved_payment_description']);
|
|
|
|
// Sanity checks
|
|
if (!$payment_provider_client || !$saved_payment_method) {
|
|
flashAlert("Stripe not enabled or no client card saved", 'error');
|
|
redirect();
|
|
} elseif ($invoice_status !== 'Sent' && $invoice_status !== 'Viewed') {
|
|
flashAlert("Invalid invoice state (draft/partial/paid/not billable)", 'error');
|
|
redirect();
|
|
} elseif ($invoice_amount == 0) {
|
|
flashAlert("Invalid invoice amount", 'error');
|
|
redirect();
|
|
}
|
|
|
|
// Initialize Stripe
|
|
require_once __DIR__ . '/../../includes/stripe_init.php';
|
|
$stripe = new \Stripe\StripeClient($private_key);
|
|
|
|
$balance_to_pay = round($invoice_amount, 2);
|
|
$pi_description = "ITFlow: $client_name payment of $invoice_currency_code $balance_to_pay for $invoice_prefix$invoice_number";
|
|
|
|
// Create a payment intent
|
|
try {
|
|
$payment_intent = $stripe->paymentIntents->create([
|
|
'amount' => intval(round($balance_to_pay * 100)), // Times by 100 as Stripe expects values in cents
|
|
'currency' => $invoice_currency_code,
|
|
'customer' => $payment_provider_client,
|
|
'payment_method' => $saved_payment_method,
|
|
'off_session' => true,
|
|
'confirm' => true,
|
|
'description' => $pi_description,
|
|
'metadata' => [
|
|
'itflow_client_id' => $client_id,
|
|
'itflow_client_name' => $client_name,
|
|
'itflow_invoice_number' => $invoice_prefix . $invoice_number,
|
|
'itflow_invoice_id' => $invoice_id,
|
|
]
|
|
]);
|
|
|
|
// Get details from PI
|
|
$pi_id = escapeSql($payment_intent->id);
|
|
$pi_date = date('Y-m-d', $payment_intent->created);
|
|
$pi_amount_paid = floatval(($payment_intent->amount_received / 100));
|
|
$pi_currency = strtoupper(escapeSql($payment_intent->currency));
|
|
$pi_livemode = $payment_intent->livemode;
|
|
|
|
} catch (Exception $e) {
|
|
$error = $e->getMessage();
|
|
error_log("Stripe payment error - encountered exception during payment intent for invoice ID $invoice_id / $invoice_prefix$invoice_number: $error");
|
|
logApp("Stripe", "error", "Exception during PI for invoice ID $invoice_id: $error");
|
|
}
|
|
|
|
if ($payment_intent->status == "succeeded" && intval($balance_to_pay) == intval($pi_amount_paid)) {
|
|
|
|
// Update Invoice Status
|
|
mysqli_query($mysqli, "UPDATE invoices SET invoice_status = 'Paid' WHERE invoice_id = $invoice_id");
|
|
|
|
// 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 = $account_id, 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 (agent)', history_invoice_id = $invoice_id");
|
|
|
|
// The client's receipt, and the paid notification address - functions/billing_email.php
|
|
if (!empty($config_smtp_provider)) {
|
|
$receipt_to = billingPaymentSend($invoice_id, $pi_amount_paid, ['date' => $pi_date, 'method' => 'Card', 'reference' => "Stripe - $pi_id", 'how' => 'Card'], true);
|
|
if ($receipt_to) {
|
|
logAudit("Invoice", "Payment", "Payment receipt for invoice $invoice_prefix$invoice_number queued to " . escapeSql(implode(', ', $receipt_to)), $client_id, $invoice_id);
|
|
}
|
|
}
|
|
|
|
// Log info
|
|
$extended_log_desc = '';
|
|
if (!$pi_livemode) {
|
|
$extended_log_desc = '(DEV MODE)';
|
|
}
|
|
|
|
// Notify/log
|
|
appNotify("Invoice Paid", "Invoice $invoice_prefix$invoice_number automatically paid", "/agent/invoice.php?invoice_id=$invoice_id", $client_id);
|
|
logAudit("Invoice", "Payment", "$session_name initiated Stripe payment amount of " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . " added to invoice $invoice_prefix$invoice_number - $pi_id $extended_log_desc", $client_id, $invoice_id);
|
|
triggerCustomAction('invoice_pay', $invoice_id);
|
|
|
|
flashAlert("Payment amount <strong>" . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . "</strong> added");
|
|
|
|
redirect();
|
|
|
|
} else {
|
|
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Payment failed', history_description = 'Stripe pay failed due to payment error', history_invoice_id = $invoice_id");
|
|
|
|
logAudit("Invoice", "Payment", "Failed online payment amount of invoice $invoice_prefix$invoice_number due to Stripe payment error", $client_id, $invoice_id);
|
|
flashAlert("Payment failed", 'error');
|
|
|
|
redirect();
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($_POST['add_bulk_payment'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
enforceBillingMoney('take');
|
|
|
|
$client_id = intval($_POST['client_id']);
|
|
$date = escapeSql($_POST['date']);
|
|
$bulk_payment_amount = floatval($_POST['amount']);
|
|
$bulk_payment_amount_static = floatval($_POST['amount']);
|
|
$total_account_balance = floatval($_POST['balance']);
|
|
$account = intval($_POST['account']);
|
|
$currency_code = escapeSql($_POST['currency_code']);
|
|
$payment_method = escapeSql($_POST['payment_method']);
|
|
$reference = escapeSql($_POST['reference']);
|
|
$email_receipt = intval($_POST['email_receipt']);
|
|
|
|
enforceClientAccess();
|
|
|
|
// Check if bulk_payment_amount exceeds total_account_balance
|
|
if ($bulk_payment_amount > $total_account_balance) {
|
|
flashAlert("Payment exceeds Client Balance.", 'error');
|
|
redirect();
|
|
}
|
|
|
|
// Get Invoices
|
|
// Only invoices still waiting on money - not drafts, not paid, not cancelled or non-billable
|
|
$sql_invoices = "SELECT * FROM invoices
|
|
WHERE invoice_status IN ('Sent', 'Viewed', 'Partial')
|
|
AND invoice_client_id = $client_id
|
|
ORDER BY invoice_number ASC";
|
|
$result_invoices = mysqli_query($mysqli, $sql_invoices);
|
|
|
|
// Loop Through Each Invoice
|
|
$applied_payments = [];
|
|
while ($row = mysqli_fetch_assoc($result_invoices)) {
|
|
$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_url_key = escapeSql($row['invoice_url_key']);
|
|
$invoice_balance_query = "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id";
|
|
$result_amount_paid = mysqli_query($mysqli, $invoice_balance_query);
|
|
$row_amount_paid = mysqli_fetch_assoc($result_amount_paid);
|
|
$amount_paid = floatval($row_amount_paid['amount_paid']);
|
|
$invoice_balance = $invoice_amount - $amount_paid;
|
|
|
|
if ($bulk_payment_amount <= 0) {
|
|
break; // Exit the loop if no payment amount is left
|
|
}
|
|
|
|
// Nothing owed on this one - a zero-value invoice, or one paid without its status catching up
|
|
if ($invoice_balance <= 0) {
|
|
continue;
|
|
}
|
|
|
|
if ($bulk_payment_amount >= $invoice_balance) {
|
|
$payment_amount = $invoice_balance;
|
|
$invoice_status = "Paid";
|
|
} else {
|
|
$payment_amount = $bulk_payment_amount;
|
|
$invoice_status = "Partial";
|
|
}
|
|
|
|
// Subtract the payment amount from the bulk payment amount
|
|
$bulk_payment_amount -= $payment_amount;
|
|
|
|
// Get Invoice Remain Balance
|
|
$remaining_invoice_balance = $invoice_balance - $payment_amount;
|
|
|
|
// Add Payment
|
|
$payment_query = "INSERT INTO payments (payment_date, payment_amount, payment_currency_code, payment_account_id, payment_method, payment_reference, payment_invoice_id) VALUES ('{$date}', {$payment_amount}, '{$currency_code}', {$account}, '{$payment_method}', '{$reference}', {$invoice_id})";
|
|
mysqli_query($mysqli, $payment_query);
|
|
$payment_id = mysqli_insert_id($mysqli);
|
|
|
|
// Update Invoice Status
|
|
$update_invoice_query = "UPDATE invoices SET invoice_status = '{$invoice_status}' WHERE invoice_id = {$invoice_id}";
|
|
mysqli_query($mysqli, $update_invoice_query);
|
|
|
|
// Add Payment to History
|
|
$history_description = "Payment added";
|
|
$add_history_query = "INSERT INTO history (history_status, history_description, history_invoice_id) VALUES ('{$invoice_status}', '{$history_description}', {$invoice_id})";
|
|
mysqli_query($mysqli, $add_history_query);
|
|
|
|
// For the receipt - what went on each invoice
|
|
$applied_payments[] = ['invoice_id' => $invoice_id, 'amount' => $payment_amount];
|
|
|
|
triggerCustomAction('invoice_pay', $invoice_id);
|
|
|
|
} // End Invoice Loop
|
|
|
|
// The client's receipt, listing each invoice paid - functions/billing_email.php
|
|
$alert_message = '';
|
|
if ($email_receipt == 1 && !empty($applied_payments)) {
|
|
$applied = [];
|
|
foreach ($applied_payments as $entry) {
|
|
$applied[] = ['invoice' => billingRow('invoice', $entry['invoice_id'], false), 'amount' => $entry['amount']];
|
|
}
|
|
[$subject, $email] = billingPaymentEmail($applied, ['date' => $date, 'method' => stripslashes($payment_method), 'reference' => stripslashes($reference), 'currency' => $currency_code]);
|
|
$receipt_to = billingEmailQueue(billingEmailRecipients($client_id), $subject, $email);
|
|
if ($receipt_to) {
|
|
foreach ($applied_payments as $entry) {
|
|
billingLog('invoice', $entry['invoice_id'], 'Sent', 'Payment receipt emailed to ' . implode(', ', $receipt_to));
|
|
}
|
|
logAudit("Payment", "Email", "Bulk Payment receipt for multiple Invoices queued to " . escapeSql(implode(', ', $receipt_to)), $client_id);
|
|
$alert_message .= "Email receipt queued and ";
|
|
}
|
|
}
|
|
|
|
logAudit("Invoice", "Payment", "Bulk Payment amount of " . numfmt_format_currency($currency_format, $bulk_payment_amount_static, $currency_code) . " applied to multiple invoices", $client_id);
|
|
|
|
flashAlert("$alert_message Bulk Payment added");
|
|
|
|
redirect();
|
|
|
|
}
|
|
|
|
if (isset($_GET['delete_payment'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
enforceBillingMoney('delete');
|
|
|
|
$payment_id = intval($_GET['delete_payment']);
|
|
|
|
// payments has no client column - the client comes from the invoice the payment sits on
|
|
$sql = mysqli_query($mysqli,"SELECT invoice_client_id, invoice_number, invoice_prefix, payment_invoice_id FROM payments
|
|
LEFT JOIN invoices ON payment_invoice_id = invoice_id
|
|
WHERE payment_id = $payment_id
|
|
LIMIT 1"
|
|
);
|
|
$row = mysqli_fetch_assoc($sql);
|
|
if (!$row) {
|
|
flashAlert('That payment no longer exists.', 'error');
|
|
redirect();
|
|
}
|
|
$invoice_id = intval($row['payment_invoice_id']);
|
|
$invoice_prefix = escapeSql($row['invoice_prefix']);
|
|
$invoice_number = intval($row['invoice_number']);
|
|
$client_id = intval($row['invoice_client_id']);
|
|
|
|
enforceClientAccess();
|
|
|
|
if (!reconcileCheckDelete('payment', $payment_id, $error)) {
|
|
flashAlert(escapeHtml($error), 'error');
|
|
redirect();
|
|
}
|
|
|
|
mysqli_query($mysqli,"DELETE FROM payments WHERE payment_id = $payment_id");
|
|
|
|
// Recalculate from what is left rather than from the pre-delete total
|
|
$invoice_status = updateInvoiceStatusFromPayments($invoice_id);
|
|
|
|
mysqli_query($mysqli,"INSERT INTO history SET history_status = '$invoice_status', history_description = 'Payment deleted', history_invoice_id = $invoice_id");
|
|
|
|
logAudit("Invoice", "Edit", "$session_name deleted Payment on Invoice $invoice_prefix$invoice_number", $client_id, $invoice_id);
|
|
|
|
flashAlert("Payment deleted", 'error');
|
|
|
|
redirect();
|
|
|
|
}
|
|
|
|
if (isset($_GET['refund_payment_stripe'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
enforceBillingMoney('delete');
|
|
|
|
$payment_id = intval($_GET['refund_payment_stripe']);
|
|
|
|
// payments has no client column - the client comes from the invoice the payment sits on
|
|
$sql = mysqli_query($mysqli,"SELECT invoice_client_id, invoice_number, invoice_prefix, payment_invoice_id FROM payments
|
|
LEFT JOIN invoices ON payment_invoice_id = invoice_id
|
|
WHERE payment_id = $payment_id
|
|
AND payment_method = 'Stripe'
|
|
LIMIT 1"
|
|
);
|
|
$row = mysqli_fetch_assoc($sql);
|
|
if (!$row) {
|
|
flashAlert('That payment no longer exists.', 'error');
|
|
redirect();
|
|
}
|
|
$invoice_id = intval($row['payment_invoice_id']);
|
|
$invoice_prefix = escapeSql($row['invoice_prefix']);
|
|
$invoice_number = intval($row['invoice_number']);
|
|
$client_id = intval($row['invoice_client_id']);
|
|
|
|
enforceClientAccess();
|
|
|
|
// Refunding deletes the payment - not one cleared on a reconciled statement
|
|
if (!reconcileCheckDelete('payment', $payment_id, $error)) {
|
|
flashAlert(escapeHtml($error), 'error');
|
|
redirect();
|
|
}
|
|
|
|
// Get Stripe Payment Intent ID
|
|
$sql = mysqli_query($mysqli,"SELECT payment_amount, payment_reference FROM payments WHERE payment_id = $payment_id LIMIT 1");
|
|
$row = mysqli_fetch_assoc($sql);
|
|
if (preg_match('/(?<payment_intent>pi_[A-Za-z0-9]+)/', $row['payment_reference'], $matches)) {
|
|
$stripe_payment_intent_id = $matches['payment_intent'];
|
|
}
|
|
// Only this payment's part - one card payment from the client portal can pay several invoices
|
|
$refund_cents = (int) round(floatval($row['payment_amount']) * 100);
|
|
|
|
// Get invoice details
|
|
$sql = mysqli_query($mysqli,"SELECT client_id, client_name, contact_email, contact_extension, contact_mobile,
|
|
contact_mobile_country_code, contact_name, contact_phone, contact_phone_country_code,
|
|
invoice_amount, invoice_currency_code, invoice_number, invoice_prefix, invoice_status,
|
|
invoice_url_key FROM invoices
|
|
LEFT JOIN clients ON invoice_client_id = client_id
|
|
LEFT JOIN contacts ON client_id = contact_client_id AND contact_primary = 1
|
|
WHERE invoice_id = $invoice_id"
|
|
);
|
|
$row = mysqli_fetch_assoc($sql);
|
|
$invoice_number = intval($row['invoice_number']);
|
|
$invoice_status = escapeSql($row['invoice_status']);
|
|
$invoice_amount = floatval($row['invoice_amount']);
|
|
$invoice_prefix = escapeSql($row['invoice_prefix']);
|
|
$invoice_number = intval($row['invoice_number']);
|
|
$invoice_url_key = escapeSql($row['invoice_url_key']);
|
|
$invoice_currency_code = escapeSql($row['invoice_currency_code']);
|
|
$client_id = intval($row['client_id']);
|
|
$client_name = escapeSql($row['client_name']);
|
|
$contact_name = escapeSql($row['contact_name']);
|
|
$contact_email = escapeSql($row['contact_email']);
|
|
$contact_phone = escapeSql(formatPhoneNumber($row['contact_phone'], $row['contact_phone_country_code']));
|
|
$contact_extension = preg_replace("/[^0-9]/", '',$row['contact_extension']);
|
|
$contact_mobile = escapeSql(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code']));
|
|
|
|
$stripe_provider = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT payment_provider_private_key FROM payment_providers WHERE payment_provider_name = 'Stripe' LIMIT 1"));
|
|
if (!$stripe_provider) {
|
|
exit("Stripe not enabled / configured");
|
|
}
|
|
$stripe_secret_key = $stripe_provider['payment_provider_private_key'];
|
|
|
|
// Initialize Stripe
|
|
try {
|
|
require_once __DIR__ . '/../../includes/stripe_init.php';
|
|
$stripe = new \Stripe\StripeClient($stripe_secret_key);
|
|
|
|
// Refund
|
|
$refund = $stripe->refunds->create([
|
|
'payment_intent' => $stripe_payment_intent_id,
|
|
'amount' => $refund_cents,
|
|
]);
|
|
} catch (Exception $e) {
|
|
$error = $e->getMessage();
|
|
error_log("Stripe refund error - encountered exception during refund for invoice ID $invoice_id / $invoice_prefix$invoice_number: $error");
|
|
logApp("Stripe", "error", "Exception during refund for invoice ID $invoice_id: $error");
|
|
flashAlert("Stripe refund failed: $error", 'error');
|
|
redirect();
|
|
}
|
|
|
|
mysqli_query($mysqli,"DELETE FROM payments WHERE payment_id = $payment_id");
|
|
|
|
// Recalculate from what is left rather than from the pre-delete total
|
|
$invoice_status = updateInvoiceStatusFromPayments($invoice_id);
|
|
|
|
mysqli_query($mysqli,"INSERT INTO history SET history_status = '$invoice_status', history_description = 'Payment $stripe_payment_intent_id deleted and refunded', history_invoice_id = $invoice_id");
|
|
|
|
logAudit("Invoice", "Edit", "$session_name refunded and deleted Payment $stripe_payment_intent_id on Invoice $invoice_prefix$invoice_number", $client_id, $invoice_id);
|
|
|
|
flashAlert("Payment deleted and refunded in Stripe", 'error');
|
|
|
|
redirect();
|
|
|
|
}
|
|
|
|
/* Email a payment's receipt to the client's billing contacts again - from its quick look. */
|
|
if (isset($_GET['payment_send_receipt'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
enforceBillingMoney('receipt');
|
|
|
|
$row = incomePaymentRow(intval($_GET['payment_send_receipt']));
|
|
if (!$row || $row['invoice_number'] === null) {
|
|
flashAlert('That payment no longer exists, or is not one you can see', 'error');
|
|
redirect();
|
|
}
|
|
$receipt_to = billingPaymentSend($row['invoice_id'], floatval($row['payment_amount']), [
|
|
'date' => $row['payment_date'],
|
|
'method' => (string) $row['payment_method'],
|
|
'reference' => (string) $row['payment_reference'],
|
|
]);
|
|
if (!$receipt_to) {
|
|
flashAlert(escapeHtml($row['client_name']) . ' has no billing contact with an email to send the receipt to', 'error');
|
|
redirect();
|
|
}
|
|
|
|
logAudit("Invoice", "Payment", "Payment receipt for invoice " . escapeSql($row['invoice_prefix'] . $row['invoice_number']) . " queued to " . escapeSql(implode(', ', $receipt_to)), intval($row['invoice_client_id']), intval($row['invoice_id']));
|
|
|
|
flashAlert("Receipt for <strong>" . escapeHtml(incomePaymentLabel($row)) . "</strong> queued to " . escapeHtml(implode(', ', $receipt_to)));
|
|
|
|
redirect();
|
|
|
|
}
|