Files
itflow/cron/nightly_tasks.php
johnnyq 422088fb60 Invoices, quotes and recurring invoices makeover stage 2: per user, per device and per license recurring lines that take their quantity from the client's contacts (all or tagged), devices (ticked types or tagged, lost/stolen/retired left out) or a license's seats (in use or bought), counted again for every invoice with an optional contract minimum, set from a Counted choice on the add row or the full Quantity from section in Edit Line with a live count, marked on each line and listed in a new Seats card with Update now when the client has changed, quarterly frequency alongside monthly and yearly counted as a third in MRR on the dashboard, client pages, client portal and reports, period words {month} {quarter} {year} {period} filled into scope, notes and lines, schedule card showing the period the next invoice covers, recurring_invoice_anchor_day so month-end schedules return to the 31st after a short month, nightly run and Invoice Now consolidated into recurringInvoiceGenerate() which claims the run so it is billed once, refreshes seat counts and takes taxes at current rates, nightly run now bills runs missed by up to 7 days, Invoice Now bills the next run early and moves the schedule on from that run instead of restarting from today, recurring email goes to non-archived primary and billing contacts once per address and stays a draft when outgoing mail is not set up. Fixes portal recurring card ignoring yearly-only clients and counting archived ones, edit recurring invoice resetting the billing day. Adds DB 2.9.1, recurring_invoice_sync and recurring_seat_count handlers, itflowBillingSeatForm(), and updates the 26.11 changelog
2026-10-16 13:09:59 -04:00

1155 lines
59 KiB
PHP

