diff --git a/.gitignore b/.gitignore
index 1dc7ee4b..f60657bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,5 +25,6 @@ plugins/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/CSS/*
xcustom/*
!xcustom/readme.php
post/xcustom
+custom/*
!post/xcustom/readme.php
.zed
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8cc0a40c..9fcc1cb9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,92 @@
This file documents all notable changes made to ITFlow.
+## [25.09]
+
+***BACK UP*** before updating.
+
+---
+
+### Breaking Changes and Notes
+- We strongly recommend updating from the command line, however if performed via the webui and after performed it will return a 404. thats normal as the directory structure has changed, just close your browser then log back in then go back to update to perform the many database updates.
+- This is a major release with significant changes. While the community has done a great job identifying bugs, some may still remain — continued testing is encouraged.
+- All AI settings will be **reset** and must be reconfigured using the new AI provider backend.
+- The `xcustom` directory has been renamed to `custom`. All custom libraries and post-processing scripts should now be placed here.
+
+---
+
+### Added / Changed
+- Numerous UI improvements and refinements across the application.
+- Enhanced visual clarity by thickening the left border on ticket comments to help identify comment types.
+- Ticket details UI redesigned to use less space at the top of the screen.
+- Introduced tracking for the **first response date/time** on tickets.
+- New reporting feature: **Average time to first response** on tickets.
+- Stripe integration rebuilt using the new **payment provider backend**.
+- Clients can now save and manage **multiple payment methods**.
+- Support for selecting saved cards for **recurring invoices** in both the client and agent portals.
+- Initial database structure and logic added for **credit management** (feature not yet enabled).
+- Major **backend directory restructuring**.
+- Introduced **stock/inventory management**, including a stock ledger backend.
+- Stock quantities now update automatically when invoice items are added or removed.
+- Invoice autocomplete now includes: **name, description, price, tax, stock levels**, and links `product_id` to `item_id`.
+- Added a **category filter** to invoices.
+- Linked stock to related expenses.
+- New product fields: **location, code, and type**.
+- Products now separated into two types: **Service** and **Product**.
+- **Dark mode** introduced.
+- Projects: Now support linking **closed tickets**.
+- Clients: Added bulk actions for tags, referral source, industry, hourly rate, email, archive, and restore.
+- Invoices: Bulk action added to **assign categories**.
+- Assets: New `client_uri` field, visible in both the agent and client portals.
+- Client Portal: Clients can now **select an asset** during ticket creation.
+- Client Portal: Company logo now **displays in the header**.
+- Client Portal: Dashboard cards are now **clickable** for more detail.
+- Assets: Option added to include **MAC Address** in additional columns.
+- Asset Interface: Bulk actions added — set DHCP, network type, and delete.
+- API:
+ - Added `/location` endpoint.
+ - Ticket content now supports **HTML formatting**.
+- New option to filter and display **500 records per page** in the footer.
+- Payment methods are now treated as a **separate entity** instead of being grouped under categories.
+- Updated libraries:
+ - **TinyMCE**
+ - **Chart.js** (major upgrade)
+ - **DataTables**
+ - **Bootstrap**
+ - **FullCalendar**
+ - **php-stripe**
+
+---
+
+### Fixed
+- Several security vulnerabilities patched.
+- Ticket status is no longer updated when scheduling.
+- Client Portal: Tech contacts can no longer edit their own details.
+- Fixed overlapping logo issue in Invoice/Quote PDF exports.
+- Refactored `check_login.php` into multiple files for modular login functionality.
+- Removed redundant logging comments for redirects.
+- Renamed `get_settings.php` to `load_global_settings.php`.
+- Simplified syntax for `ajax-modal` and updated usage throughout the app.
+- Fixed issue where primary contact text wasn’t displaying.
+- Corrected client **Net Terms** display.
+- Fixed logic for recurring expense **next run date**.
+- Resolved broken **IMAP test button**.
+- Archived clients can no longer log into the portal.
+- Searching closed tickets no longer reverts to open tickets.
+- Fixed project search filter not showing completed projects.
+- Fixed issue where company logo was not being removed correctly.
+- Resolved API bugs:
+ - Default rate and net terms.
+ - Contact location.
+ - Document endpoint.
+
+---
+
+### Developer Updates
+- Replaced legacy code with newer functions like `redirect()`, `getFieldById()`, and `flash_alert()`.
+- Significantly improved performance of queries used for filter selection boxes.
+
+
## [25.06.1]
### Fixed
diff --git a/admin/ai_model.php b/admin/ai_model.php
new file mode 100644
index 00000000..cca1f863
--- /dev/null
+++ b/admin/ai_model.php
@@ -0,0 +1,108 @@
+
+
+
-Tell your admin: Your role does not have admin access.");
+}
+require_once "../includes/header.php";
+require_once "../includes/top_nav.php";
+require_once "includes/side_nav.php";
+require_once "../includes/inc_wrapper.php";
+require_once "../includes/inc_alert_feedback.php";
+require_once "../includes/filter_header.php";
+require_once "../includes/app_version.php";
diff --git a/includes/admin_side_nav.php b/admin/includes/side_nav.php
similarity index 54%
rename from includes/admin_side_nav.php
rename to admin/includes/side_nav.php
index f1b870e0..8e32ef66 100644
--- a/includes/admin_side_nav.php
+++ b/admin/includes/side_nav.php
@@ -1,6 +1,6 @@
\ No newline at end of file
diff --git a/admin/modals/ai/ai_model_edit.php b/admin/modals/ai/ai_model_edit.php
new file mode 100644
index 00000000..87c5bbc2
--- /dev/null
+++ b/admin/modals/ai/ai_model_edit.php
@@ -0,0 +1,90 @@
+
+
+
diff --git a/client/invoices.php b/client/invoices.php
index 654a4bd3..eb2070d4 100644
--- a/client/invoices.php
+++ b/client/invoices.php
@@ -86,7 +86,6 @@ $invoices_sql = mysqli_query($mysqli, "SELECT * FROM invoices WHERE invoice_clie
-
diff --git a/client/login.php b/client/login.php
index e69c6234..d0b6aff8 100644
--- a/client/login.php
+++ b/client/login.php
@@ -10,7 +10,7 @@ require_once '../config.php';
require_once '../functions.php';
-require_once '../includes/get_settings.php';
+require_once '../includes/load_global_settings.php';
if (!isset($_SESSION)) {
// HTTP Only cookies
@@ -57,7 +57,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['login'])) {
} else {
- $sql = mysqli_query($mysqli, "SELECT * FROM users LEFT JOIN contacts ON user_id = contact_user_id WHERE user_email = '$email' AND user_archived_at IS NULL AND user_type = 2 AND user_status = 1 LIMIT 1");
+ $sql = mysqli_query($mysqli, "SELECT * FROM users LEFT JOIN contacts ON user_id = contact_user_id LEFT JOIN clients ON contact_client_id = client_id WHERE user_email = '$email' AND client_archived_at IS NULL AND user_archived_at IS NULL AND user_type = 2 AND user_status = 1 LIMIT 1");
$row = mysqli_fetch_array($sql);
$client_id = intval($row['contact_client_id']);
$user_id = intval($row['user_id']);
diff --git a/client/login_microsoft.php b/client/login_microsoft.php
index 4c8eae65..c182a16d 100644
--- a/client/login_microsoft.php
+++ b/client/login_microsoft.php
@@ -100,7 +100,7 @@ if (isset($_POST['code']) && $_POST['state'] == session_id()) {
$upn = mysqli_real_escape_string($mysqli, $msgraph_response["userPrincipalName"]);
- $sql = mysqli_query($mysqli, "SELECT * FROM users LEFT JOIN contacts ON user_id = contact_user_id WHERE user_email = '$upn' AND user_archived_at IS NULL AND user_type = 2 AND user_status = 1 LIMIT 1");
+ $sql = mysqli_query($mysqli, "SELECT * FROM users LEFT JOIN contacts ON user_id = contact_user_id LEFT JOIN contact_client_id = client_id WHERE user_email = '$upn' AND user_archived_at IS NULL AND client_archived_at IS NULL AND user_type = 2 AND user_status = 1 LIMIT 1");
$row = mysqli_fetch_array($sql);
$client_id = intval($row['contact_client_id']);
$user_id = intval($row['user_id']);
diff --git a/client/login_reset.php b/client/login_reset.php
index 9992f052..a90dda7e 100644
--- a/client/login_reset.php
+++ b/client/login_reset.php
@@ -8,7 +8,7 @@ header("Content-Security-Policy: default-src 'self'");
require_once '../config.php';
require_once '../functions.php';
-require_once '../includes/get_settings.php';
+require_once '../includes/load_global_settings.php';
if (empty($config_smtp_host)) {
@@ -45,7 +45,7 @@ $company_name = sanitizeInput($company_results['company_name']);
$company_phone = sanitizeInput(formatPhoneNumber($company_results['company_phone']));
$company_name_display = $company_results['company_name'];
-// Get settings from get_settings.php and sanitize them
+// Get settings from load_global_settings.php and sanitize them
$config_ticket_from_name = sanitizeInput($config_ticket_from_name);
$config_ticket_from_email = sanitizeInput($config_ticket_from_email);
$config_mail_from_name = sanitizeInput($config_mail_from_name);
diff --git a/client/post.php b/client/post.php
index 03ec1773..4b4b9925 100644
--- a/client/post.php
+++ b/client/post.php
@@ -5,7 +5,7 @@
*/
require_once '../config.php';
-require_once '../includes/get_settings.php';
+require_once '../includes/load_global_settings.php';
require_once '../functions.php';
require_once 'includes/check_login.php';
require_once 'functions.php';
@@ -15,8 +15,9 @@ if (isset($_POST['add_ticket'])) {
$subject = sanitizeInput($_POST['subject']);
$details = mysqli_real_escape_string($mysqli, ($_POST['details']));
$category = intval($_POST['category']);
+ $asset = intval($_POST['asset']);
- // Get settings from get_settings.php
+ // Get settings from load_global_settings.php
$config_ticket_prefix = sanitizeInput($config_ticket_prefix);
$config_ticket_from_name = sanitizeInput($config_ticket_from_name);
$config_ticket_from_email = sanitizeInput($config_ticket_from_email);
@@ -38,7 +39,7 @@ if (isset($_POST['add_ticket'])) {
$new_config_ticket_next_number = $config_ticket_next_number + 1;
mysqli_query($mysqli, "UPDATE settings SET config_ticket_next_number = $new_config_ticket_next_number WHERE company_id = 1");
- mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'Portal', ticket_category = $category, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = $session_user_id, ticket_contact_id = $session_contact_id, ticket_url_key = '$url_key', ticket_client_id = $session_client_id");
+ mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'Portal', ticket_category = $category, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = $session_user_id, ticket_contact_id = $session_contact_id, ticket_asset_id = $asset, ticket_url_key = '$url_key', ticket_client_id = $session_client_id");
$ticket_id = mysqli_insert_id($mysqli);
// Notify agent DL of the new ticket, if populated with a valid email
@@ -67,10 +68,9 @@ if (isset($_POST['add_ticket'])) {
// Custom action/notif handler
customAction('ticket_create', $ticket_id);
- // Logging
logAction("Ticket", "Create", "$session_contact_name created ticket $config_ticket_prefix$ticket_number - $subject from the client portal", $session_client_id, $ticket_id);
- header("Location: ticket.php?id=" . $ticket_id);
+ redirect("ticket.php?id=" . $ticket_id);
}
@@ -81,8 +81,8 @@ if (isset($_POST['add_ticket_comment'])) {
// After stripping bad HTML, check the comment isn't just empty
if (empty($comment)) {
- header("Location: " . $_SERVER["HTTP_REFERER"]);
- exit;
+ flash_alert("You must enter a comment", 'danger');
+ redirect();
}
// Verify the contact has access to the provided ticket ID
@@ -171,16 +171,16 @@ if (isset($_POST['add_ticket_comment'])) {
customAction('ticket_reply_client', $ticket_id);
// Redirect back to original page
- header("Location: " . $_SERVER["HTTP_REFERER"]);
+ redirect();
} else {
// The client does not have access to this ticket
- header("Location: post.php?logout");
- exit();
+ redirect("post.php?logout");
}
}
if (isset($_POST['add_ticket_feedback'])) {
+
$ticket_id = intval($_POST['ticket_id']);
$feedback = sanitizeInput($_POST['add_ticket_feedback']);
@@ -201,16 +201,16 @@ if (isset($_POST['add_ticket_feedback'])) {
customAction('ticket_feedback', $ticket_id);
// Redirect
- header("Location: " . $_SERVER["HTTP_REFERER"]);
+ redirect();
} else {
// The client does not have access to this ticket
- header("Location: post.php?logout");
- exit();
+ redirect("post.php?logout");
}
}
if (isset($_GET['resolve_ticket'])) {
+
$ticket_id = intval($_GET['resolve_ticket']);
// Get ticket details for logging
@@ -228,19 +228,18 @@ if (isset($_GET['resolve_ticket'])) {
// Add reply
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket resolved by $session_contact_name.', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
- // Logging
logAction("Ticket", "Edit", "$session_contact_name marked ticket $ticket_prefix$ticket_number as resolved in the client portal", $session_client_id, $ticket_id);
// Custom action/notif handler
customAction('ticket_resolve', $ticket_id);
- header("Location: ticket.php?id=" . $ticket_id);
+ redirect("ticket.php?id=" . $ticket_id);
} else {
// The client does not have access to this ticket - send them home
- header("Location: index.php");
- exit();
+ redirect("index.php");
}
+
}
if (isset($_GET['reopen_ticket'])) {
@@ -261,22 +260,22 @@ if (isset($_GET['reopen_ticket'])) {
// Add reply
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket reopened by $session_contact_name.', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
- // Logging
logAction("Ticket", "Edit", "$session_contact_name reopend ticket $ticket_prefix$ticket_number in the client portal", $session_client_id, $ticket_id);
// Custom action/notif handler
customAction('ticket_update', $ticket_id);
- header("Location: ticket.php?id=" . $ticket_id);
+ redirect("ticket.php?id=" . $ticket_id);
} else {
// The client does not have access to this ticket - send them home
- header("Location: index.php");
- exit();
+ redirect("index.php");
}
+
}
if (isset($_GET['close_ticket'])) {
+
$ticket_id = intval($_GET['close_ticket']);
// Get ticket details for logging
@@ -294,32 +293,35 @@ if (isset($_GET['close_ticket'])) {
// Add reply
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket closed by $session_contact_name.', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
- // Logging
logAction("Ticket", "Edit", "$session_contact_name closed ticket $ticket_prefix$ticket_number in the client portal", $session_client_id, $ticket_id);
// Custom action/notif handler
customAction('ticket_close', $ticket_id);
- header("Location: ticket.php?id=" . $ticket_id);
+ redirect("ticket.php?id=" . $ticket_id);
+
} else {
// The client does not have access to this ticket - send them home
- header("Location: index.php");
- exit();
+ redirect("index.php");
}
}
if (isset($_GET['logout'])) {
+
setcookie("PHPSESSID", '', time() - 3600, "/");
unset($_COOKIE['PHPSESSID']);
session_unset();
session_destroy();
- header('Location: login.php');
+ redirect('login.php');
+
}
if (isset($_POST['edit_profile'])) {
+
$new_password = $_POST['new_password'];
+
if (!empty($new_password)) {
$password_hash = password_hash($new_password, PASSWORD_DEFAULT);
mysqli_query($mysqli, "UPDATE users SET user_password = '$password_hash' WHERE user_id = $session_user_id");
@@ -327,14 +329,15 @@ if (isset($_POST['edit_profile'])) {
// Logging
logAction("Contact", "Edit", "Client contact $session_contact_name edited their profile/password in the client portal", $session_client_id, $session_contact_id);
}
- header('Location: index.php');
+
+ redirect('index.php');
+
}
if (isset($_POST['add_contact'])) {
if ($session_contact_primary == 0 && !$session_contact_is_technical_contact) {
- header("Location: post.php?logout");
- exit();
+ redirect("post.php?logout");
}
$contact_name = sanitizeInput($_POST['contact_name']);
@@ -346,10 +349,8 @@ if (isset($_POST['add_contact'])) {
// Check the email isn't already in use
$sql = mysqli_query($mysqli, "SELECT user_id FROM users WHERE user_email = '$contact_email'");
if ($sql && mysqli_num_rows($sql) > 0) {
- $_SESSION['alert_type'] = "danger";
- $_SESSION['alert_message'] = "Cannot add contact as that email address is already in use";
- header('Location: contact_add.php');
- exit();
+ flash_alert("Cannot add contact as that email address is already in use", 'danger');
+ redirect('contact_add.php');
}
// Create user account with rand password for the contact
@@ -361,10 +362,12 @@ if (isset($_POST['add_contact'])) {
mysqli_query($mysqli, "INSERT INTO users SET user_name = '$contact_name', user_email = '$contact_email', user_password = '$password_hash', user_auth_method = '$contact_auth_method', user_type = 2");
$contact_user_id = mysqli_insert_id($mysqli);
+
}
// Create contact record
mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '$contact_name', contact_email = '$contact_email', contact_billing = $contact_billing, contact_technical = $contact_technical, contact_client_id = $session_client_id, contact_user_id = $contact_user_id");
+
$contact_id = mysqli_insert_id($mysqli);
// Logging
@@ -372,16 +375,16 @@ if (isset($_POST['add_contact'])) {
customAction('contact_create', $contact_id);
- $_SESSION['alert_message'] = "Contact $contact_name created";
+ flash_alert("Contact $contact_name created");
+
+ redirect('contacts.php');
- header('Location: contacts.php');
}
if (isset($_POST['edit_contact'])) {
if ($session_contact_primary == 0 && !$session_contact_is_technical_contact) {
- header("Location: post.php?logout");
- exit();
+ redirect("post.php?logout");
}
$contact_id = intval($_POST['contact_id']);
@@ -399,10 +402,8 @@ if (isset($_POST['edit_contact'])) {
// Check the email isn't already in use
$sql = mysqli_query($mysqli, "SELECT user_id FROM users WHERE user_email = '$contact_email' AND user_id != $contact_user_id");
if ($sql && mysqli_num_rows($sql) > 0) {
- $_SESSION['alert_type'] = "danger";
- $_SESSION['alert_message'] = "Cannot update contact as that email address is already in use";
- header('Location: contact_edit.php?id=' . $contact_id);
- exit();
+ flash_alert("Cannot update contact as that email address is already in use", 'danger');
+ redirect('contact_edit.php?id=' . $contact_id);
}
// Update Existing User
@@ -420,333 +421,766 @@ if (isset($_POST['edit_contact'])) {
// Update contact
mysqli_query($mysqli, "UPDATE contacts SET contact_name = '$contact_name', contact_email = '$contact_email', contact_billing = $contact_billing, contact_technical = $contact_technical, contact_user_id = $contact_user_id WHERE contact_id = $contact_id AND contact_client_id = $session_client_id AND contact_archived_at IS NULL AND contact_primary = 0");
- // Logging
logAction("Contact", "Edit", "Client contact $session_contact_name edited contact $contact_name in the client portal", $session_client_id, $contact_id);
- $_SESSION['alert_message'] = "Contact $contact_name updated";
+ flash_alert("Contact $contact_name updated");
- header('Location: contacts.php');
+ redirect('contacts.php');
customAction('contact_update', $contact_id);
+
+}
+
+if (isset($_GET['add_payment_by_provider'])) {
+
+ $invoice_id = intval($_GET['invoice_id']);
+ $saved_payment_id = intval($_GET['add_payment_by_provider']);
+
+ // Get invoice details
+ $sql = mysqli_query($mysqli,"SELECT * 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_array($sql);
+ $invoice_number = intval($row['invoice_number']);
+ $invoice_status = sanitizeInput($row['invoice_status']);
+ $invoice_amount = floatval($row['invoice_amount']);
+ $invoice_prefix = sanitizeInput($row['invoice_prefix']);
+ $invoice_number = intval($row['invoice_number']);
+ $invoice_url_key = sanitizeInput($row['invoice_url_key']);
+ $invoice_currency_code = sanitizeInput($row['invoice_currency_code']);
+ $client_id = intval($row['client_id']);
+ $client_name = sanitizeInput($row['client_name']);
+ $contact_name = sanitizeInput($row['contact_name']);
+ $contact_email = sanitizeInput($row['contact_email']);
+ $contact_phone = sanitizeInput(formatPhoneNumber($row['contact_phone'], $row['contact_phone_country_code']));
+ $contact_extension = preg_replace("/[^0-9]/", '',$row['contact_extension']);
+ $contact_mobile = sanitizeInput(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code']));
+
+ // Check to make sure saved payment method belongs to logged in client
+ if ($client_id !== $session_client_id) {
+ flash_alert("Saved Payment method does not belong to you!", 'danger');
+ redirect();
+ }
+
+ // Get ITFlow company details
+ $sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1");
+ $row = mysqli_fetch_array($sql);
+ $company_name = sanitizeInput($row['company_name']);
+ $company_country = sanitizeInput($row['company_country']);
+ $company_address = sanitizeInput($row['company_address']);
+ $company_city = sanitizeInput($row['company_city']);
+ $company_state = sanitizeInput($row['company_state']);
+ $company_zip = sanitizeInput($row['company_zip']);
+ $company_phone = sanitizeInput(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
+ $company_email = sanitizeInput($row['company_email']);
+ $company_website = sanitizeInput($row['company_website']);
+
+ // Sanitize Config vars from get_settings.php
+ $config_invoice_from_name = sanitizeInput($config_invoice_from_name);
+ $config_invoice_from_email = sanitizeInput($config_invoice_from_email);
+
+ // Get Client Payment Details
+ $sql = mysqli_query($mysqli, "SELECT * 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_array($sql);
+
+ $public_key = sanitizeInput($row['payment_provider_public_key']);
+ $private_key = sanitizeInput($row['payment_provider_private_key']);
+ $account_id = intval($row['payment_provider_account']);
+ $expense_category_id = intval($row['payment_provider_expense_category']);
+ $expense_vendor_id = intval($row['payment_provider_expense_vendor']);
+ $expense_percentage_fee = floatval($row['payment_provider_expense_percentage_fee']);
+ $expense_flat_fee = floatval($row['payment_provider_expense_flat_fee']);
+ $payment_provider_client = sanitizeInput($row['payment_provider_client']);
+ $saved_payment_method = sanitizeInput($row['saved_payment_provider_method']);
+ $saved_payment_description = sanitizeInput($row['saved_payment_description']);
+
+ // Sanity checks
+ if (!$payment_provider_client || !$saved_payment_method) {
+ flash_alert("Stripe not enabled or no client card saved", 'error');
+ redirect();
+ } elseif ($invoice_status !== 'Sent' && $invoice_status !== 'Viewed') {
+ flash_alert("Invalid invoice state (draft/partial/paid/not billable)", 'error');
+ redirect();
+ } elseif ($invoice_amount == 0) {
+ flash_alert("Invalid invoice amount", 'error');
+ redirect();
+ }
+
+ // Initialize Stripe
+ require_once __DIR__ . '/../plugins/stripe-php/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($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 = sanitizeInput($payment_intent->id);
+ $pi_date = date('Y-m-d', $payment_intent->created);
+ $pi_amount_paid = floatval(($payment_intent->amount_received / 100));
+ $pi_currency = strtoupper(sanitizeInput($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");
+
+ // Email receipt
+ if (!empty($config_smtp_host)) {
+ $subject = "Payment Received - Invoice $invoice_prefix$invoice_number";
+ $body = "Hello $contact_name,
We have received online payment for the amount of " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . " for invoice $invoice_prefix$invoice_number. Please keep this email as a receipt for your records.
This is a notification that an invoice has been paid in ITFlow. Below is a copy of the receipt sent to the client:-
--------
Hello $contact_name,
We have received online payment for the amount of " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . " for invoice $invoice_prefix$invoice_number. Please keep this email as a receipt for your records.
-- $company_name - Billing Department $config_invoice_from_email $company_phone";
+
+ $data[] = [
+ 'from' => $config_invoice_from_email,
+ 'from_name' => $config_invoice_from_name,
+ 'recipient' => $config_invoice_paid_notification_email,
+ 'recipient_name' => $contact_name,
+ 'subject' => $subject,
+ 'body' => $body,
+ ];
+ }
+
+ $mail = addToMailQueue($data);
+
+ // Email Logging
+ $email_id = mysqli_insert_id($mysqli);
+ mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Payment Receipt sent to mail queue ID: $email_id!', history_invoice_id = $invoice_id");
+ logAction("Invoice", "Payment", "Payment receipt for invoice $invoice_prefix$invoice_number queued to $contact_email Email ID: $email_id", $client_id, $invoice_id);
+ }
+
+ // Log info
+ $extended_log_desc = '';
+ if (!$pi_livemode) {
+ $extended_log_desc = '(DEV MODE)';
+ }
+
+ // Create Stripe payment gateway fee as an expense (if configured)
+ if ($expense_vendor_id > 0 && $expense_category_id > 0) {
+ $gateway_fee = round($invoice_amount * $expense_percentage_fee + $expense_flat_fee, 2);
+ mysqli_query($mysqli,"INSERT INTO expenses SET expense_date = '$pi_date', expense_amount = $gateway_fee, expense_currency_code = '$invoice_currency_code', expense_account_id = $account_id, expense_vendor_id = $expense_vendor_id, expense_client_id = $client_id, expense_category_id = $expense_category_id, expense_description = 'Stripe Transaction for Invoice $invoice_prefix$invoice_number In the Amount of $balance_to_pay', expense_reference = 'Stripe - $pi_id $extended_log_desc'");
+ }
+
+ // Notify/log
+ appNotify("Invoice Paid", "Invoice $invoice_prefix$invoice_number automatically paid", "invoice.php?invoice_id=$invoice_id", $client_id);
+ logAction("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);
+ customAction('invoice_pay', $invoice_id);
+
+ flash_alert("The amount " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . " paid Invoice $invoice_prefix$invoice_number");
+
+ 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");
+
+ logAction("Invoice", "Payment", "Failed online payment amount of invoice $invoice_prefix$invoice_number due to Stripe payment error", $client_id, $invoice_id);
+ flash_alert("Payment failed", 'error');
+
+ redirect();
+ }
+
}
if (isset($_POST['create_stripe_customer'])) {
if ($session_contact_primary == 0 && !$session_contact_is_billing_contact) {
- header("Location: post.php?logout");
- exit();
+ redirect("post.php?logout");
}
- // Get Stripe vars
- $stripe_vars = mysqli_fetch_array(mysqli_query($mysqli, "SELECT config_stripe_enable, config_stripe_publishable, config_stripe_secret FROM settings WHERE company_id = 1"));
- $config_stripe_enable = intval($stripe_vars['config_stripe_enable']);
- $config_stripe_secret = nullable_htmlentities($stripe_vars['config_stripe_secret']);
+ // Get Stripe provider
+ $stripe_provider_result = mysqli_query($mysqli, "
+ SELECT * FROM payment_providers
+ WHERE payment_provider_name = 'Stripe'
+ AND payment_provider_active = 1
+ LIMIT 1
+ ");
- if (!$config_stripe_enable) {
- header("Location: autopay.php");
- exit();
+ $stripe_provider = mysqli_fetch_array($stripe_provider_result);
+ if (!$stripe_provider) {
+ flash_alert("Stripe provider is not configured in the system.", 'danger');
+ redirect("saved_payment_methods.php");
}
- // Include stripe SDK
- require_once '../plugins/stripe-php/init.php';
+ $stripe_provider_id = intval($stripe_provider['payment_provider_id']);
+ $stripe_secret_key = nullable_htmlentities($stripe_provider['payment_provider_private_key']);
- // Get client's StripeID from database (should be none)
- $stripe_client_details = mysqli_fetch_array(mysqli_query($mysqli, "SELECT stripe_id FROM client_stripe WHERE client_id = $session_client_id LIMIT 1"));
- if (!$stripe_client_details) {
+ if (empty($stripe_secret_key)) {
+ flash_alert("Stripe credentials missing. Please contact support.", 'danger');
+ redirect("saved_payment_methods.php");
+ }
+ // Check if client already has a Stripe customer
+ $existing_customer = mysqli_fetch_array(mysqli_query($mysqli, "
+ SELECT payment_provider_client
+ FROM client_payment_provider
+ WHERE client_id = $session_client_id
+ AND payment_provider_id = $stripe_provider_id
+ LIMIT 1
+ "));
+
+ if (!$existing_customer) {
try {
- // Initiate Stripe
- $stripe = new \Stripe\StripeClient($config_stripe_secret);
+ // Initialize Stripe
+ require_once '../plugins/stripe-php/init.php';
+ $stripe = new \Stripe\StripeClient($stripe_secret_key);
- // Create customer
+ // Create new customer in Stripe
$customer = $stripe->customers->create([
'name' => $session_client_name,
'email' => $session_contact_email,
'metadata' => [
'itflow_client_id' => $session_client_id,
- 'consent' => $session_contact_name
+ 'consent_by' => $session_contact_name
]
]);
+ $stripe_customer_id = sanitizeInput($customer->id);
+
+ // Insert customer into client_payment_provider
+ mysqli_query($mysqli, "
+ INSERT INTO client_payment_provider
+ SET client_id = $session_client_id,
+ payment_provider_id = $stripe_provider_id,
+ payment_provider_client = '$stripe_customer_id',
+ client_payment_provider_created_at = NOW()
+ ");
+
+ logAction("Stripe", "Create", "$session_contact_name created Stripe customer for $session_client_name as $stripe_customer_id and authorized future automatic payments", $session_client_id, $session_client_id);
+
+ flash_alert("Stripe customer created. Thank you for your consent.");
+
} catch (Exception $e) {
$error = $e->getMessage();
- error_log("Stripe payment error - encountered exception when creating customer record for $session_client_name: $error");
- logApp("Stripe", "error", "Exception creating customer $session_client_name: $error");
+
+ error_log("Stripe error while creating customer for $session_client_name: $error");
+
+ logApp("Stripe", "error", "Failed to create Stripe customer for $session_client_name: $error");
+
+ flash_alert("An error occurred while creating your Stripe customer. Please try again.", 'danger');
+
}
- // Get & Store customer ID
- $stripe_id = sanitizeInput($customer->id);
-
- mysqli_query($mysqli, "INSERT INTO client_stripe SET client_id = $session_client_id, stripe_id = '$stripe_id'");
-
- // Logging
- logAction("Stripe", "Create", "$session_contact_name created Stripe customer for $session_client_name as $stripe_id and authorised future automatic payments", $session_client_id, $session_client_id);
-
- $_SESSION['alert_message'] = "Stripe customer created, thank you for your consent";
-
} else {
- $_SESSION['alert_type'] = "danger";
- $_SESSION['alert_message'] = "Stripe customer already exists";
+ flash_alert("Stripe customer already exists for your account.", 'danger');
}
- header('Location: autopay.php');
+ redirect('saved_payment_methods.php');
}
if (isset($_GET['create_stripe_checkout'])) {
- // This page is called by the autopay_setup_stripe.js, it returns a checkout session client secret
+ // This page is called by autopay_setup_stripe.js, returns a Checkout Session client_secret
if ($session_contact_primary == 0 && !$session_contact_is_billing_contact) {
- header("Location: post.php?logout");
+ redirect("post.php?logout");
+ }
+
+ // Fetch Stripe provider info
+ $stripe_provider_result = mysqli_query($mysqli, "
+ SELECT * FROM payment_providers
+ WHERE payment_provider_name = 'Stripe'
+ AND payment_provider_active = 1
+ LIMIT 1
+ ");
+
+ $stripe_provider = mysqli_fetch_array($stripe_provider_result);
+ if (!$stripe_provider) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Stripe provider not configured']);
exit();
}
- // Get Stripe vars
- $stripe_vars = mysqli_fetch_array(mysqli_query($mysqli, "SELECT config_stripe_enable, config_stripe_publishable, config_stripe_secret FROM settings WHERE company_id = 1"));
- $config_stripe_enable = intval($stripe_vars['config_stripe_enable']);
- $config_stripe_secret = nullable_htmlentities($stripe_vars['config_stripe_secret']);
+ $stripe_provider_id = intval($stripe_provider['payment_provider_id']);
+ $stripe_secret_key = nullable_htmlentities($stripe_provider['payment_provider_private_key']);
- if (!$config_stripe_enable) {
- header("Location: autopay.php");
+ if (empty($stripe_secret_key)) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Stripe secret key missing']);
exit();
}
- // Client Currency
- $client_currency_details = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT client_currency_code FROM clients WHERE client_id = $session_client_id LIMIT 1"));
- $client_currency = $client_currency_details['client_currency_code'];
+ // Get client currency
+ $client_currency_result = mysqli_query($mysqli, "
+ SELECT client_currency_code
+ FROM clients
+ WHERE client_id = $session_client_id
+ LIMIT 1
+ ");
+ $client_currency_row = mysqli_fetch_assoc($client_currency_result);
+ $client_currency = $client_currency_row['client_currency_code'] ?? 'usd';
- // Define return URL that user is redirected to once payment method is verified by Stripe
+ // Return URL when checkout finishes
$return_url = "https://$config_base_url/client/post.php?stripe_save_card&session_id={CHECKOUT_SESSION_ID}";
try {
- // Initialize stripe
require_once '../plugins/stripe-php/init.php';
- $stripe = new \Stripe\StripeClient($config_stripe_secret);
+ $stripe = new \Stripe\StripeClient($stripe_secret_key);
- // Create checkout session (server side)
+ // Create checkout session
$checkout_session = $stripe->checkout->sessions->create([
'currency' => $client_currency,
'mode' => 'setup',
'ui_mode' => 'embedded',
'return_url' => $return_url,
]);
+
+ echo json_encode(['clientSecret' => $checkout_session->client_secret]);
+
} catch (Exception $e) {
$error = $e->getMessage();
- error_log("Stripe payment error - encountered exception when creating checkout session: $error");
- logApp("Stripe", "error", "Exception creating checkout: $error");
+ error_log("Stripe error creating checkout session: $error");
+ logApp("Stripe", "error", "Exception creating checkout session: $error");
+ http_response_code(500);
+ echo json_encode(['error' => 'Stripe Checkout session failed']);
}
- // Return the client secret to the js script
- echo json_encode(array('clientSecret' => $checkout_session->client_secret));
-
- // No redirect & no point logging this
+ exit;
}
if (isset($_GET['stripe_save_card'])) {
if ($session_contact_primary == 0 && !$session_contact_is_billing_contact) {
- header("Location: post.php?logout");
- exit();
+ redirect("post.php?logout");
}
- // Get Stripe vars
- $stripe_vars = mysqli_fetch_array(mysqli_query($mysqli, "SELECT config_stripe_enable, config_stripe_publishable, config_stripe_secret FROM settings WHERE company_id = 1"));
- $config_stripe_enable = intval($stripe_vars['config_stripe_enable']);
- $config_stripe_secret = nullable_htmlentities($stripe_vars['config_stripe_secret']);
+ // Get Stripe provider
+ $stripe_provider_result = mysqli_query($mysqli, "
+ SELECT * FROM payment_providers
+ WHERE payment_provider_name = 'Stripe'
+ AND payment_provider_active = 1
+ LIMIT 1
+ ");
- if (!$config_stripe_enable) {
- header("Location: autopay.php");
- exit();
+ $stripe_provider = mysqli_fetch_array($stripe_provider_result);
+ if (!$stripe_provider) {
+ flash_alert("Stripe provider not configured.", 'danger');
+ redirect("saved_payment_methods.php");
+ }
+
+ $stripe_provider_id = intval($stripe_provider['payment_provider_id']);
+ $stripe_secret_key = nullable_htmlentities($stripe_provider['payment_provider_private_key']);
+
+ if (empty($stripe_secret_key)) {
+ flash_alert("Stripe credentials missing.", 'danger');
+ redirect("saved_payment_methods.php");
+ }
+
+ // Get client's Stripe customer ID
+ $client_provider_query = mysqli_query($mysqli, "
+ SELECT payment_provider_client
+ FROM client_payment_provider
+ WHERE client_id = $session_client_id
+ AND payment_provider_id = $stripe_provider_id
+ LIMIT 1
+ ");
+ $client_provider = mysqli_fetch_array($client_provider_query);
+ $stripe_customer_id = sanitizeInput($client_provider['payment_provider_client'] ?? '');
+
+ if (empty($stripe_customer_id)) {
+ flash_alert("Stripe customer ID not found for client.", 'danger');
+ redirect("saved_payment_methods.php");
}
// Get session ID from URL
$checkout_session_id = sanitizeInput($_GET['session_id']);
- // Get client's StripeID from database
- $stripe_client_details = mysqli_fetch_array(mysqli_query($mysqli, "SELECT stripe_id FROM client_stripe WHERE client_id = $session_client_id LIMIT 1"));
- $client_stripe_id = sanitizeInput($stripe_client_details['stripe_id']);
-
try {
- // Initialize stripe
require_once '../plugins/stripe-php/init.php';
- $stripe = new \Stripe\StripeClient($config_stripe_secret);
+ $stripe = new \Stripe\StripeClient($stripe_secret_key);
- // Retrieve checkout session
- $checkout_session = $stripe->checkout->sessions->retrieve($checkout_session_id,[]);
-
- // Get setup intent
+ // Retrieve checkout session & setup intent
+ $checkout_session = $stripe->checkout->sessions->retrieve($checkout_session_id, []);
$setup_intent_id = $checkout_session->setup_intent;
-
- // Retrieve the setup intent details
$setup_intent = $stripe->setupIntents->retrieve($setup_intent_id, []);
+ $payment_method_id = sanitizeInput($setup_intent->payment_method);
- // Get the payment method token
- $payment_method = sanitizeInput($setup_intent->payment_method);
+ // Attach the payment method to the Stripe customer
+ $stripe->paymentMethods->attach($payment_method_id, ['customer' => $stripe_customer_id]);
- // Attach the payment method to the client in Stripe
- $stripe->paymentMethods->attach($payment_method, ['customer' => $client_stripe_id]);
+ // Retrieve PM details for logging and UI
+ $payment_method_details = $stripe->paymentMethods->retrieve($payment_method_id, []);
+ $card_brand = sanitizeInput($payment_method_details->card->brand);
+ $last4 = sanitizeInput($payment_method_details->card->last4);
+ $exp_month = sanitizeInput($payment_method_details->card->exp_month);
+ $exp_year = sanitizeInput($payment_method_details->card->exp_year);
+
+ $saved_payment_description = "$card_brand - $last4 | Exp $exp_month/$exp_year";
+
+ // Insert into client_saved_payment_methods
+ mysqli_query($mysqli, "
+ INSERT INTO client_saved_payment_methods
+ SET
+ saved_payment_provider_method = '$payment_method_id',
+ saved_payment_description = '$saved_payment_description',
+ saved_payment_client_id = $session_client_id,
+ saved_payment_provider_id = $stripe_provider_id,
+ saved_payment_created_at = NOW()
+ ");
} catch (Exception $e) {
$error = $e->getMessage();
- error_log("Stripe payment error - encountered exception when adding payment method info: $error");
- logApp("Stripe", "error", "Exception adding payment method: $error");
+ error_log("Stripe error while saving payment method: $error");
+ logApp("Stripe", "error", "Exception saving payment method: $error");
+
+ flash_alert("An error occurred while saving your payment method.", 'danger');
+ redirect("saved_payment_methods.php");
}
- // Update ITFlow
- mysqli_query($mysqli, "UPDATE client_stripe SET stripe_pm = '$payment_method' WHERE client_id = $session_client_id LIMIT 1");
-
- // Get some card/payment method details for the email/logging
- $payment_method_details = $stripe->paymentMethods->retrieve($payment_method);
- $card_type = sanitizeInput($payment_method_details->card->brand);
- $last4 = sanitizeInput($payment_method_details->card->last4);
- $expiry_month = sanitizeInput($payment_method_details->card->exp_month);
- $expiry_year = sanitizeInput($payment_method_details->card->exp_year);
-
- // Format the payment details string (Visa - 4324 | Exp 12/25)
- $stripe_pm_details = "$card_type - $last4 | Exp $expiry_month/$expiry_year";
-
- // Save the formatted payment details into stripe_pm_details
- $update_query = "
- UPDATE client_stripe
- SET stripe_pm_details = '$stripe_pm_details'
- WHERE client_id = $session_client_id LIMIT 1";
- mysqli_query($mysqli, $update_query);
-
- // Send email confirmation
- // Company Details & Settings
- $sql_settings = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1");
+ // Email Confirmation
+ $sql_settings = mysqli_query($mysqli, "
+ SELECT * FROM companies, settings
+ WHERE companies.company_id = settings.company_id
+ AND companies.company_id = 1
+ ");
$row = mysqli_fetch_array($sql_settings);
+
$company_name = sanitizeInput($row['company_name']);
$company_phone = sanitizeInput(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
- $config_smtp_host = $row['config_smtp_host'];
- $config_smtp_port = intval($row['config_smtp_port']);
- $config_smtp_encryption = $row['config_smtp_encryption'];
- $config_smtp_username = $row['config_smtp_username'];
- $config_smtp_password = $row['config_smtp_password'];
- $config_invoice_from_name = sanitizeInput($row['config_invoice_from_name']);
$config_invoice_from_email = sanitizeInput($row['config_invoice_from_email']);
- $config_base_url = sanitizeInput($config_base_url);
+ $config_invoice_from_name = sanitizeInput($row['config_invoice_from_name']);
- if (!empty($config_smtp_host)) {
+ if (!empty($row['config_smtp_host'])) {
$subject = "Payment method saved";
- $body = "Hello $session_contact_name,
We’re writing to confirm that your payment details have been securely stored with Stripe, our trusted payment processor.
By agreeing to save your payment information, you have authorized us to automatically bill your card ($stripe_pm_details) for any future invoices. The payment details you’ve provided are securely stored with Stripe and will be used solely for invoices. We do not have access to your full card details.
You may update or remove your payment information at any time using the portal.
+
+
+ Save card details
+ In order to set up automatic payments, you must create a customer record in Stripe.
+ First, you must authorize Stripe to store your card details for the purpose of automatic payment.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Manage saved payment methods
+
+
+
You currently have no saved payment methods. Please add one below.
+
+
diff --git a/client/ticket_add.php b/client/ticket_add.php
index ceffca1c..6037f5b1 100644
--- a/client/ticket_add.php
+++ b/client/ticket_add.php
@@ -6,6 +6,9 @@
require_once 'includes/inc_all.php';
+// Allow clients to select a related asset when raising a ticket
+$sql_assets = mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_type FROM assets WHERE asset_contact_id = $session_contact_id AND asset_client_id = $session_client_id AND asset_archived_at IS NULL ORDER BY asset_name ASC");
+
?>
@@ -75,6 +78,30 @@ require_once 'includes/inc_all.php';
+
+
+ CURRENT_DATE
- AND domain_expire < CURRENT_DATE + INTERVAL 45 DAY
- ORDER BY domain_expire ASC"
-);
-
-// Get Certificates Expiring
-$sql_certificates_expiring = mysqli_query(
- $mysqli,
- "SELECT * FROM certificates
- WHERE certificate_client_id = $client_id
- AND certificate_expire IS NOT NULL
- AND certificate_archived_at IS NULL
- AND certificate_expire > CURRENT_DATE
- AND certificate_expire < CURRENT_DATE + INTERVAL 45 DAY
- ORDER BY certificate_expire ASC"
-);
-
-// Get Licenses Expiring
-$sql_licenses_expiring = mysqli_query(
- $mysqli,
- "SELECT * FROM software
- WHERE software_client_id = $client_id
- AND software_expire IS NOT NULL
- AND software_archived_at IS NULL
- AND software_expire > CURRENT_DATE
- AND software_expire < CURRENT_DATE + INTERVAL 45 DAY
- ORDER BY software_expire ASC"
-);
-
-// Get Asset Warranties Expiring
-$sql_asset_warranties_expiring = mysqli_query(
- $mysqli,
- "SELECT * FROM assets
- WHERE asset_client_id = $client_id
- AND asset_warranty_expire IS NOT NULL
- AND asset_archived_at IS NULL
- AND asset_warranty_expire > CURRENT_DATE
- AND asset_warranty_expire < CURRENT_DATE + INTERVAL 45 DAY
- ORDER BY asset_warranty_expire ASC"
-);
-
-// Get Assets Retiring 7 Year
-$sql_asset_retire = mysqli_query(
- $mysqli,
- "SELECT * FROM assets
- WHERE asset_client_id = $client_id
- AND asset_install_date IS NOT NULL
- AND asset_archived_at IS NULL
- AND asset_install_date + INTERVAL 7 YEAR > CURRENT_DATE
- AND asset_install_date + INTERVAL 7 YEAR <= CURRENT_DATE + INTERVAL 45 DAY
- ORDER BY asset_install_date ASC"
-);
-
-/*
- * EXPIRED ITEMS
- */
-
-// Get Domains Expired
-$sql_domains_expired = mysqli_query(
- $mysqli,
- "SELECT * FROM domains
- WHERE domain_client_id = $client_id
- AND domain_expire IS NOT NULL
- AND domain_archived_at IS NULL
- AND domain_expire < CURRENT_DATE
- ORDER BY domain_expire ASC"
-);
-
-// Get Certificates Expired
-$sql_certificates_expired = mysqli_query(
- $mysqli,
- "SELECT * FROM certificates
- WHERE certificate_client_id = $client_id
- AND certificate_expire IS NOT NULL
- AND certificate_archived_at IS NULL
- AND certificate_expire < CURRENT_DATE
- ORDER BY certificate_expire ASC"
-);
-
-// Get Licenses Expired
-$sql_licenses_expired = mysqli_query(
- $mysqli,
- "SELECT * FROM software
- WHERE software_client_id = $client_id
- AND software_expire IS NOT NULL
- AND software_archived_at IS NULL
- AND software_expire < CURRENT_DATE
- ORDER BY software_expire ASC"
-);
-
-// Get Asset Warranties Expired
-$sql_asset_warranties_expired = mysqli_query(
- $mysqli,
- "SELECT * FROM assets
- WHERE asset_client_id = $client_id
- AND asset_warranty_expire IS NOT NULL
- AND asset_archived_at IS NULL
- AND asset_warranty_expire < CURRENT_DATE
- ORDER BY asset_warranty_expire ASC"
-);
-
-// Get Retired Assets
-$sql_asset_retired = mysqli_query(
- $mysqli,
- "SELECT * FROM assets
- WHERE asset_client_id = $client_id
- AND asset_install_date IS NOT NULL
- AND asset_archived_at IS NULL
- AND asset_install_date + INTERVAL 7 YEAR < CURRENT_DATE -- Assets retired (installed more than 7 years ago)
- ORDER BY asset_install_date ASC"
-);
-
-
-?>
-
-
";
- require_once 'includes/guest_footer.php';
- error_log("Stripe payment error - disabled. Check payments are enabled, Expense account is set, Stripe publishable and secret keys are configured.");
- exit();
-}
// Show payment form
-// Users are directed to this page with the invoice_id and url_key params to make a payment
if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent'])) {
$invoice_url_key = sanitizeInput($_GET['url_key']);
- $invoice_id = intval($_GET['invoice_id']);
+ $invoice_id = intval($_GET['invoice_id']);
// Query invoice details
$sql = mysqli_query(
$mysqli,
"SELECT * 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 != 'Draft'
- AND invoice_status != 'Paid'
- AND invoice_status != 'Cancelled'
- LIMIT 1"
+ 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 we have a valid invoice
+ // 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 'includes/guest_footer.php';
- error_log("Stripe payment error - Invoice with ID $invoice_id is unknown/not eligible to be paid.");
+ error_log("Stripe payment error - Invoice with ID $invoice_id not found or not eligible.");
exit();
}
- // Process invoice, client and company details/settings
$row = mysqli_fetch_array($sql);
- $invoice_id = intval($row['invoice_id']);
- $invoice_prefix = nullable_htmlentities($row['invoice_prefix']);
- $invoice_number = intval($row['invoice_number']);
- $invoice_status = nullable_htmlentities($row['invoice_status']);
- $invoice_date = nullable_htmlentities($row['invoice_date']);
- $invoice_due = nullable_htmlentities($row['invoice_due']);
- $invoice_discount = floatval($row['invoice_discount_amount']);
- $invoice_amount = floatval($row['invoice_amount']);
+ $invoice_id = intval($row['invoice_id']);
+ $invoice_prefix = nullable_htmlentities($row['invoice_prefix']);
+ $invoice_number = intval($row['invoice_number']);
+ $invoice_status = nullable_htmlentities($row['invoice_status']);
+ $invoice_date = nullable_htmlentities($row['invoice_date']);
+ $invoice_due = nullable_htmlentities($row['invoice_due']);
+ $invoice_discount = floatval($row['invoice_discount_amount']);
+ $invoice_amount = floatval($row['invoice_amount']);
$invoice_currency_code = nullable_htmlentities($row['invoice_currency_code']);
- $client_id = intval($row['client_id']);
- $client_name = nullable_htmlentities($row['client_name']);
-
- $sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1");
- $row = mysqli_fetch_array($sql);
- $company_locale = nullable_htmlentities($row['company_locale']);
+ $client_id = intval($row['client_id']);
+ $client_name = nullable_htmlentities($row['client_name']);
- // Add up all the payments for the invoice and get the total amount paid to the invoice
+ // Company info for currency formatting, etc
+ $sql_company = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1");
+ $company_row = mysqli_fetch_array($sql_company);
+ $company_locale = nullable_htmlentities($company_row['company_locale']);
+ $config_base_url = nullable_htmlentities($company_row['company_base_url'] ?? ''); // You might want to pull from settings if needed
+
+ // 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");
- $row = mysqli_fetch_array($sql_amount_paid);
- $amount_paid = floatval($row['amount_paid']);
- $balance_to_pay = $invoice_amount - $amount_paid;
-
- //Round balance to pay to 2 decimal places
- $balance_to_pay = round($balance_to_pay, 2);
+ $amount_paid = floatval(mysqli_fetch_array($sql_amount_paid)['amount_paid']);
+ $balance_to_pay = round($invoice_amount - $amount_paid, 2);
// Get invoice items
$sql_invoice_items = mysqli_query($mysqli, "SELECT * FROM invoice_items WHERE item_invoice_id = $invoice_id ORDER BY item_id ASC");
- // Set Currency Formatting
+ // Currency formatting
$currency_format = numfmt_create($company_locale, NumberFormatter::CURRENCY);
?>
-
+
-
-
-
+
@@ -161,12 +135,10 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent
-
+
-
-
-
+
-
-
-
client_secret !== $pi_cs) {
@@ -208,13 +172,11 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent
} elseif ($pi_obj->status !== "succeeded") {
exit(WORDING_PAYMENT_FAILED);
} elseif ($pi_obj->amount !== $pi_obj->amount_received) {
- // The invoice wasn't paid in full
- // this should be flagged for manual review as would indicate something weird happening
error_log("Stripe payment error - payment amount does not match amount paid for $pi_id");
exit(WORDING_PAYMENT_FAILED);
}
- // Get details from PI
+ // 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);
@@ -226,20 +188,17 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent
$invoice_sql = mysqli_query(
$mysqli,
"SELECT * 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 != 'Draft'
- AND invoice_status != 'Paid'
- AND invoice_status != 'Cancelled'
- LIMIT 1"
+ 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 $invoice_id is unknown/not eligible to be paid. PI $pi_id");
+ error_log("Stripe payment error - Invoice with ID $pi_invoice_id is unknown/not eligible. PI $pi_id");
exit(WORDING_PAYMENT_FAILED);
}
- // Invoice exists - get details
$row = mysqli_fetch_array($invoice_sql);
$invoice_id = intval($row['invoice_id']);
$invoice_prefix = sanitizeInput($row['invoice_prefix']);
@@ -251,48 +210,35 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent
$client_name = sanitizeInput($row['client_name']);
$contact_name = sanitizeInput($row['contact_name']);
$contact_email = sanitizeInput($row['contact_email']);
-
+
$sql_company = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1");
$row = mysqli_fetch_array($sql_company);
-
$company_name = sanitizeInput($row['company_name']);
$company_phone = sanitizeInput(formatPhoneNumber($row['company_phone']));
$company_locale = sanitizeInput($row['company_locale']);
- // Set Currency Formatting
$currency_format = numfmt_create($company_locale, NumberFormatter::CURRENCY);
- // Add up all the payments for the invoice and get the total amount paid to the invoice already (if any)
$sql_amount_paid_previously = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id");
- $row = mysqli_fetch_array($sql_amount_paid_previously);
- $amount_paid_previously = $row['amount_paid'];
+ $amount_paid_previously = floatval(mysqli_fetch_array($sql_amount_paid_previously)['amount_paid']);
$balance_to_pay = $invoice_amount - $amount_paid_previously;
- // Check to see if Expense Fields are configured to create Stripe payment expense
- if ($config_stripe_expense_vendor > 0 && $config_stripe_expense_category > 0) {
- // Calculate gateway expense fee
- $gateway_fee = round($balance_to_pay * $config_stripe_percentage_fee + $config_stripe_flat_fee, 2);
-
- // Add Expense
- mysqli_query($mysqli,"INSERT INTO expenses SET expense_date = '$pi_date', expense_amount = $gateway_fee, expense_currency_code = '$invoice_currency_code', expense_account_id = $config_stripe_account, expense_vendor_id = $config_stripe_expense_vendor, expense_client_id = $client_id, expense_category_id = $config_stripe_expense_category, expense_description = 'Stripe Transaction for Invoice $invoice_prefix$invoice_number In the Amount of $balance_to_pay', expense_reference = 'Stripe - $pi_id'");
+ // Stripe expense
+ if ($stripe_expense_vendor > 0 && $stripe_expense_category > 0) {
+ $gateway_fee = round($balance_to_pay * $stripe_percentage_fee + $stripe_flat_fee, 2);
+ mysqli_query($mysqli, "INSERT INTO expenses SET expense_date = '$pi_date', expense_amount = $gateway_fee, expense_currency_code = '$invoice_currency_code', expense_account_id = $stripe_account, expense_vendor_id = $stripe_expense_vendor, expense_client_id = $client_id, expense_category_id = $stripe_expense_category, expense_description = 'Stripe Transaction for Invoice $invoice_prefix$invoice_number In the Amount of $balance_to_pay', expense_reference = 'Stripe - $pi_id'");
}
- // Round balance to pay to 2 decimal places
- $balance_to_pay = round($balance_to_pay, 2);
-
- // Sanity check that the amount paid is exactly the invoice outstanding balance
if (intval($balance_to_pay) !== intval($pi_amount_paid)) {
error_log("Stripe payment error - Invoice balance does not match amount paid for $pi_id");
exit(WORDING_PAYMENT_FAILED);
}
- // Apply payment
-
// 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 = $config_stripe_account, payment_method = 'Stripe', payment_reference = 'Stripe - $pi_id', payment_invoice_id = $invoice_id");
+ 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) - $ip - $os - $browser', history_invoice_id = $invoice_id");
// Notify
@@ -300,30 +246,20 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent
customAction('invoice_pay', $invoice_id);
- // Logging
$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");
-
-
- // Send email receipt
+ // Email Receipt
$sql_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1");
- $row = mysqli_fetch_array($sql_settings);
+ $settings = mysqli_fetch_array($sql_settings);
- $config_smtp_host = $row['config_smtp_host'];
- $config_smtp_port = intval($row['config_smtp_port']);
- $config_smtp_encryption = $row['config_smtp_encryption'];
- $config_smtp_username = $row['config_smtp_username'];
- $config_smtp_password = $row['config_smtp_password'];
- $config_invoice_from_name = sanitizeInput($row['config_invoice_from_name']);
- $config_invoice_from_email = sanitizeInput($row['config_invoice_from_email']);
- $config_invoice_paid_notification_email = sanitizeInput($row['config_invoice_paid_notification_email']);
-
- $config_base_url = sanitizeInput($config_base_url);
+ $config_smtp_host = $settings['config_smtp_host'];
+ $config_invoice_from_name = sanitizeInput($settings['config_invoice_from_name']);
+ $config_invoice_from_email = sanitizeInput($settings['config_invoice_from_email']);
+ $config_invoice_paid_notification_email = sanitizeInput($settings['config_invoice_paid_notification_email']);
if (!empty($config_smtp_host)) {
$subject = "Payment Received - Invoice $invoice_prefix$invoice_number";
@@ -339,36 +275,29 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent
'body' => $body,
]
];
-
-
- // Email the internal notification address too
+ // Internal notification
if (!empty($config_invoice_paid_notification_email)) {
- $subject = "Payment Received - $client_name - Invoice $invoice_prefix$invoice_number";
- $body = "Hello,
This is a notification that an invoice has been paid in ITFlow. Below is a copy of the receipt sent to the client:-
--------
Hello $contact_name,
We have received online payment for the amount of " . $pi_currency . $pi_amount_paid . " for invoice $invoice_prefix$invoice_number. Please keep this email as a receipt for your records.
~ $company_name - Billing $config_invoice_from_email $company_phone";
-
+ $subject_internal = "Payment Received - $client_name - Invoice $invoice_prefix$invoice_number";
+ $body_internal = "This is a notification that an invoice has been paid in ITFlow. Below is a copy of the receipt sent to the client:-