mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 15:07:24 +00:00
1094 lines
55 KiB
PHP
1094 lines
55 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_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']);
|
|
|
|
// 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();
|
|
|
|
// Forget the portal password links that have run out - an invite works for a
|
|
// week, Forgot password for an hour (functions/portal.php), and one with no
|
|
// expiry is from before links had one
|
|
mysqli_query($mysqli, "UPDATE users SET user_password_reset_token = NULL, user_password_reset_token_expire = NULL
|
|
WHERE user_password_reset_token IS NOT NULL AND (user_password_reset_token_expire IS NULL OR user_password_reset_token_expire < NOW())");
|
|
|
|
// 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 the PDFs made to go with billing emails - sent or given up on by now
|
|
billingEmailPdfCleanup(7);
|
|
|
|
// 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 logs - each kept for its own number of days, 0 for keep forever (Admin > Settings > Logs, functions/app_log.php)
|
|
$log_retention = appLogRetention();
|
|
|
|
// The audit log, and API key use with it
|
|
if ($log_retention['audit'] > 0) {
|
|
mysqli_query($mysqli, "DELETE FROM logs WHERE log_created_at < CURDATE() - INTERVAL {$log_retention['audit']} DAY");
|
|
mysqli_query($mysqli, "DELETE FROM api_key_usage WHERE api_key_usage_date < CURDATE() - INTERVAL {$log_retention['audit']} DAY");
|
|
}
|
|
|
|
// The app logs
|
|
if ($log_retention['app'] > 0) {
|
|
mysqli_query($mysqli, "DELETE FROM app_logs WHERE app_log_created_at < CURDATE() - INTERVAL {$log_retention['app']} 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", "Close", "$ticket_prefix$ticket_number auto closed", $client_id, $ticket_id);
|
|
|
|
triggerCustomAction('ticket_close', $ticket_id);
|
|
|
|
// The client is told, as when an agent closes it
|
|
if (intval($config_ticket_client_general_notifications) === 1) {
|
|
ticketMailClosed($ticket_id);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Payment reminders, on the schedule in the invoice settings (functions/billing_email.php,
|
|
* billingReminderRules()): each open invoice whose day it is - before, on or after its due
|
|
* date - gets one, to the client's primary and billing contacts, unless the client is left
|
|
* out of reminders. One a recurring autopay will charge on its due date isn't reminded
|
|
* before then. Agents hear about each one late, on its reminder days, and the late fee
|
|
* still comes every 30 days late. At most one of each a day, however often this runs.
|
|
*/
|
|
$reminder_rules = billingReminderRules(true);
|
|
if ($reminder_rules['enabled']) {
|
|
|
|
$earliest = min(0, $reminder_rules['days'] ? min($reminder_rules['days']) : 0);
|
|
$sql_reminders = mysqli_query($mysqli, "SELECT invoice_id, DATEDIFF(CURDATE(), invoice_due) AS days_late, client_reminders,
|
|
invoice_recurring_invoice_id > 0 AND EXISTS (SELECT 1 FROM recurring_payments WHERE recurring_payment_recurring_invoice_id = invoice_recurring_invoice_id) AS autopay
|
|
FROM invoices
|
|
JOIN clients ON client_id = invoice_client_id
|
|
WHERE invoice_status IN ('Sent', 'Viewed', 'Partial') AND invoice_due IS NOT NULL AND DATEDIFF(CURDATE(), invoice_due) >= $earliest
|
|
ORDER BY invoice_due ASC, invoice_id ASC");
|
|
|
|
while ($row = mysqli_fetch_assoc($sql_reminders)) {
|
|
$invoice_id = intval($row['invoice_id']);
|
|
$days = intval($row['days_late']);
|
|
$invoice = billingRow('invoice', $invoice_id, false);
|
|
if (!$invoice) {
|
|
continue;
|
|
}
|
|
$balance = invoiceState($invoice, invoicePaid($invoice_id))['balance'];
|
|
$number = billingNumber('invoice', $invoice);
|
|
|
|
// Nothing actually owed (paid in full but status lagging) - skip
|
|
if ($balance <= 0) {
|
|
continue;
|
|
}
|
|
|
|
// Late fee - every 30 days late, as a line of its own, at most once a day
|
|
if ($config_invoice_late_fee_enable == 1 && $days > 0 && $days % 30 === 0 && !cronInvoiceHistoryToday($invoice_id, 'Cron applied a late fee')) {
|
|
$late_fee = round($balance * $config_invoice_late_fee_percent / 100, 2);
|
|
if ($late_fee > 0) {
|
|
billingItemSave('invoice', $invoice_id, ['name' => 'Late Fee', 'description' => floatval($config_invoice_late_fee_percent) . '% late fee applied on ' . date('Y-m-d'), 'qty' => 1, 'price' => $late_fee, 'tax_id' => 0]);
|
|
billingLog('invoice', $invoice_id, $invoice['invoice_status'], 'Cron applied a late fee of ' . billingMoney($late_fee, $invoice['invoice_currency_code']));
|
|
appNotify("Invoice Late Charge", escapeSql("Invoice $number for {$invoice['client_name']} with a balance of " . billingMoney($balance, $invoice['invoice_currency_code']) . " was charged a late fee of " . billingMoney($late_fee, $invoice['invoice_currency_code'])), "/agent/invoice.php?invoice_id=$invoice_id", intval($invoice['client_id']));
|
|
$invoice = billingRow('invoice', $invoice_id, false);
|
|
$balance = invoiceState($invoice, invoicePaid($invoice_id))['balance'];
|
|
}
|
|
}
|
|
|
|
if (!billingReminderOnDay($days, $reminder_rules)) {
|
|
continue;
|
|
}
|
|
|
|
if ($days > 0) {
|
|
appNotify("Invoice Overdue", escapeSql("Invoice $number for {$invoice['client_name']} with a balance of " . billingMoney($balance, $invoice['invoice_currency_code']) . " is overdue by $days days"), "/agent/invoice.php?invoice_id=$invoice_id", intval($invoice['client_id']));
|
|
}
|
|
|
|
// Left out, charged by autopay on the day, already reminded today, or no mail
|
|
if (!intval($row['client_reminders']) || ($days <= 0 && intval($row['autopay'])) || empty($config_smtp_provider)
|
|
|| cronInvoiceHistoryToday($invoice_id, 'Reminder emailed') || cronInvoiceHistoryToday($invoice_id, 'Reminder not sent') || cronInvoiceHistoryToday($invoice_id, 'Cron Emailed Overdue Invoice')) {
|
|
continue;
|
|
}
|
|
|
|
$recipients = billingEmailRecipients($invoice['client_id']);
|
|
if (!$recipients) {
|
|
billingLog('invoice', $invoice_id, $invoice['invoice_status'], 'Reminder not sent - no primary or billing contact with an email address');
|
|
continue;
|
|
}
|
|
[$subject, $email] = billingInvoiceEmail($invoice, ['kind' => 'reminder']);
|
|
$attach = [];
|
|
if ($reminder_rules['attach_pdf'] && ($file = billingEmailPdf(billingPdf('invoice', $invoice)))) {
|
|
$attach[] = $file;
|
|
}
|
|
$sent = billingEmailQueue($recipients, $subject, $email, ['attach' => $attach]);
|
|
billingLog('invoice', $invoice_id, $invoice['invoice_status'], 'Reminder emailed to ' . implode(', ', $sent) . ' - ' . billingReminderWhen($days));
|
|
}
|
|
|
|
// Statements each month, on the day in the invoice settings, to every client with a balance
|
|
if ($reminder_rules['statement_monthly'] && intval(date('j')) === $reminder_rules['statement_day'] && !empty($config_smtp_provider)) {
|
|
$sql_statements = mysqli_query($mysqli, "SELECT DISTINCT client_id FROM invoices
|
|
JOIN clients ON client_id = invoice_client_id
|
|
WHERE invoice_status IN ('Sent', 'Viewed', 'Partial') AND client_reminders = 1 AND client_archived_at IS NULL");
|
|
while ($row = mysqli_fetch_assoc($sql_statements)) {
|
|
$client_id = intval($row['client_id']);
|
|
$already = mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM logs WHERE log_type = 'Client' AND log_action = 'Statement' AND log_client_id = $client_id AND log_created_at >= CURDATE() LIMIT 1"));
|
|
$statement = billingStatement($client_id);
|
|
if ($already || $statement['total'] <= 0) {
|
|
continue;
|
|
}
|
|
$sent = billingStatementSend($statement, null, ['attach' => $reminder_rules['attach_pdf']]);
|
|
if ($sent) {
|
|
logAudit("Client", "Statement", escapeSql("Cron emailed the monthly statement (" . count($statement['invoices']) . " invoices, " . billingMoney($statement['total'], $statement['currency']) . " due) to " . implode(', ', $sent)), $client_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 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");
|
|
|
|
// The client's receipt, and the paid notification address - functions/billing_email.php
|
|
if (!empty($config_smtp_provider)) {
|
|
$receipt_to = billingPaymentSend($invoice_id, $pi_amount_paid, ['date' => $pi_date, 'method' => 'Card', 'reference' => "Stripe - $pi_id", 'how' => 'Automatic card'], true);
|
|
if ($receipt_to) {
|
|
logAudit("Invoice", "Payment", "Payment receipt for invoice $invoice_prefix$invoice_number queued to " . escapeSql(implode(', ', $receipt_to)), $client_id, $invoice_id);
|
|
}
|
|
}
|
|
|
|
// 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. One card payment over several
|
|
* invoices (the client portal's Pay invoices) is a payment row per invoice
|
|
* with the same reference - it has one fee, so it is taken once.
|
|
*/
|
|
$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, MIN(payment_date) AS payment_date, SUM(payment_amount) AS payment_amount,
|
|
GROUP_CONCAT(CONCAT(invoice_prefix, invoice_number) ORDER BY invoice_number SEPARATOR ', ') AS invoice_numbers,
|
|
MIN(invoice_client_id) AS 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
|
|
)
|
|
GROUP BY payment_reference
|
|
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_numbers = escapeSql($missing['invoice_numbers']);
|
|
$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_numbers 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 - each running one due today, or missed by up to
|
|
* RECURRING_EXPENSE_GRACE_DAYS (nights the cron didn't run), is recorded as
|
|
* an expense dated the day it was due, and moves on a period
|
|
* (recurringExpenseRecord() in functions/expenses.php - the same code Record
|
|
* Now runs). One with an end date stops after its last run. One that falls
|
|
* further behind than that is left for someone to look at, and they are told
|
|
* once, the day it does.
|
|
*/
|
|
$sql_recurring_expenses = mysqli_query($mysqli, "SELECT recurring_expense_id, recurring_expense_next_date FROM recurring_expenses
|
|
WHERE " . recurringExpenseRunningSql() . "
|
|
AND recurring_expense_next_date <= CURDATE() AND recurring_expense_next_date >= CURDATE() - INTERVAL " . RECURRING_EXPENSE_GRACE_DAYS . " DAY
|
|
ORDER BY recurring_expense_next_date ASC, recurring_expense_id ASC");
|
|
while ($row = mysqli_fetch_assoc($sql_recurring_expenses)) {
|
|
$expense_id = recurringExpenseRecord(intval($row['recurring_expense_id']), 0, $row['recurring_expense_next_date']);
|
|
if ($expense_id) {
|
|
$expense = expenseRow($expense_id, false);
|
|
appNotify("Expense Created", escapeSql("Recorded " . expenseLabel($expense) . " from the recurring expense"), "/agent/expenses.php?open=$expense_id", intval($expense['expense_client_id']), $expense_id);
|
|
}
|
|
}
|
|
|
|
$sql_recurring_expenses = mysqli_query($mysqli, "SELECT recurring_expense_id FROM recurring_expenses
|
|
WHERE " . recurringExpenseRunningSql() . " AND recurring_expense_next_date = CURDATE() - INTERVAL " . (RECURRING_EXPENSE_GRACE_DAYS + 1) . " DAY");
|
|
while ($row = mysqli_fetch_assoc($sql_recurring_expenses)) {
|
|
$recurring = recurringExpenseRow(intval($row['recurring_expense_id']), false);
|
|
appNotify("Expense", escapeSql("The recurring expense " . recurringExpenseLabel($recurring) . " is " . (RECURRING_EXPENSE_GRACE_DAYS + 1) . " days behind and won't be recorded by itself - record the missed runs or move it on"),
|
|
"/agent/recurring_expenses.php?status=late", intval($recurring['recurring_expense_client_id']));
|
|
}
|
|
|
|
// 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");
|