<?php
/*
* ITFlow - Nightly tasks
*
* Everything that runs once a day: clean-up, recurring invoices and tickets, reminders,
* autopay, telemetry, the update check. Run by cron/cron.php at 03:00; can also be run
* directly at any time.
*/
// Set working directory to the directory this cron script lives at.
chdir(dirname(__FILE__));
// Ensure we're running from command line
if (php_sapi_name() !== 'cli') {
die("This script must be run from the command line.\n");
}
// Prevent overlapping runs of this script
$cron_lock_script = __FILE__;
require_once "includes/cron_lock.php";
require_once "../config.php";
// Set Timezone
require_once "../includes/inc_set_timezone.php";
require_once "../functions.php";
$sql_companies = mysqli_query($mysqli, "SELECT company_city, company_country, company_currency, company_email, company_locale,
company_name, company_phone, company_phone_country_code, company_state, company_website,
config_enable_alert_domain_expire, config_enable_cron, config_invoice_from_email,
config_invoice_from_name, config_invoice_late_fee_enable, config_invoice_late_fee_percent,
config_invoice_prefix, config_log_retention, config_login_remember_me_expire,
config_mail_from_email, config_mail_from_name, config_module_enable_accounting,
config_module_enable_itdoc, config_module_enable_ticketing,
config_recurring_auto_send_invoice, config_send_invoice_reminders, config_smtp_encryption,
config_smtp_host, config_smtp_password, config_smtp_port, config_smtp_provider,
config_smtp_username, config_telemetry, config_theme, config_ticket_autoclose_hours,
config_ticket_client_general_notifications, config_ticket_email_parse,
config_ticket_from_email, config_ticket_from_name,
config_ticket_new_ticket_notification_email, config_ticket_prefix,
config_whitelabel_enabled, config_whitelabel_key FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1");
$row = mysqli_fetch_assoc($sql_companies);
// Company Details
$company_name = escapeSql($row['company_name']);
$company_phone = escapeSql(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
$company_email = escapeSql($row['company_email']);
$company_website = escapeSql($row['company_website']);
$company_city = escapeSql($row['company_city']);
$company_state = escapeSql($row['company_state']);
$company_country = escapeSql($row['company_country']);
$company_locale = escapeSql($row['company_locale']);
$company_currency = escapeSql($row['company_currency']);
// Company Settings
$config_enable_cron = intval($row['config_enable_cron']);
$config_invoice_prefix = escapeSql($row['config_invoice_prefix']);
$config_invoice_from_email = escapeSql($row['config_invoice_from_email']);
$config_invoice_from_name = escapeSql($row['config_invoice_from_name']);
$config_invoice_late_fee_enable = intval($row['config_invoice_late_fee_enable']);
$config_invoice_late_fee_percent = floatval($row['config_invoice_late_fee_percent']);
// Mail Settings
$config_smtp_provider = escapeSql($row['config_smtp_provider']);
$config_smtp_host = $row['config_smtp_host'];
$config_smtp_username = $row['config_smtp_username'];
$config_smtp_password = $row['config_smtp_password'];
$config_smtp_port = intval($row['config_smtp_port']);
$config_smtp_encryption = $row['config_smtp_encryption'];
$config_mail_from_email = escapeSql($row['config_mail_from_email']);
$config_mail_from_name = escapeSql($row['config_mail_from_name']);
$config_recurring_auto_send_invoice = intval($row['config_recurring_auto_send_invoice']);
// Tickets
$config_ticket_prefix = escapeSql($row['config_ticket_prefix']);
$config_ticket_from_name = escapeSql($row['config_ticket_from_name']);
$config_ticket_from_email = escapeSql($row['config_ticket_from_email']);
$config_ticket_client_general_notifications = intval($row['config_ticket_client_general_notifications']);
$config_ticket_autoclose_hours = intval($row['config_ticket_autoclose_hours']);
$config_ticket_new_ticket_notification_email = escapeSql($row['config_ticket_new_ticket_notification_email']);
// Get Config for Telemetry
$config_theme = $row['config_theme'];
$config_ticket_email_parse = intval($row['config_ticket_email_parse']);
$config_module_enable_itdoc = intval($row['config_module_enable_itdoc']);
$config_module_enable_ticketing = intval($row['config_module_enable_ticketing']);
$config_module_enable_accounting = intval($row['config_module_enable_accounting']);
$config_telemetry = intval($row['config_telemetry']);
// Alerts
$config_enable_alert_domain_expire = intval($row['config_enable_alert_domain_expire']);
$config_send_invoice_reminders = intval($row['config_send_invoice_reminders']);
// Remember-me Token Expiry
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
// Log retention
$config_log_retention = intval($row['config_log_retention']);
// Set Currency Format
$currency_format = numfmt_create($company_locale, NumberFormatter::CURRENCY);
// White label
$config_whitelabel_enabled = intval($row['config_whitelabel_enabled']);
$config_whitelabel_key = $row['config_whitelabel_key'];
// Check cron is enabled
if ($config_enable_cron == 0) {
cronJobStop("Cron: is not enabled -- Quitting..");
}
/*
* Whether cron has already recorded doing something to an invoice today, judged by the
* history rows this script writes. The overdue and autopay queries below are day-matched -
* they select the same invoices on every run of a given day - so each action that emails a
* client or changes money checks here first. This is what makes a second run in one day
* (Run Now after the 3am pass) safe. Named for this script per the shared-process rule in
* CONTRIBUTING.
*/
function cronInvoiceHistoryToday(int $invoice_id, string $description_prefix): bool
{
global $mysqli;
$description_prefix = escapeSql($description_prefix);
$sql = mysqli_query($mysqli, "SELECT history_id FROM history
WHERE history_invoice_id = $invoice_id
AND history_description LIKE '$description_prefix%'
AND history_created_at >= CURDATE()
LIMIT 1");
return mysqli_num_rows($sql) > 0;
}
/*
* ###############################################################################################################
* STARTUP ACTIONS
* ###############################################################################################################
*/
//Logging
logApp("Cron", "info", "Cron Started");
/*
* ###############################################################################################################
* CLEAN UP (OLD) DATA
* ###############################################################################################################
*/
// Clean-up ticket views table used for collision detection
mysqli_query($mysqli, "TRUNCATE TABLE ticket_views");
// Shared links: wipe the secrets of the ones that ended, and forget them
// after SHARE_KEEP_DAYS - until then they stay listed, with who opened them
sharePrune();
// Invalidate any password reset links
mysqli_query($mysqli, "UPDATE users SET user_password_reset_token = NULL WHERE user_archived_at IS NULL");
mysqli_query($mysqli, "UPDATE users SET user_password_reset_token = NULL"); // TODO: Make this 'expired' tokens only when we actually use expiry
// Clean-up old dismissed notifications
mysqli_query($mysqli, "DELETE FROM notifications WHERE notification_dismissed_at < CURDATE() - INTERVAL 90 DAY");
// Clean-up mail queue
mysqli_query($mysqli, "DELETE FROM email_queue WHERE email_queued_at < CURDATE() - INTERVAL 90 DAY");
// Clean-up old remember me tokens
mysqli_query($mysqli, "DELETE FROM remember_tokens WHERE remember_token_created_at < CURDATE() - INTERVAL $config_login_remember_me_expire DAY");
// Cleanup old backups, and reconcile rows whose file is gone against files with no row.
// Retention lives here rather than in cron/backup.php so a failed backup run can never
// delete the archive it was supposed to replace.
backupRunRetention($mysqli);
// Cleanup old audit logs
mysqli_query($mysqli, "DELETE FROM logs WHERE log_created_at < CURDATE() - INTERVAL $config_log_retention DAY");
// Cleanup old app/debug logs
mysqli_query($mysqli, "DELETE FROM app_logs WHERE app_log_created_at < CURDATE() - INTERVAL $config_log_retention DAY");
// Cleanup old auth logs
mysqli_query($mysqli, "DELETE FROM auth_logs WHERE auth_log_created_at < CURDATE() - INTERVAL $config_log_retention DAY");
// CLeanup old domain history
$sql = mysqli_query($mysqli, "SELECT domain_id FROM domains");
while ($row = mysqli_fetch_assoc($sql)) {
$domain_id = intval($row['domain_id']);
mysqli_query($mysqli, "
DELETE FROM domain_history
WHERE domain_history_id NOT IN (
SELECT domain_history_id FROM (
SELECT domain_history_id FROM domain_history
WHERE domain_history_domain_id = $domain_id
ORDER BY domain_history_modified_at DESC
LIMIT 25
) AS recent_entries
) AND domain_history_domain_id = $domain_id
");
}
// Logging
// logAudit("Cron", "Task", "Cron cleaned up old data");
/*
* ###############################################################################################################
* ACTION DATA
* ###############################################################################################################
*/
// Whitelabel - Disable if expired/invalid
if ($config_whitelabel_enabled && !validateWhitelabelKey($config_whitelabel_key)) {
mysqli_query($mysqli, "UPDATE settings SET config_whitelabel_enabled = 0, config_whitelabel_key = '' WHERE company_id = 1");
appNotify("Settings", "White-labelling was disabled due to expired/invalid key", "/admin/settings_modules.php");
}
// GET NOTIFICATIONS
// DOMAINS EXPIRING
if ($config_enable_alert_domain_expire == 1) {
$domainAlertArray = [1,7,45];
foreach ($domainAlertArray as $day) {
//Get Domains Expiring
$sql = mysqli_query(
$mysqli,
"SELECT client_id, client_name, domain_expire, domain_auto_renew, domain_id, domain_name FROM domains
LEFT JOIN clients ON domain_client_id = client_id
WHERE domain_expire IS NOT NULL AND domain_expire = CURDATE() + INTERVAL $day DAY
AND domain_archived_at IS NULL AND client_archived_at IS NULL"
);
while ($row = mysqli_fetch_assoc($sql)) {
$domain_id = intval($row['domain_id']);
$domain_name = escapeSql($row['domain_name']);
$domain_expire = escapeSql($row['domain_expire']);
$client_id = intval($row['client_id']);
$client_name = escapeSql($row['client_name']);
// Auto-renew still fails on an expired card, so it is said rather than skipped
$domain_renewal = intval($row['domain_auto_renew']) ? ' - set to auto-renew, check it goes through' : '';
appNotify("Domain Expiring", "Domain $domain_name for $client_name will expire in $day Days on $domain_expire$domain_renewal", "/agent/domains.php?client_id=$client_id&open=$domain_id", $client_id);
}
}
// Logging
// logAudit("Cron", "Task", "Cron created notifications for domains expiring");
}
// CERTIFICATES EXPIRING
// A month and a half out, then a week, then the day before. A short-lived
// certificate (Let's Encrypt and friends) renews itself with about 30 days
// left, so its 45-day mark is routine - only the week and the day are said,
// and by then it should already have renewed.
$certificateAlertArray = [1,7,45];
foreach ($certificateAlertArray as $day) {
//Get Certs Expiring
$sql = mysqli_query(
$mysqli,
"SELECT certificate_domain, certificate_expire, certificate_issued_at, certificate_id, certificate_name,
client_id, client_name FROM certificates
LEFT JOIN clients ON certificate_client_id = client_id
WHERE certificate_expire = CURDATE() + INTERVAL $day DAY
AND certificate_archived_at IS NULL AND client_archived_at IS NULL"
);
while ($row = mysqli_fetch_assoc($sql)) {
$certificate_id = intval($row['certificate_id']);
$certificate_name = escapeSql($row['certificate_name']);
$certificate_expire = escapeSql($row['certificate_expire']);
$client_id = intval($row['client_id']);
$client_name = escapeSql($row['client_name']);
$short_lived = certificateShortLived($row);
if ($day == 45 && $short_lived) {
echo "Not raising a notification for short-lived certificate $certificate_name expiring in 45 days\n";
continue;
}
$renewal = $short_lived ? ' - it renews itself, so check why it has not' : '';
appNotify("Certificate Expiring", "Certificate $certificate_name for $client_name will expire in $day day(s) on $certificate_expire$renewal", "/agent/certificates.php?client_id=$client_id&open=$certificate_id", $client_id);
}
}
// Logging
// logAudit("Cron", "Task", "Cron created notifications for certificates expiring");
// Asset Warranties Expiring
$warranty_alert_array = [1,7,45];
foreach ($warranty_alert_array as $day) {
//Get Asset Warranty Expiring
$sql = mysqli_query(
$mysqli,
"SELECT asset_id, asset_name, asset_warranty_expire, client_id, client_name FROM assets
LEFT JOIN clients ON asset_client_id = client_id
WHERE asset_warranty_expire = CURDATE() + INTERVAL $day DAY"
);
while ($row = mysqli_fetch_assoc($sql)) {
$asset_id = intval($row['asset_id']);
$asset_name = escapeSql($row['asset_name']);
$asset_warranty_expire = escapeSql($row['asset_warranty_expire']);
$client_id = intval($row['client_id']);
$client_name = escapeSql($row['client_name']);
appNotify("Asset Warranty Expiring", "Asset $asset_name warranty for $client_name will expire in $day Days on $asset_warranty_expire", "/agent/assets.php?client_id=$client_id", $client_id);
}
}
// Logging
// logAudit("Cron", "Task", "Cron created notifications for asset warranties expiring");
// Services due for review - said once, the day it falls due
$sql = mysqli_query($mysqli, "SELECT service_id, service_name, service_importance, client_id, client_name FROM services
JOIN clients ON service_client_id = client_id
WHERE service_review_due = CURDATE() AND service_archived_at IS NULL AND client_archived_at IS NULL");
while ($row = mysqli_fetch_assoc($sql)) {
$service_id = intval($row['service_id']);
$service_name = escapeSql($row['service_name']);
$client_id = intval($row['client_id']);
$client_name = escapeSql($row['client_name']);
appNotify("Service Review", "Service $service_name for $client_name (" . escapeSql($row['service_importance']) . " importance) is due for review today", "/agent/" . serviceUrl($service_id, $client_id), $client_id);
}
// Notify of New Tickets
// Get Ticket Pending Assignment
$sql_tickets_pending_assignment = mysqli_query($mysqli,"SELECT ticket_id FROM tickets WHERE ticket_status = 1");
$tickets_pending_assignment = mysqli_num_rows($sql_tickets_pending_assignment);
if ($tickets_pending_assignment > 0) {
appNotify("Pending Tickets", "There are $tickets_pending_assignment new tickets pending assignment", "/agent/tickets.php?status=New");
// Logging
logApp("Cron", "info", "Cron created notifications for new tickets that are pending assignment");
}
// Recurring tickets - each one due today, or missed (a night the cron didn't
// run), raises one ticket and moves on to its next run after today. Paused ones
// raise nothing. recurringTicketRunNow() (functions/ticket.php) is the same code
// the Raise now button runs.
$sql_recurring_tickets = mysqli_query($mysqli, "SELECT recurring_ticket_id, recurring_ticket_next_run, recurring_ticket_frequency, recurring_ticket_start_date
FROM recurring_tickets WHERE recurring_ticket_next_run <= CURDATE() AND recurring_ticket_paused_at IS NULL");
$recurring_tickets_raised = 0;
while ($row = mysqli_fetch_assoc($sql_recurring_tickets)) {
if (recurringTicketRunNow($row, 'Cron')) {
$recurring_tickets_raised++;
}
}
if ($recurring_tickets_raised) {
logApp("Cron", "info", "Cron raised $recurring_tickets_raised recurring ticket(s)");
}
// Logging
// logAudit("Cron", "Task", "Cron created sent out recurring tickets");
// TICKET RESOLUTION/CLOSURE PROCESS
// Changes tickets status from 'Resolved' >> 'Closed' after a defined interval
$sql_resolved_tickets_to_close = mysqli_query(
$mysqli,
"SELECT ticket_assigned_to, ticket_client_id, ticket_id, ticket_number, ticket_prefix,
ticket_status, ticket_subject FROM tickets
WHERE ticket_status = 4
AND ticket_updated_at < NOW() - INTERVAL $config_ticket_autoclose_hours HOUR"
);
while ($row = mysqli_fetch_assoc($sql_resolved_tickets_to_close)) {
$ticket_id = $row['ticket_id'];
$ticket_prefix = escapeSql($row['ticket_prefix']);
$ticket_number = intval($row['ticket_number']);
$ticket_subject = escapeSql($row['ticket_subject']);
$ticket_status = escapeSql($row['ticket_status']);
$ticket_assigned_to = escapeSql($row['ticket_assigned_to']);
$client_id = intval($row['ticket_client_id']);
mysqli_query($mysqli,"UPDATE tickets SET ticket_status = 5, ticket_closed_at = NOW(), ticket_closed_by = $ticket_assigned_to WHERE ticket_id = $ticket_id");
syncTicketSlaClock($ticket_id);
//Logging
logTicketHistory($ticket_id, "Automatically closed after $config_ticket_autoclose_hours hours with no activity");
logAudit("Ticket", "Closed", "$ticket_prefix$ticket_number auto closed", $client_id, $ticket_id);
triggerCustomAction('ticket_close', $ticket_id);
//TODO: Add client notifs if $config_ticket_client_general_notifications is on
}
if ($config_send_invoice_reminders == 1) {
// PAST DUE INVOICE Notifications
$invoiceAlertArray = [1,30,60,90,120,150,180,210,240,270,300,330,360,390,420,450,480,510,540,570,590,620,650,680,710,740];
foreach ($invoiceAlertArray as $day) {
$sql = mysqli_query(
$mysqli,
"SELECT client_id, client_name, contact_email, contact_name, invoice_amount, invoice_currency_code,
invoice_date, invoice_due, invoice_id, invoice_number, invoice_prefix, invoice_status,
invoice_url_key FROM invoices
LEFT JOIN clients ON invoice_client_id = client_id
LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1
WHERE invoice_status != 'Draft'
AND invoice_status != 'Paid'
AND invoice_status != 'Cancelled'
AND invoice_status != 'Non-Billable'
AND DATE_ADD(invoice_due, INTERVAL $day DAY) = CURDATE()
ORDER BY invoice_number DESC"
);
while ($row = mysqli_fetch_assoc($sql)) {
$invoice_id = intval($row['invoice_id']);
$invoice_prefix = escapeSql($row['invoice_prefix']);
$invoice_number = intval($row['invoice_number']);
$invoice_status = escapeSql($row['invoice_status']);
$invoice_date = escapeSql($row['invoice_date']);
$invoice_due = escapeSql($row['invoice_due']);
$invoice_url_key = escapeSql($row['invoice_url_key']);
$invoice_amount = floatval($row['invoice_amount']);
$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']);
// Sum payments already applied, derive the real balance owed
$sql_paid = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id");
$paid_row = mysqli_fetch_assoc($sql_paid);
$amount_paid = floatval($paid_row['amount_paid']);
$invoice_balance = $invoice_amount - $amount_paid;
// Nothing actually owed (e.g. paid in full but status lagging) - skip
if ($invoice_balance <= 0) {
continue;
}
// Late Charges - at most one per invoice per day, or a second run of this
// script stacks another fee on the already-inflated balance
if ($config_invoice_late_fee_enable == 1 && $day > 1 && !cronInvoiceHistoryToday($invoice_id, 'Cron applied a late fee')) {
$todays_date = date('Y-m-d');
$late_fee_amount = ($invoice_balance * $config_invoice_late_fee_percent) / 100;
$new_invoice_amount = $invoice_amount + $late_fee_amount;
mysqli_query($mysqli, "UPDATE invoices SET invoice_amount = $new_invoice_amount WHERE invoice_id = $invoice_id");
//Insert Items into New Invoice
mysqli_query($mysqli, "INSERT INTO invoice_items SET item_name = 'Late Fee', item_description = '$config_invoice_late_fee_percent% late fee applied on $todays_date', item_quantity = 1, item_price = $late_fee_amount, item_total = $late_fee_amount, item_order = 998, item_invoice_id = $invoice_id");
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Sent', history_description = 'Cron applied a late fee of $late_fee_amount', history_invoice_id = $invoice_id");
appNotify("Invoice Late Charge", "Invoice $invoice_prefix$invoice_number for $client_name in the amount of $invoice_amount was charged a late fee of $late_fee_amount", "/agent/invoice.php?invoice_id=$invoice_id", $client_id);
// Roll the fee into the balance and total we report below
$invoice_amount = $new_invoice_amount;
$invoice_balance = $invoice_balance + $late_fee_amount;
}
appNotify("Invoice Overdue", "Invoice $invoice_prefix$invoice_number for $client_name with a balance of " . numfmt_format_currency($currency_format, $invoice_balance, $invoice_currency_code) . " is overdue by $day days", "/agent/invoice.php?invoice_id=$invoice_id", $client_id);
// One client email per invoice per day - the 3am run and a Run Now the same
// afternoon must not both mail them
if (cronInvoiceHistoryToday($invoice_id, 'Cron Emailed Overdue Invoice')) {
continue;
}
$subject = "Overdue Invoice $invoice_prefix$invoice_number";
// Only show the paid line if a payment has actually been applied
$paid_line = $amount_paid > 0 ? "Amount Paid: " . numfmt_format_currency($currency_format, $amount_paid, $invoice_currency_code) . "<br>" : "";
$body = "Hello $contact_name,<br><br>Our records indicate that we have not yet received payment in full for the invoice $invoice_prefix$invoice_number. We kindly request that you submit your payment as soon as possible. If you have any questions or concerns, please do not hesitate to contact us at $company_email or $company_phone.
<br>
Kindly review the invoice details mentioned below.<br><br>Invoice: $invoice_prefix$invoice_number<br>Issue Date: $invoice_date<br>Invoice Total: " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . "<br>$paid_line" . "Balance Due: " . numfmt_format_currency($currency_format, $invoice_balance, $invoice_currency_code) . "<br>Due Date: $invoice_due<br>Over Due By: $day Days<br><br><br>To view your invoice, please click <a href=\'https://$config_base_url/guest/guest_view_invoice.php?invoice_id=$invoice_id&url_key=$invoice_url_key\'>here</a>.<br><br><br>--<br>$company_name - Billing<br>$config_invoice_from_email<br>$company_phone";
$mail = addToMailQueue([
[
'from' => $config_invoice_from_email,
'from_name' => $config_invoice_from_name,
'recipient' => $contact_email,
'recipient_name' => $contact_name,
'subject' => $subject,
'body' => $body
]
]);
if ($mail === true) {
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Sent', history_description = 'Cron Emailed Overdue Invoice', history_invoice_id = $invoice_id");
} else {
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Sent', history_description = 'Cron Failed to send Overdue Invoice', history_invoice_id = $invoice_id");
appNotify("Mail", "Failed to send email to $contact_email");
// Logging
logApp("Mail", "error", "Failed to send email to $contact_email regarding $subject. $mail");
}
}
}
}
// Logging
// logAudit("Cron", "Task", "Cron created notifications for past due invoices and sent out notifications to the primary and billing contacts email");
// Recurring invoices - each one due today, or missed by up to
// RECURRING_INVOICE_GRACE_DAYS (a night or a few the cron didn't run), makes
// its invoice and moves on a period from the run it billed. One further
// behind than that is left for someone to look at - the page says it is late
// and offers Invoice Now, or moving it on without billing the missed runs.
// recurringInvoiceGenerate() (functions/billing.php) is the same code the
// Invoice Now button runs.
$sql_recurring_invoices = mysqli_query($mysqli, "SELECT recurring_invoice_id, recurring_invoice_next_date FROM recurring_invoices
WHERE recurring_invoice_status = 1 AND recurring_invoice_archived_at IS NULL
AND recurring_invoice_next_date <= CURDATE() AND recurring_invoice_next_date >= CURDATE() - INTERVAL " . RECURRING_INVOICE_GRACE_DAYS . " DAY
ORDER BY recurring_invoice_next_date ASC, recurring_invoice_id ASC");
$recurring_invoices_made = 0;
while ($row = mysqli_fetch_assoc($sql_recurring_invoices)) {
if (recurringInvoiceGenerate($row['recurring_invoice_id'], 'Cron', false, $row['recurring_invoice_next_date'])) {
$recurring_invoices_made++;
}
}
if ($recurring_invoices_made) {
logApp("Cron", "info", "Cron made $recurring_invoices_made invoice(s) from recurring invoices");
}
// Start Flag any active recurring "next run" dates that are in the past - too far behind for the run above to bill
$sql_invalid_recurring_invoices = mysqli_query($mysqli, "SELECT recurring_invoice_id, recurring_invoice_number, recurring_invoice_prefix, recurring_invoice_client_id FROM recurring_invoices
WHERE recurring_invoice_next_date < CURDATE() AND recurring_invoice_status = 1 AND recurring_invoice_archived_at IS NULL");
while ($row = mysqli_fetch_assoc($sql_invalid_recurring_invoices)) {
$invoice_prefix = $row['recurring_invoice_prefix'];
$invoice_number = intval($row['recurring_invoice_number']);
appNotify("Invoice", escapeSql("Recurring invoice $invoice_prefix$invoice_number is behind - its next date has passed and it won't bill until it is invoiced or moved on"), "/agent/recurring_invoice.php?recurring_invoice_id=" . intval($row['recurring_invoice_id']), intval($row['recurring_invoice_client_id']));
}
// End Flag any active recurring "next run" dates that are in the past
// Start Notify draft invoices needing review
$sql_invoices_pending_send = mysqli_query($mysqli,"SELECT invoice_id FROM invoices WHERE invoice_status = 'Draft'");
$invoices_pending_send = mysqli_num_rows($sql_invoices_pending_send);
if ($invoices_pending_send > 0) {
appNotify("Draft Invoices", "There are $invoices_pending_send draft invoices pending review", "/agent/invoices.php?&status=Draft");
}
// End Notify draft invoices needing review
// Start Recurring Payments
$sql_recurring_payments = mysqli_query($mysqli, "
SELECT client_id, client_name, contact_email, contact_name, invoice_amount, invoice_currency_code,
invoice_date, invoice_due, invoice_id, invoice_number, invoice_prefix, invoice_scope,
invoice_url_key, recurring_payment_account_id, recurring_payment_currency_code,
recurring_payment_method, recurring_payment_saved_payment_id FROM recurring_payments
LEFT JOIN invoices ON invoice_recurring_invoice_id = recurring_payment_recurring_invoice_id
LEFT JOIN clients ON client_id = invoice_client_id
LEFT JOIN contacts ON client_id = contact_client_id AND contact_primary = 1
WHERE invoice_due = CURDATE()
AND (invoice_status = 'Sent' OR invoice_status = 'Viewed')
");
while ($row = mysqli_fetch_assoc($sql_recurring_payments)) {
$invoice_id = intval($row['invoice_id']);
$invoice_prefix = escapeSql($row['invoice_prefix']);
$invoice_number = intval($row['invoice_number']);
$invoice_scope = escapeSql($row['invoice_scope']);
$invoice_date = escapeSql($row['invoice_date']);
$invoice_due = escapeSql($row['invoice_due']);
$invoice_amount = floatval($row['invoice_amount']);
$invoice_url_key = escapeSql($row['invoice_url_key']);
$invoice_currency_code = escapeSql($row['invoice_currency_code']);
$recurring_payment_account_id = intval($row['recurring_payment_account_id']);
$recurring_payment_method = escapeSql($row['recurring_payment_method']);
$recurring_payment_currency_code = escapeSql($row['recurring_payment_currency_code']);
$recurring_payment_saved_payment_id = intval($row['recurring_payment_saved_payment_id']);
$client_id = intval($row['client_id']);
$client_name = escapeSql($row['client_name']);
$contact_name = escapeSql($row['contact_name']);
$contact_email = escapeSql($row['contact_email']);
// A card that already declined today is not retried - the day-matched selection above
// would otherwise re-attempt the same charge on every extra run of this script
if (cronInvoiceHistoryToday($invoice_id, 'Stripe autopay failed')) {
continue;
}
// Only attempt autopay if a saved payment method is set
if ($recurring_payment_saved_payment_id) {
// Get the saved payment method and provider details
$saved_payment = mysqli_fetch_assoc(mysqli_query($mysqli, "
SELECT payment_provider_account, payment_provider_id, payment_provider_name,
payment_provider_private_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
WHERE saved_payment_id = $recurring_payment_saved_payment_id
AND saved_payment_client_id = $client_id
AND payment_provider_active = 1
LIMIT 1
"));
if (!$saved_payment) {
logAudit("Invoice", "Payment", "Failed auto Payment for invoice $invoice_prefix$invoice_number: Saved payment method not found or provider inactive", $client_id, $invoice_id);
continue;
}
$provider_id = intval($saved_payment['payment_provider_id']);
$provider_name = escapeSql($saved_payment['payment_provider_name']);
$provider_private_key = $saved_payment['payment_provider_private_key'];
$account_id = intval($saved_payment['payment_provider_account']);
$saved_payment_description = escapeSql($saved_payment['saved_payment_description']);
$stripe_payment_method_id = $saved_payment['saved_payment_provider_method'];
// NEW: Get the payment_provider_client (Stripe Customer ID) from client_payment_provider
$cpp_query = mysqli_query($mysqli, "
SELECT payment_provider_client FROM client_payment_provider
WHERE client_id = $client_id
AND payment_provider_id = $provider_id
LIMIT 1
");
$cpp_row = mysqli_fetch_assoc($cpp_query);
$stripe_customer_id = $cpp_row ? escapeSql($cpp_row['payment_provider_client']) : '';
// Stripe
if ($provider_name === "Stripe") {
if ($provider_private_key && $stripe_customer_id && $stripe_payment_method_id) {
require_once __DIR__ . '/../includes/stripe_init.php';
$stripe = new \Stripe\StripeClient($provider_private_key);
$balance_to_pay = round($invoice_amount, 2);
$pi_description = "ITFlow: $client_name payment of $recurring_payment_currency_code $balance_to_pay for $invoice_prefix$invoice_number";
try {
$payment_intent = $stripe->paymentIntents->create([
'amount' => intval(round($balance_to_pay * 100)),
'currency' => $recurring_payment_currency_code,
'customer' => $stripe_customer_id,
'payment_method' => $stripe_payment_method_id,
'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,
]
]);
$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");
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Payment failed', history_description = 'Stripe autopay failed due to payment error', history_invoice_id = $invoice_id");
logAudit("Invoice", "Payment", "Failed auto Payment amount of invoice $invoice_prefix$invoice_number due to Stripe payment error: $error", $client_id, $invoice_id);
continue;
}
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 (autopay)', history_invoice_id = $invoice_id");
// RECEIPT EMAIL
if (!empty($config_smtp_provider)) {
$subject = "Payment Received - Invoice $invoice_prefix$invoice_number";
$body = "Hello $contact_name<br><br>We have received online payment for the amount of " . numfmt_format_currency($currency_format, $invoice_amount, $recurring_payment_currency_code) . " for invoice <a href=\\'https://$config_base_url/guest/guest_view_invoice.php?invoice_id=$invoice_id&url_key=$invoice_url_key\\'>$invoice_prefix$invoice_number</a>. Please keep this email as a receipt for your records.<br><br>Amount Paid: " . numfmt_format_currency($currency_format, $invoice_amount, $recurring_payment_currency_code) . "<br><br>Thank you for your business!<br><br><br>--<br>$company_name - Billing Department<br>$config_invoice_from_email<br>$company_phone";
$data = [[
'from' => $config_invoice_from_email,
'from_name' => $config_invoice_from_name,
'recipient' => $contact_email,
'recipient_name' => $contact_name,
'subject' => $subject,
'body' => $body,
]];
// Internal notification
if (!empty($config_invoice_paid_notification_email)) {
$subject_int = "Payment Received - $client_name - Invoice $invoice_prefix$invoice_number";
$body_int = "This is a notification that an invoice has been paid in ITFlow. Below is a copy of the receipt sent to the client:-<br><br>--------<br><br>$body";
$data[] = [
'from' => $config_invoice_from_email,
'from_name' => $config_invoice_from_name,
'recipient' => $config_invoice_paid_notification_email,
'recipient_name' => $contact_name,
'subject' => $subject_int,
'body' => $body_int,
];
}
$mail = addToMailQueue($data);
$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");
logAudit("Invoice", "Payment", "Payment receipt for invoice $invoice_prefix$invoice_number queued to $contact_email Email ID: $email_id", $client_id, $invoice_id);
}
// LOGGING
$extended_log_desc = !$pi_livemode ? '(DEV MODE)' : '';
appNotify("Invoice Paid", "Invoice $invoice_prefix$invoice_number automatically paid", "/agent/invoice.php?invoice_id=$invoice_id", $client_id);
logAudit("Invoice", "Payment", "Auto Stripe payment amount of " . numfmt_format_currency($currency_format, $invoice_amount, $recurring_payment_currency_code) . " added to invoice $invoice_prefix$invoice_number - $pi_id $extended_log_desc", $client_id, $invoice_id);
triggerCustomAction('invoice_pay', $invoice_id);
} else {
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Payment failed', history_description = 'Stripe autopay failed: Status {$payment_intent->status}', history_invoice_id = $invoice_id");
logAudit("Invoice", "Payment", "Failed auto Payment for invoice $invoice_prefix$invoice_number. Stripe PI status: {$payment_intent->status}", $client_id, $invoice_id);
}
} // End if Stripe creds and IDs
} // End if Stripe provider
// Add other provider logic here as needed
} else {
// Handle Non-payment-provider autopay
mysqli_query($mysqli, "INSERT INTO payments SET payment_date = CURDATE(), payment_amount = $invoice_amount, payment_currency_code = '$recurring_payment_currency_code', payment_account_id = $recurring_payment_account_id, payment_method = '$recurring_payment_method', payment_reference = 'Paid via AutoPay', payment_invoice_id = $invoice_id");
$payment_id = mysqli_insert_id($mysqli);
mysqli_query($mysqli, "UPDATE invoices SET invoice_status = 'Paid' WHERE invoice_id = $invoice_id");
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Paid', history_description = 'Payment added via Auto Pay', history_invoice_id = $invoice_id");
logAudit("Invoice", "Payment", "Auto Payment amount of $recurring_payment_currency_code $invoice_amount added to invoice $invoice_prefix$invoice_number", $client_id, $invoice_id);
}
}
/*
* Stripe fee reconciliation
* A payment can complete before Stripe attaches the balance transaction,
* in which case the fee expense is skipped at payment time. Find recent
* Stripe payments with no matching fee expense and record the actual fee
* now that the balance transaction exists.
*/
$stripe_provider = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM payment_providers WHERE payment_provider_name = 'Stripe' LIMIT 1"));
if ($stripe_provider) {
$provider_private_key = $stripe_provider['payment_provider_private_key'];
$expense_vendor_id = intval($stripe_provider['payment_provider_expense_vendor']);
$expense_category_id = intval($stripe_provider['payment_provider_expense_category']);
$expense_account_id = intval($stripe_provider['payment_provider_account']);
if ($provider_private_key && $expense_vendor_id > 0 && $expense_category_id > 0) {
$sql_missing_fee = mysqli_query($mysqli, "
SELECT payment_reference, payment_date, payment_amount, invoice_prefix, invoice_number, invoice_client_id
FROM payments
LEFT JOIN invoices ON payment_invoice_id = invoice_id
WHERE payment_reference LIKE 'Stripe - pi\_%'
AND payment_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND NOT EXISTS (
SELECT 1 FROM expenses WHERE LOCATE(payments.payment_reference, expenses.expense_reference) = 1
)
LIMIT 50
");
if ($sql_missing_fee && mysqli_num_rows($sql_missing_fee) > 0) {
require_once __DIR__ . '/../includes/stripe_init.php';
$stripe = new \Stripe\StripeClient($provider_private_key);
while ($missing = mysqli_fetch_assoc($sql_missing_fee)) {
$payment_reference = escapeSql($missing['payment_reference']);
$payment_date = escapeSql($missing['payment_date']);
$payment_amount = floatval($missing['payment_amount']);
$invoice_prefix = escapeSql($missing['invoice_prefix']);
$invoice_number = intval($missing['invoice_number']);
$client_id = intval($missing['invoice_client_id']);
$pi_id = str_replace('Stripe - ', '', $missing['payment_reference']);
try {
$payment_intent = $stripe->paymentIntents->retrieve($pi_id, ['expand' => ['latest_charge.balance_transaction']]);
} catch (Exception $e) {
logApp("Stripe", "warning", "Fee reconciliation - could not retrieve $pi_id: " . $e->getMessage());
continue;
}
// Actual fee from the balance transaction (null until Stripe attaches it - retried next run)
$balance_transaction = $payment_intent->latest_charge->balance_transaction ?? null;
if ($balance_transaction && !is_string($balance_transaction)) {
$gateway_fee = round($balance_transaction->fee / 100, 2);
$gateway_fee_currency = escapeSql(strtoupper($balance_transaction->currency));
mysqli_query($mysqli, "INSERT INTO expenses SET expense_date = '$payment_date', expense_amount = $gateway_fee, expense_currency_code = '$gateway_fee_currency', expense_account_id = $expense_account_id, expense_vendor_id = $expense_vendor_id, expense_client_id = $client_id, expense_category_id = $expense_category_id, expense_description = 'Stripe fee for Invoice $invoice_prefix$invoice_number payment of $payment_amount', expense_reference = '$payment_reference'");
logApp("Stripe", "info", "Fee reconciliation - recorded Stripe fee of $gateway_fee for $pi_id");
}
// Still-missing balance transactions get picked up on the next run
}
}
}
}
// Recurring Expenses
// Loop through all recurring expenses that match today's date and is active
$sql_recurring_expenses = mysqli_query($mysqli, "SELECT recurring_expense_account_id, recurring_expense_amount, recurring_expense_category_id,
recurring_expense_client_id, recurring_expense_currency_code, recurring_expense_day,
recurring_expense_description, recurring_expense_frequency, recurring_expense_id,
recurring_expense_month, recurring_expense_payment_method, recurring_expense_reference,
recurring_expense_vendor_id FROM recurring_expenses WHERE recurring_expense_next_date = CURDATE() AND recurring_expense_status = 1");
while ($row = mysqli_fetch_assoc($sql_recurring_expenses)) {
$recurring_expense_id = intval($row['recurring_expense_id']);
$recurring_expense_frequency = intval($row['recurring_expense_frequency']);
$recurring_expense_month = intval($row['recurring_expense_month']);
$recurring_expense_day = intval($row['recurring_expense_day']);
$recurring_expense_description = escapeSql($row['recurring_expense_description']);
$recurring_expense_amount = floatval($row['recurring_expense_amount']);
$recurring_expense_payment_method = escapeSql($row['recurring_expense_payment_method']);
$recurring_expense_reference = escapeSql($row['recurring_expense_reference']);
$recurring_expense_currency_code = escapeSql($row['recurring_expense_currency_code']);
$recurring_expense_vendor_id = intval($row['recurring_expense_vendor_id']);
$recurring_expense_category_id = intval($row['recurring_expense_category_id']);
$recurring_expense_account_id = intval($row['recurring_expense_account_id']);
$recurring_expense_client_id = intval($row['recurring_expense_client_id']);
// Calculate next billing date based on frequency
if ($recurring_expense_frequency == 1) { // Monthly
$next_date_query = "DATE_ADD(CURDATE(), INTERVAL 1 MONTH)";
} elseif ($recurring_expense_frequency == 2) { // Yearly
$next_date_query = "DATE(CONCAT(YEAR(CURDATE()) + 1, '-', $recurring_expense_month, '-', $recurring_expense_day))";
} else {
// Handle unexpected frequency values. For now, just use current date.
$next_date_query = "CURDATE()";
}
mysqli_query($mysqli,"INSERT INTO expenses SET expense_date = CURDATE(), expense_amount = $recurring_expense_amount, expense_currency_code = '$recurring_expense_currency_code', expense_account_id = $recurring_expense_account_id, expense_vendor_id = $recurring_expense_vendor_id, expense_client_id = $recurring_expense_client_id, expense_category_id = $recurring_expense_category_id, expense_description = '$recurring_expense_description', expense_reference = '$recurring_expense_reference'");
$expense_id = mysqli_insert_id($mysqli);
appNotify("Expense Created", "Expense $recurring_expense_description created from recurring expenses", "/agent/expenses.php", $recurring_expense_client_id);
// Update recurring dates using calculated next billing date
mysqli_query($mysqli, "UPDATE recurring_expenses SET recurring_expense_last_sent = CURDATE(), recurring_expense_next_date = $next_date_query WHERE recurring_expense_id = $recurring_expense_id");
} //End Recurring expenses loop
// Flag any active recurring "next run" dates that are in the past
$sql_invalid_recurring_expenses = mysqli_query($mysqli, "SELECT recurring_expense_description FROM recurring_expenses WHERE recurring_expense_next_date < CURDATE() AND recurring_expense_status = 1");
while ($row = mysqli_fetch_assoc($sql_invalid_recurring_expenses)) {
$recurring_expense_description = escapeSql($row['recurring_expense_description']);
appNotify("Expense", "Recurring expense $recurring_expense_description next run date is in the past!", "/agent/recurring_expenses.php");
}
// Logging
//logApp("Cron", "info", "Cron created expenses from recurring expenses");
// TELEMETRY
if ($config_telemetry > 0 || $config_telemetry == 2) {
$current_version = gitCurrentCommit();
// Client Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('client_id') AS num FROM clients"));
$client_count = $row['num'];
// Ticket Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('recurring_id') AS num FROM tickets"));
$ticket_count = $row['num'];
// Recurring Ticket Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('recurring_ticket_id') AS num FROM recurring_tickets"));
$recurring_ticket_count = $row['num'];
// Calendar Event Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('event_id') AS num FROM calendar_events"));
$calendar_event_count = $row['num'];
// Quote Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('quote_id') AS num FROM quotes"));
$quote_count = $row['num'];
// Invoice Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('invoice_id') AS num FROM invoices"));
$invoice_count = $row['num'];
// Revenue Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('revenue_id') AS num FROM revenues"));
$revenue_count = $row['num'];
// Recurring Invoice Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('recurring_invoice_id') AS num FROM recurring_invoices"));
$recurring_invoice_count = $row['num'];
// Account Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('account_id') AS num FROM accounts"));
$account_count = $row['num'];
// Tax Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('tax_id') AS num FROM taxes"));
$tax_count = $row['num'];
// Product Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('product_id') AS num FROM products"));
$product_count = $row['num'];
// Payment Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('payment_id') AS num FROM payments WHERE payment_invoice_id > 0"));
$payment_count = $row['num'];
// Company Vendor Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('vendor_id') AS num FROM vendors WHERE vendor_client_id = 0"));
$company_vendor_count = $row['num'];
// Expense Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('expense_id') AS num FROM expenses WHERE expense_vendor_id > 0"));
$expense_count = $row['num'];
// Trip Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('trip_id') AS num FROM trips"));
$trip_count = $row['num'];
// Transfer Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('transfer_id') AS num FROM transfers"));
$transfer_count = $row['num'];
// Contact Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('contact_id') AS num FROM contacts"));
$contact_count = $row['num'];
// Location Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('location_id') AS num FROM locations"));
$location_count = $row['num'];
// Asset Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('asset_id') AS num FROM assets"));
$asset_count = $row['num'];
// Software Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('software_id') AS num FROM software"));
$software_count = $row['num'];
// Software Template Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('software_template_id') AS num FROM software_templates"));
$software_template_count = $row['num'];
// Credential Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('credential_id') AS num FROM credentials"));
$credential_count = $row['num'];
// Network Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('network_id') AS num FROM networks"));
$network_count = $row['num'];
// Certificate Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('certificate_id') AS num FROM certificates"));
$certificate_count = $row['num'];
// Domain Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('domain_id') AS num FROM domains"));
$domain_count = $row['num'];
// Service Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('service_id') AS num FROM services"));
$service_count = $row['num'];
// Client Vendor Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('vendor_id') AS num FROM vendors WHERE vendor_client_id > 0"));
$client_vendor_count = $row['num'];
// Vendor Template Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('vendor_template_id') AS num FROM vendor_templates"));
$vendor_template_count = $row['num'];
// File Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('file_id') AS num FROM files"));
$file_count = $row['num'];
// Document Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('document_id') AS num FROM documents"));
$document_count = $row['num'];
// Document Template Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('document_template_id') AS num FROM document_templates"));
$document_template_count = $row['num'];
// Shared Item Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('item_id') AS num FROM shared_items"));
$shared_item_count = $row['num'];
// Company Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('company_id') AS num FROM companies"));
$company_count = $row['num'];
// User Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('user_id') AS num FROM users"));
$user_count = $row['num'];
// Category Expense Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('category_id') AS num FROM categories WHERE category_type = 'Expense'"));
$category_expense_count = $row['num'];
// Category Income Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('category_id') AS num FROM categories WHERE category_type = 'Income'"));
$category_income_count = $row['num'];
// Category Referral Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('category_id') AS num FROM categories WHERE category_type = 'Referral'"));
$category_referral_count = $row['num'];
// Category Payment Method Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('category_id') AS num FROM categories WHERE category_type = 'Payment Method'"));
$category_payment_method_count = $row['num'];
// Tag Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('tag_id') AS num FROM tags"));
$tag_count = $row['num'];
// API Key Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('api_key_id') AS num FROM api_keys"));
$api_key_count = $row['num'];
// Log Count
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT('log_id') AS num FROM logs"));
$log_count = $row['num'];
$postdata = http_build_query(
array(
'installation_id' => "$installation_id",
'version' => "$current_version",
'company_name' => "$company_name",
'website' => "$company_website",
'city' => "$company_city",
'state' => "$company_state",
'country' => "$company_country",
'currency' => "$company_currency",
'client_count' => $client_count,
'ticket_count' => $ticket_count,
'recurring_ticket_count' => $recurring_ticket_count,
'calendar_event_count' => $calendar_event_count,
'quote_count' => $quote_count,
'invoice_count' => $invoice_count,
'revenue_count' => $revenue_count,
'recurring_invoice_count' => $recurring_invoice_count,
'account_count' => $account_count,
'tax_count' => $tax_count,
'product_count' => $product_count,
'payment_count' => $payment_count,
'company_vendor_count' => $company_vendor_count,
'expense_count' => $expense_count,
'trip_count' => $trip_count,
'transfer_count' => $transfer_count,
'contact_count' => $contact_count,
'location_count' => $location_count,
'asset_count' => $asset_count,
'software_count' => $software_count,
'software_template_count' => $software_template_count,
'credential_count' => $credential_count,
'network_count' => $network_count,
'certificate_count' => $certificate_count,
'domain_count' => $domain_count,
'service_count' => $service_count,
'client_vendor_count' => $client_vendor_count,
'vendor_template_count' => $vendor_template_count,
'file_count' => $file_count,
'document_count' => $document_count,
'document_template_count' => $document_template_count,
'shared_item_count' => $shared_item_count,
'company_count' => $company_count,
'user_count' => $user_count,
'category_expense_count' => $category_expense_count,
'category_income_count' => $category_income_count,
'category_referral_count' => $category_referral_count,
'category_payment_method_count' => $category_payment_method_count,
'tag_count' => $tag_count,
'api_key_count' => $api_key_count,
'log_count' => $log_count,
'config_theme' => "$config_theme",
'config_enable_cron' => $config_enable_cron,
'config_ticket_email_parse' => $config_ticket_email_parse,
'config_module_enable_itdoc' => $config_module_enable_itdoc,
'config_module_enable_ticketing' => $config_module_enable_ticketing,
'config_module_enable_accounting' => $config_module_enable_accounting,
'config_telemetry' => $config_telemetry,
'collection_method' => 3
)
);
$opts = array('http' =>
array(
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => $postdata
)
);
$context = stream_context_create($opts);
$result = file_get_contents('https://telemetry.itflow.org', false, $context);
// Logging
// logAudit("Cron", "Task", "Cron sent telemetry results to ITFlow Developers");
}
// Fetch Updates
/*
* The check itself is cron/update_check.php now - read what it stored rather than running a
* second git fetch here. Nothing to say until that job has run once, which is the same
* position this was in when a fetch failed.
*/
if (settingsColumnExists($mysqli, 'config_update_latest_commit')) {
$update_check_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_update_latest_commit FROM settings WHERE company_id = 1"));
$latest_version = (string) ($update_check_row['config_update_latest_commit'] ?? '');
$current_version = gitCurrentCommit();
if ($latest_version !== '' && $current_version !== '' && $latest_version !== $current_version) {
// Send Alert to inform Updates Available
appNotify("Update", "New Updates are Available [$latest_version]", "/admin/update.php");
}
}
/*
* ###############################################################################################################
* FINISH UP
* ###############################################################################################################
*/
// Logging
logApp("Cron", "info", "Cron executed successfully");