38471-vm/includes/wablas_helper.php
2026-05-03 07:32:13 +00:00

1091 lines
39 KiB
PHP

<?php
declare(strict_types=1);
if (!function_exists('wablasTableExists')) {
function wablasTableExists(string $tableName): bool
{
static $cache = [];
$normalized = strtolower(trim($tableName));
if ($normalized === '') {
return false;
}
if (array_key_exists($normalized, $cache)) {
return $cache[$normalized];
}
try {
$stmt = db()->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;
}
}