prepare('SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1'); $stmt->execute([$tableName]); $cache[$normalized] = (bool) $stmt->fetchColumn(); } catch (Throwable $e) { $cache[$normalized] = false; } return $cache[$normalized]; } } if (!function_exists('wablasColumnExists')) { function wablasColumnExists(string $tableName, string $columnName): bool { static $cache = []; $cacheKey = strtolower(trim($tableName) . '.' . trim($columnName)); if ($cacheKey === '.') { return false; } if (array_key_exists($cacheKey, $cache)) { return $cache[$cacheKey]; } try { $stmt = db()->prepare('SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1'); $stmt->execute([$tableName, $columnName]); $cache[$cacheKey] = (bool) $stmt->fetchColumn(); } catch (Throwable $e) { $cache[$cacheKey] = false; } return $cache[$cacheKey]; } } if (!function_exists('wablasFirstExistingColumn')) { function wablasFirstExistingColumn(string $tableName, array $columnNames): ?string { foreach ($columnNames as $columnName) { if (!is_string($columnName) || $columnName === '') { continue; } if (wablasColumnExists($tableName, $columnName)) { return $columnName; } } return null; } } if (!function_exists('wablasSettingsMap')) { function wablasSettingsMap(array $keys = [], bool $forceReload = false): array { static $cache = []; static $allLoaded = false; if ($forceReload) { $cache = []; $allLoaded = false; } $normalizedKeys = []; foreach ($keys as $key) { $key = trim((string) $key); if ($key !== '') { $normalizedKeys[$key] = true; } } $normalizedKeys = array_keys($normalizedKeys); if ($normalizedKeys === []) { if (!$allLoaded) { try { $stmt = db()->query('SELECT `key`, `value` FROM settings'); $rows = $stmt ? ($stmt->fetchAll(PDO::FETCH_KEY_PAIR) ?: []) : []; foreach ($rows as $key => $value) { $cache[(string) $key] = (string) $value; } $allLoaded = true; } catch (Throwable $e) { // Ignore and return what we have. } } return $cache; } $missing = []; foreach ($normalizedKeys as $key) { if (!array_key_exists($key, $cache)) { $missing[] = $key; } } if ($missing !== []) { try { $placeholders = implode(', ', array_fill(0, count($missing), '?')); $stmt = db()->prepare("SELECT `key`, `value` FROM settings WHERE `key` IN ($placeholders)"); $stmt->execute($missing); $rows = $stmt->fetchAll(PDO::FETCH_KEY_PAIR) ?: []; foreach ($missing as $key) { $cache[$key] = array_key_exists($key, $rows) ? (string) $rows[$key] : ''; } } catch (Throwable $e) { foreach ($missing as $key) { $cache[$key] = ''; } } } $result = []; foreach ($normalizedKeys as $key) { $result[$key] = $cache[$key] ?? ''; } return $result; } } if (!function_exists('wablasSettingValue')) { function wablasSettingValue(string $key, ?string $default = null): ?string { $key = trim($key); if ($key === '') { return $default; } $settings = wablasSettingsMap([$key]); $value = $settings[$key] ?? ''; if ($value === '') { return $default; } return $value; } } if (!function_exists('wablasSettingEnabled')) { function wablasSettingEnabled(string $key): bool { return wablasSettingValue($key, '0') === '1'; } } if (!function_exists('wablasAppTimezone')) { function wablasAppTimezone(): DateTimeZone { $configured = trim((string) (wablasSettingValue('timezone', date_default_timezone_get()) ?? date_default_timezone_get())); try { return new DateTimeZone($configured !== '' ? $configured : 'UTC'); } catch (Throwable $e) { return new DateTimeZone('UTC'); } } } if (!function_exists('wablasNow')) { function wablasNow(): DateTimeImmutable { return new DateTimeImmutable('now', wablasAppTimezone()); } } if (!function_exists('wablasFormatMoney')) { function wablasFormatMoney(float $amount): string { return number_format($amount, 3, '.', ''); } } if (!function_exists('wablasCurrentBaseUrl')) { function wablasCurrentBaseUrl(): string { $host = trim((string) ($_SERVER['HTTP_HOST'] ?? '')); if ($host === '') { return ''; } $scheme = 'http'; $forwardedProto = strtolower(trim((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''))); if ($forwardedProto === 'https' || (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off')) { $scheme = 'https'; } return $scheme . '://' . $host; } } if (!function_exists('wablasInvoiceRouteUrl')) { function wablasInvoiceRouteUrl(int $invoiceId, string $baseUrl = ''): string { $relative = function_exists('page_url') ? page_url('sales', ['invoice_id' => $invoiceId]) : ('sales.php?invoice_id=' . $invoiceId); $baseUrl = trim($baseUrl); if ($baseUrl === '') { return $relative; } return rtrim($baseUrl, '/') . '/' . ltrim($relative, '/'); } } if (!function_exists('wablasNormalizePhone')) { function wablasNormalizePhone(string $number, string $defaultCountryCode = ''): ?string { $raw = trim($number); if ($raw === '') { return null; } $digits = preg_replace('/\D+/', '', $raw) ?? ''; if ($digits === '') { return null; } $countryDigits = preg_replace('/\D+/', '', $defaultCountryCode) ?? ''; if ($countryDigits !== '' && !str_starts_with($countryDigits, '0')) { // keep as-is } if (str_starts_with($raw, '+')) { return $digits; } if ($countryDigits !== '' && str_starts_with($digits, '0')) { return $countryDigits . ltrim($digits, '0'); } return $digits; } } if (!function_exists('wablasNumbersFromText')) { function wablasNumbersFromText(string $rawNumbers, string $defaultCountryCode = ''): array { $rawNumbers = str_replace(["\r\n", "\r"], "\n", $rawNumbers); $parts = preg_split('/[\n,;]+/', $rawNumbers) ?: []; $normalizedNumbers = []; foreach ($parts as $part) { $normalized = wablasNormalizePhone((string) $part, $defaultCountryCode); if ($normalized !== null && $normalized !== '') { $normalizedNumbers[$normalized] = true; } } return array_slice(array_keys($normalizedNumbers), 0, 100); } } if (!function_exists('wablasRecipientNumbersFromSetting')) { function wablasRecipientNumbersFromSetting(string $settingKey): array { $raw = str_replace([" ", " "], " ", (string) (wablasSettingValue($settingKey, '') ?? '')); $defaultCountryCode = (string) (wablasSettingValue('wablas_default_country_code', '') ?? ''); $seen = []; foreach (preg_split('/[ ,]+/', $raw) ?: [] as $part) { $normalized = wablasNormalizePhone((string) $part, $defaultCountryCode); if ($normalized === null) { continue; } $seen[$normalized] = true; } return array_keys($seen); } } if (!function_exists('wablasRenderTemplate')) { function wablasRenderTemplate(string $template, array $placeholders): string { $replacements = []; foreach ($placeholders as $key => $value) { if (!is_string($key) || $key === '') { continue; } $replacements['{' . $key . '}'] = trim((string) $value); } $rendered = strtr($template, $replacements); $rendered = preg_replace("/ {3,}/", " ", $rendered) ?? $rendered; return trim($rendered); } } if (!function_exists('wablasDefaultInvoiceTemplate')) { function wablasDefaultInvoiceTemplate(): string { return 'Hello {customer_name}, your invoice {invoice_no} total is {grand_total}. View: {invoice_url}'; } } if (!function_exists('wablasDefaultSummaryTemplate')) { function wablasDefaultSummaryTemplate(): string { return 'Daily summary {report_date}: invoices {invoice_count}, sales {sales_total}, cash {cash_total}, card {card_total}'; } } if (!function_exists('wablasJsonEncode')) { function wablasJsonEncode(mixed $value): string { $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); return $json === false ? '{}' : $json; } } if (!function_exists('wablasJsonDecode')) { function wablasJsonDecode(?string $json): array { if (!is_string($json) || trim($json) === '') { return []; } $decoded = json_decode($json, true); return is_array($decoded) ? $decoded : []; } } if (!function_exists('wablasBuildInvoiceContext')) { function wablasBuildInvoiceContext(int $invoiceId, array $options = []): ?array { if ($invoiceId <= 0 || !wablasTableExists('invoices')) { return null; } $taxColumn = wablasFirstExistingColumn('customers', ['tax_id', 'tax_number', 'vat_number', 'tax_no', 'vat_no', 'trn']); $taxSelect = $taxColumn !== null ? "c.`$taxColumn` AS customer_tax_id" : "'' AS customer_tax_id"; $outletSelect = "'' AS outlet_name"; $outletJoin = ''; if (wablasTableExists('outlets') && wablasColumnExists('invoices', 'outlet_id') && wablasColumnExists('outlets', 'name')) { $outletSelect = 'o.name AS outlet_name'; $outletJoin = 'LEFT JOIN outlets o ON i.outlet_id = o.id'; } $stmt = db()->prepare( "SELECT i.id, i.transaction_no, i.invoice_date, i.due_date, i.status, i.payment_type, i.total_amount, i.vat_amount, i.total_with_vat, i.paid_amount, c.name AS customer_name, c.phone AS customer_phone, $taxSelect, $outletSelect FROM invoices i LEFT JOIN customers c ON i.customer_id = c.id $outletJoin WHERE i.id = ? LIMIT 1" ); $stmt->execute([$invoiceId]); $invoice = $stmt->fetch(PDO::FETCH_ASSOC); if (!is_array($invoice) || $invoice === []) { return null; } $grandTotal = (float) ($invoice['total_with_vat'] ?? (((float) ($invoice['total_amount'] ?? 0)) + ((float) ($invoice['vat_amount'] ?? 0)))); $paidAmount = (float) ($invoice['paid_amount'] ?? 0); $invoiceNo = trim((string) ($invoice['transaction_no'] ?? '')); if ($invoiceNo === '') { $invoiceNo = 'INV-' . str_pad((string) $invoiceId, 5, '0', STR_PAD_LEFT); } $status = trim((string) ($invoice['status'] ?? '')); if ($status !== '') { $status = ucwords(str_replace('_', ' ', $status)); } $baseUrl = trim((string) ($options['base_url'] ?? wablasCurrentBaseUrl())); $companyName = (string) (wablasSettingValue('company_name', 'Your Company') ?? 'Your Company'); return [ 'invoice_id' => (string) $invoiceId, 'invoice_no' => $invoiceNo, 'customer_name' => (string) ($invoice['customer_name'] ?? 'Customer'), 'customer_phone' => (string) ($invoice['customer_phone'] ?? ''), 'customer_tax_id' => (string) ($invoice['customer_tax_id'] ?? ''), 'invoice_date' => (string) ($invoice['invoice_date'] ?? ''), 'due_date' => (string) ($invoice['due_date'] ?? ''), 'status' => $status, 'payment_type' => strtoupper((string) ($invoice['payment_type'] ?? '')), 'grand_total' => wablasFormatMoney($grandTotal), 'total_amount' => wablasFormatMoney((float) ($invoice['total_amount'] ?? 0)), 'vat_amount' => wablasFormatMoney((float) ($invoice['vat_amount'] ?? 0)), 'paid_amount' => wablasFormatMoney($paidAmount), 'balance_due' => wablasFormatMoney(max($grandTotal - $paidAmount, 0.0)), 'company_name' => $companyName, 'outlet_name' => (string) ($invoice['outlet_name'] ?? ''), 'invoice_url' => wablasInvoiceRouteUrl($invoiceId, $baseUrl), 'wablas_sender' => (string) (wablasSettingValue('wablas_sender', '') ?? ''), ]; } } if (!function_exists('wablasBuildDailySummaryContext')) { function wablasBuildDailySummaryContext(string $reportDate, ?DateTimeImmutable $at = null): array { $reportDate = trim($reportDate); $at = $at ?? wablasNow(); $sql = "SELECT COUNT(*) AS invoice_count, COALESCE(SUM(total_with_vat), 0) AS sales_total, COALESCE(SUM(paid_amount), 0) AS paid_total, COALESCE(SUM(CASE WHEN LOWER(COALESCE(payment_type, '')) LIKE '%cash%' THEN paid_amount ELSE 0 END), 0) AS cash_total, COALESCE(SUM(CASE WHEN LOWER(COALESCE(payment_type, '')) LIKE '%card%' OR LOWER(COALESCE(payment_type, '')) LIKE '%visa%' OR LOWER(COALESCE(payment_type, '')) LIKE '%master%' OR LOWER(COALESCE(payment_type, '')) LIKE '%credit%' OR LOWER(COALESCE(payment_type, '')) LIKE '%debit%' OR LOWER(COALESCE(payment_type, '')) LIKE '%bank%' OR LOWER(COALESCE(payment_type, '')) LIKE '%pos%' THEN paid_amount ELSE 0 END), 0) AS card_total, COALESCE(SUM(CASE WHEN COALESCE(status, '') IN ('unpaid', 'partially_paid', 'pending') THEN GREATEST(total_with_vat - paid_amount, 0) ELSE 0 END), 0) AS due_total FROM invoices WHERE invoice_date = ? AND (type = 'sale' OR type IS NULL OR type = '')"; $stmt = db()->prepare($sql); $stmt->execute([$reportDate]); $row = $stmt->fetch(PDO::FETCH_ASSOC) ?: []; return [ 'report_date' => $reportDate, 'report_time' => $at->format('H:i'), 'invoice_count' => (string) ((int) ($row['invoice_count'] ?? 0)), 'sales_total' => wablasFormatMoney((float) ($row['sales_total'] ?? 0)), 'paid_total' => wablasFormatMoney((float) ($row['paid_total'] ?? 0)), 'cash_total' => wablasFormatMoney((float) ($row['cash_total'] ?? 0)), 'card_total' => wablasFormatMoney((float) ($row['card_total'] ?? 0)), 'due_total' => wablasFormatMoney((float) ($row['due_total'] ?? 0)), 'company_name' => (string) (wablasSettingValue('company_name', 'Your Company') ?? 'Your Company'), 'wablas_sender' => (string) (wablasSettingValue('wablas_sender', '') ?? ''), ]; } } if (!function_exists('wablasInvoiceScheduledFor')) { function wablasInvoiceScheduledFor(?string $timeValue, ?DateTimeImmutable $base = null): DateTimeImmutable { $base = $base ?? wablasNow(); $timeValue = trim((string) $timeValue); if ($timeValue === '' || !preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/', $timeValue)) { return $base; } $candidate = DateTimeImmutable::createFromFormat('Y-m-d H:i', $base->format('Y-m-d') . ' ' . $timeValue, $base->getTimezone()); if (!$candidate instanceof DateTimeImmutable) { return $base; } if ($candidate->format('Y-m-d H:i') === $base->format('Y-m-d H:i')) { return $base; } if ($candidate < $base) { return $candidate->modify('+1 day'); } return $candidate; } } if (!function_exists('wablasDailySummaryDueAt')) { function wablasDailySummaryDueAt(?string $timeValue, ?DateTimeImmutable $base = null): DateTimeImmutable { $base = $base ?? wablasNow(); $timeValue = trim((string) $timeValue); if ($timeValue === '' || !preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/', $timeValue)) { $timeValue = '20:00'; } $candidate = DateTimeImmutable::createFromFormat('Y-m-d H:i', $base->format('Y-m-d') . ' ' . $timeValue, $base->getTimezone()); return $candidate instanceof DateTimeImmutable ? $candidate : $base->setTime(20, 0); } } if (!function_exists('wablasDispatchLogTableReady')) { function wablasDispatchLogTableReady(): bool { return wablasTableExists('wablas_dispatch_logs'); } } if (!function_exists('wablasFetchDispatchLog')) { function wablasFetchDispatchLog(string $eventType, string $eventKey): ?array { if (!wablasDispatchLogTableReady()) { return null; } $stmt = db()->prepare('SELECT * FROM wablas_dispatch_logs WHERE channel = ? AND event_type = ? AND event_key = ? LIMIT 1'); $stmt->execute(['wablas', $eventType, $eventKey]); $row = $stmt->fetch(PDO::FETCH_ASSOC); return is_array($row) ? $row : null; } } if (!function_exists('wablasFetchRecentDispatchLogs')) { function wablasFetchRecentDispatchLogs(int $limit = 25, array $filters = []): array { if (!wablasDispatchLogTableReady()) { return []; } $limit = max(1, min(100, $limit)); $conditions = ["channel = 'wablas'"]; $params = []; $status = trim((string) ($filters['status'] ?? '')); if ($status !== '' && in_array($status, ['pending', 'sent', 'failed'], true)) { $conditions[] = 'status = ?'; $params[] = $status; } $eventType = trim((string) ($filters['event_type'] ?? '')); if ($eventType !== '' && in_array($eventType, ['invoice', 'daily_summary', 'manual_test'], true)) { $conditions[] = 'event_type = ?'; $params[] = $eventType; } $sql = 'SELECT * FROM wablas_dispatch_logs WHERE ' . implode(' AND ', $conditions) . ' ORDER BY id DESC LIMIT ' . $limit; $stmt = db()->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; } } if (!function_exists('wablasDispatchEventLabel')) { function wablasDispatchEventLabel(string $eventType): string { return match ($eventType) { 'invoice' => 'Invoice', 'daily_summary' => 'Daily Summary', 'manual_test' => 'Manual Test', default => ucwords(str_replace('_', ' ', trim($eventType))) ?: 'Dispatch', }; } } if (!function_exists('wablasDispatchStatusMeta')) { function wablasDispatchStatusMeta(string $status): array { return match (trim($status)) { 'sent' => ['label' => 'Sent', 'class' => 'bg-success bg-opacity-10 text-success'], 'failed' => ['label' => 'Failed', 'class' => 'bg-danger bg-opacity-10 text-danger'], default => ['label' => 'Pending', 'class' => 'bg-warning bg-opacity-10 text-warning'], }; } } if (!function_exists('wablasQueueDispatch')) { function wablasQueueDispatch(string $eventType, string $eventKey, DateTimeImmutable $scheduledFor, array $payload, int $recipientCount): bool { if (!wablasDispatchLogTableReady()) { return false; } $stmt = db()->prepare( 'INSERT INTO wablas_dispatch_logs ( channel, event_type, event_key, scheduled_for, last_attempt_at, attempt_count, recipient_count, status, request_payload, response_payload, error_message ) VALUES ( ?, ?, ?, ?, NULL, 0, ?, ?, ?, NULL, NULL ) ON DUPLICATE KEY UPDATE scheduled_for = VALUES(scheduled_for), last_attempt_at = NULL, attempt_count = 0, recipient_count = VALUES(recipient_count), status = VALUES(status), request_payload = VALUES(request_payload), response_payload = NULL, error_message = NULL, updated_at = CURRENT_TIMESTAMP' ); return $stmt->execute([ 'wablas', $eventType, $eventKey, $scheduledFor->format('Y-m-d H:i:s'), max(0, $recipientCount), 'pending', wablasJsonEncode($payload), ]); } } if (!function_exists('wablasFetchDueDispatches')) { function wablasFetchDueDispatches(int $limit = 20): array { if (!wablasDispatchLogTableReady()) { return []; } $limit = max(1, min(100, $limit)); $stmt = db()->prepare( "SELECT * FROM wablas_dispatch_logs WHERE channel = 'wablas' AND status IN ('pending', 'failed') AND attempt_count < 5 AND (scheduled_for IS NULL OR scheduled_for <= ?) ORDER BY scheduled_for ASC, id ASC LIMIT $limit" ); $stmt->execute([wablasNow()->format('Y-m-d H:i:s')]); return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; } } if (!function_exists('wablasMarkDispatchResult')) { function wablasMarkDispatchResult(int $id, string $status, array $response = [], ?string $errorMessage = null): void { if (!wablasDispatchLogTableReady() || $id <= 0) { return; } $stmt = db()->prepare( 'UPDATE wablas_dispatch_logs SET status = ?, response_payload = ?, error_message = ?, last_attempt_at = NOW(), attempt_count = attempt_count + 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?' ); $stmt->execute([ $status, wablasJsonEncode($response), $errorMessage !== null ? substr($errorMessage, 0, 4000) : null, $id, ]); } } if (!function_exists('wablasApiConfig')) { function wablasApiConfig(): array { return wablasSettingsMap(['wablas_api_url', 'wablas_token', 'wablas_security_key']); } } if (!function_exists('wablasApiReady')) { function wablasApiReady(): bool { $config = wablasApiConfig(); return trim((string) ($config['wablas_api_url'] ?? '')) !== '' && trim((string) ($config['wablas_token'] ?? '')) !== '' && trim((string) ($config['wablas_security_key'] ?? '')) !== ''; } } if (!function_exists('wablasEndpointUrl')) { function wablasEndpointUrl(string $path): string { $base = trim((string) (wablasSettingValue('wablas_api_url', '') ?? '')); if ($base === '') { return ''; } $base = rtrim($base, '/'); $base = preg_replace('#/api/.*$#i', '', $base) ?? $base; return $base . '/' . ltrim($path, '/'); } } if (!function_exists('wablasSendText')) { function wablasSendText(array $numbers, string $message, array $options = []): array { $numbers = array_values(array_filter(array_map(static function ($value): string { return trim((string) $value); }, $numbers), static function (string $value): bool { return $value !== ''; })); $message = trim($message); if ($numbers === []) { return ['success' => false, 'error' => 'Missing recipient numbers.']; } if ($message === '') { return ['success' => false, 'error' => 'Message body is empty.']; } if (!wablasApiReady()) { return ['success' => false, 'error' => 'Wablas API is not fully configured.']; } $config = wablasApiConfig(); $auth = trim((string) ($config['wablas_token'] ?? '')) . '.' . trim((string) ($config['wablas_security_key'] ?? '')); $endpoint = wablasEndpointUrl('/api/v2/send-message'); if ($endpoint === '') { return ['success' => false, 'error' => 'Wablas API URL is empty.']; } $payload = ['data' => []]; $refId = trim((string) ($options['ref_id'] ?? '')); $source = trim((string) ($options['source'] ?? 'admin-panel')); foreach ($numbers as $index => $phone) { $item = [ 'phone' => $phone, 'message' => $message, 'isGroup' => false, ]; if ($refId !== '') { $item['ref_id'] = count($numbers) > 1 ? ($refId . '-' . ($index + 1)) : $refId; } if ($source !== '') { $item['source'] = $source; } $payload['data'][] = $item; } $jsonPayload = wablasJsonEncode($payload); $ch = curl_init($endpoint); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: ' . $auth, 'Content-Type: application/json', 'Accept: application/json', ], CURLOPT_POSTFIELDS => $jsonPayload, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30, ]); $rawResponse = curl_exec($ch); $curlError = curl_error($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($rawResponse === false) { return [ 'success' => false, 'error' => $curlError !== '' ? $curlError : 'Unknown cURL error.', 'http_code' => $httpCode, 'payload' => $payload, ]; } $decoded = json_decode($rawResponse, true); $statusValue = is_array($decoded) ? ($decoded['status'] ?? $decoded['success'] ?? null) : null; $success = $httpCode >= 200 && $httpCode < 300 && ( $statusValue === true || $statusValue === 1 || $statusValue === '1' || $statusValue === 'true' || $statusValue === null ); return [ 'success' => $success, 'error' => $success ? null : (is_array($decoded) ? ((string) ($decoded['message'] ?? $decoded['error'] ?? 'Wablas request failed.')) : ('HTTP ' . $httpCode)), 'http_code' => $httpCode, 'payload' => $payload, 'response' => is_array($decoded) ? $decoded : ['raw' => (string) $rawResponse], ]; } } if (!function_exists('wablasSendManualTest')) { function wablasSendManualTest(array $numbers, string $message): array { $numbers = array_values(array_filter(array_map(static function ($value): string { return trim((string) $value); }, $numbers), static function (string $value): bool { return $value !== ''; })); $message = trim($message); if ($numbers === []) { return ['success' => false, 'error' => 'Missing recipient numbers.']; } if ($message === '') { return ['success' => false, 'error' => 'Message body is empty.']; } $now = wablasNow(); try { $suffix = bin2hex(random_bytes(4)); } catch (Throwable $e) { $suffix = substr(str_replace('.', '', uniqid('', true)), -8); } $eventKey = $now->format('YmdHis') . '-' . $suffix; $payload = [ 'numbers' => $numbers, 'message' => $message, 'ref_id' => 'manual-test-' . $eventKey, 'source' => 'settings-manual-test', 'meta' => [ 'manual' => true, ], ]; $logId = 0; if (wablasDispatchLogTableReady()) { $queued = wablasQueueDispatch('manual_test', $eventKey, $now, $payload, count($numbers)); if ($queued) { $log = wablasFetchDispatchLog('manual_test', $eventKey); $logId = (int) ($log['id'] ?? 0); } } $result = wablasSendText($numbers, $message, [ 'ref_id' => 'manual-test-' . $eventKey, 'source' => 'settings-manual-test', ]); if ($logId > 0) { wablasMarkDispatchResult( $logId, !empty($result['success']) ? 'sent' : 'failed', $result, !empty($result['success']) ? null : (string) ($result['error'] ?? 'Unknown Wablas error.') ); } return [ 'success' => !empty($result['success']), 'event_key' => $eventKey, 'log_id' => $logId, 'recipient_count' => count($numbers), 'http_code' => (int) ($result['http_code'] ?? 0), 'error' => $result['error'] ?? null, 'payload' => $result['payload'] ?? $payload, 'response' => $result['response'] ?? [], ]; } } if (!function_exists('wablasQueueInvoiceNotification')) { function wablasQueueInvoiceNotification(int $invoiceId): array { if ($invoiceId <= 0) { return ['success' => false, 'error' => 'Invalid invoice ID.']; } if (!wablasSettingEnabled('wablas_enabled') || !wablasSettingEnabled('wablas_invoice_enabled')) { return ['success' => false, 'skipped' => true, 'reason' => 'disabled', 'notice' => '']; } if (!wablasDispatchLogTableReady()) { return ['success' => false, 'error' => 'Wablas dispatch log table is not available.']; } $numbers = wablasRecipientNumbersFromSetting('wablas_invoice_numbers'); if ($numbers === []) { return ['success' => false, 'skipped' => true, 'reason' => 'no_recipients', 'notice' => ' WhatsApp skipped: add invoice recipient numbers in Settings.']; } $context = wablasBuildInvoiceContext($invoiceId, ['base_url' => wablasCurrentBaseUrl()]); if ($context === null) { return ['success' => false, 'error' => 'Invoice not found.']; } $eventType = 'invoice'; $eventKey = 'sale:' . $invoiceId; $existing = wablasFetchDispatchLog($eventType, $eventKey); if ($existing !== null && in_array((string) ($existing['status'] ?? ''), ['pending', 'sent'], true)) { return [ 'success' => true, 'queued' => (string) ($existing['status'] ?? '') === 'pending', 'skipped' => (string) ($existing['status'] ?? '') === 'sent', 'reason' => 'already_exists', 'scheduled_for' => (string) ($existing['scheduled_for'] ?? ''), 'notice' => '', ]; } $template = trim((string) (wablasSettingValue('wablas_invoice_template', wablasDefaultInvoiceTemplate()) ?? wablasDefaultInvoiceTemplate())); $message = wablasRenderTemplate($template !== '' ? $template : wablasDefaultInvoiceTemplate(), $context); if ($message === '') { return ['success' => false, 'skipped' => true, 'reason' => 'empty_message', 'notice' => '']; } $scheduledFor = wablasInvoiceScheduledFor(wablasSettingValue('wablas_invoice_time', ''), wablasNow()); $payload = [ 'numbers' => $numbers, 'message' => $message, 'ref_id' => 'invoice-' . $invoiceId, 'source' => 'admin-panel', 'meta' => [ 'invoice_id' => $invoiceId, 'invoice_no' => $context['invoice_no'] ?? '', 'customer_name' => $context['customer_name'] ?? '', ], ]; $queued = wablasQueueDispatch($eventType, $eventKey, $scheduledFor, $payload, count($numbers)); if (!$queued) { return ['success' => false, 'error' => 'Unable to queue the invoice notification.']; } $notice = ' WhatsApp queued for ' . $scheduledFor->format('Y-m-d H:i') . '.'; if ($scheduledFor->format('Y-m-d H:i') === wablasNow()->format('Y-m-d H:i')) { $notice = ' WhatsApp queued for dispatch.'; } return [ 'success' => true, 'queued' => true, 'event_key' => $eventKey, 'scheduled_for' => $scheduledFor->format('Y-m-d H:i:s'), 'scheduled_for_label' => $scheduledFor->format('Y-m-d H:i'), 'notice' => $notice, ]; } } if (!function_exists('wablasQueueDailySummaryIfDue')) { function wablasQueueDailySummaryIfDue(?DateTimeImmutable $now = null): array { $now = $now ?? wablasNow(); if (!wablasSettingEnabled('wablas_enabled') || !wablasSettingEnabled('wablas_daily_summary_enabled')) { return ['success' => false, 'skipped' => true, 'reason' => 'disabled']; } if (!wablasDispatchLogTableReady()) { return ['success' => false, 'error' => 'Wablas dispatch log table is not available.']; } $dueAt = wablasDailySummaryDueAt(wablasSettingValue('wablas_daily_summary_time', '20:00'), $now); if ($now < $dueAt) { return [ 'success' => false, 'skipped' => true, 'reason' => 'not_due', 'scheduled_for' => $dueAt->format('Y-m-d H:i:s'), ]; } $numbers = wablasRecipientNumbersFromSetting('wablas_daily_summary_numbers'); if ($numbers === []) { return ['success' => false, 'skipped' => true, 'reason' => 'no_recipients']; } $reportDate = $now->format('Y-m-d'); $eventType = 'daily_summary'; $eventKey = $reportDate; $existing = wablasFetchDispatchLog($eventType, $eventKey); if ($existing !== null && in_array((string) ($existing['status'] ?? ''), ['pending', 'sent', 'failed'], true)) { return [ 'success' => true, 'queued' => (string) ($existing['status'] ?? '') === 'pending', 'skipped' => (string) ($existing['status'] ?? '') === 'sent', 'reason' => 'already_exists', 'status' => (string) ($existing['status'] ?? ''), ]; } $context = wablasBuildDailySummaryContext($reportDate, $now); $template = trim((string) (wablasSettingValue('wablas_daily_summary_template', wablasDefaultSummaryTemplate()) ?? wablasDefaultSummaryTemplate())); $message = wablasRenderTemplate($template !== '' ? $template : wablasDefaultSummaryTemplate(), $context); if ($message === '') { return ['success' => false, 'skipped' => true, 'reason' => 'empty_message']; } $payload = [ 'numbers' => $numbers, 'message' => $message, 'ref_id' => 'daily-summary-' . $reportDate, 'source' => 'admin-panel', 'meta' => [ 'report_date' => $reportDate, ], ]; $queued = wablasQueueDispatch($eventType, $eventKey, $dueAt, $payload, count($numbers)); if (!$queued) { return ['success' => false, 'error' => 'Unable to queue the daily summary.']; } return [ 'success' => true, 'queued' => true, 'event_key' => $eventKey, 'scheduled_for' => $dueAt->format('Y-m-d H:i:s'), ]; } } if (!function_exists('wablasProcessDueDispatches')) { function wablasProcessDueDispatches(int $limit = 20): array { $summary = [ 'checked' => 0, 'sent' => 0, 'failed' => 0, 'skipped' => 0, 'messages' => [], ]; if (!wablasDispatchLogTableReady()) { $summary['messages'][] = 'Dispatch log table is missing.'; return $summary; } if (!wablasSettingEnabled('wablas_enabled')) { $summary['messages'][] = 'Wablas automation is disabled from Settings.'; return $summary; } if (!wablasApiReady()) { $summary['messages'][] = 'Wablas API URL, token, or security key is missing.'; return $summary; } foreach (wablasFetchDueDispatches($limit) as $row) { $summary['checked']++; $eventType = (string) ($row['event_type'] ?? ''); if ($eventType === 'invoice' && !wablasSettingEnabled('wablas_invoice_enabled')) { $summary['skipped']++; $summary['messages'][] = 'Invoice automation is disabled; queued invoice ' . ((string) ($row['event_key'] ?? '')) . ' remains pending.'; continue; } if ($eventType === 'daily_summary' && !wablasSettingEnabled('wablas_daily_summary_enabled')) { $summary['skipped']++; $summary['messages'][] = 'Daily summary automation is disabled; ' . ((string) ($row['event_key'] ?? '')) . ' remains pending.'; continue; } $payload = wablasJsonDecode((string) ($row['request_payload'] ?? '')); $numbers = isset($payload['numbers']) && is_array($payload['numbers']) ? $payload['numbers'] : []; $message = trim((string) ($payload['message'] ?? '')); if ($numbers === [] || $message === '') { wablasMarkDispatchResult((int) ($row['id'] ?? 0), 'failed', ['payload' => $payload], 'Queued payload is incomplete.'); $summary['failed']++; $summary['messages'][] = 'Failed ' . $eventType . ' ' . ((string) ($row['event_key'] ?? '')) . ': queued payload is incomplete.'; continue; } $result = wablasSendText($numbers, $message, [ 'ref_id' => (string) ($payload['ref_id'] ?? ($eventType . '-' . ($row['event_key'] ?? ''))), 'source' => (string) ($payload['source'] ?? 'admin-panel'), ]); if (!empty($result['success'])) { wablasMarkDispatchResult((int) ($row['id'] ?? 0), 'sent', $result, null); $summary['sent']++; $summary['messages'][] = 'Sent ' . $eventType . ' ' . ((string) ($row['event_key'] ?? '')) . ' to ' . count($numbers) . ' recipient(s).'; } else { wablasMarkDispatchResult((int) ($row['id'] ?? 0), 'failed', $result, (string) ($result['error'] ?? 'Unknown Wablas error.')); $summary['failed']++; $summary['messages'][] = 'Failed ' . $eventType . ' ' . ((string) ($row['event_key'] ?? '')) . ': ' . ((string) ($result['error'] ?? 'Unknown Wablas error.')); } } return $summary; } }