## Why Customer `k.sand@ksand.dk` reported never receiving wash certificates for completed bookings. Two methods contained silent early-return guards so the actual reason was unobservable from container logs: - `order_bookings_o::sendWashCertificateToCustomer()` — 5 silent returns - `email::sendWashCertificateEmailToCustomer()` — 1 silent return The most likely root cause: `email_notifications_enabled` defaults to `0` in the schema and `users.add()` does not set it on insert, so newly imported customers have notifications off until toggled. `wantsEmailNotifications()` then returns false and the email silently skips. ## What changed ### Original commit (`0ead5de5`) - `objects/order_bookings_o.php` — all 5 silent early-returns now log via new `logWashCertificateSkip()` helper (Redis stream `module=email / action=WASH_CERT_SKIP` + `error_log('[wash-cert-skip] …')`). - `classes/email.php` — silent `hasTransaction()` return in `sendWashCertificateEmailToCustomer()` now logs too. - `objects/bookings_o.php` — emits `WASH_CERT_SKIP` (legacy_no_wash_certificate_email) when `washCertificateEmail` is empty; no behavioural change. - **New** `routes/washCertificateDebugRoute.php` — `GET /debug/wash-certificates/diagnose?customer_number=&from=&to=` (404 in prod via $DEBUG; superuser-auth otherwise) replays the decision tree and reports `blocking_reason` per booking. ### Follow-up commit (`46a59e4e`) — silent-failure sweep **PART A — silent returns / silent errors (10 fixes):** - `email::sendEmailMailerSend()` — blacklisted-recipient skip now logs with context. - `email::sendNewCustomerRegistrationNotifications()` — empty-email skip + per-recipient try/catch with error_log (was unprotected; a single MailerSend error broke the loop). - `bookings_new_o::generateWashCertificate()` — wrapped `sendWashCertificateEmail()` in try/catch with error_log and re-throw (same pattern as the k.sand fix). - `users_o::getCustomerName()` — replaced catch-and-swallow with structured error_log. - `users_o::getCustomerEcocomicData()` — same. - `bookingsRoute.php` — added booking-id context to 4 × `$response->error('Booking not found', 404)` calls. **PART B — cron paths (10 files):** Added error_log breadcrumb + try/catch to `CheckUnfulfilledBookings`, `ClearAllUsersEconomicCustomerDetails`, `ClearAllUsersEconomicCustomerDiscounts`, `RunXLVaskModuleCron`, `SyncBookings`, `SyncEconomicInvoiceStatus`, `SyncLogs`, `BackfillEconomicV2History`, `EnsureXLVaskAutomationSchema`, and 3 functions in `Cron.php`. Each uses a distinct `[cron-…]` prefix for grep-ability. **PART C — real bugs (2 fixed):** 1. `email::sendEmailMailerSend()` attachment `array_map` — the previous exception message emitted a binary blob because `$attachment[0]` was already overwritten by `file_get_contents()`. Now captures $path first. 2. `bookings_new_o::generateWashCertificate()` — booking persisted as `completed` before email was sent, with no try/catch. Fixed (see PART A). ## How to verify 1. Deploy to staging. 2. Hit `/debug/wash-certificates/diagnose?customer_number=<k.sand's customer_number>` as a superuser — the response lists every booking's `blocking_reason`. 3. Tail container logs for `[wash-cert-skip]`, `[email-skip]`, `[cron-…]`, and Redis stream `module=email` action `WASH_CERT_SKIP` to see real-world skips going forward. ## Follow-ups (out of scope) - Schema migration to default `email_notifications_enabled` to `1` and backfill non-empty-email customers. - Move `error_log` to a proper PSR-3 logger. ## Risk - Logging only + new debug endpoint (404-gated in prod). No behavioural change for any path that previously sent mail successfully. `php -l` could not be run in the original sandbox; please verify on your CI box before deploying. 🤖 Generated with [OpenClaw](https://openclaw.ai)
72 lines
2.6 KiB
PHP
72 lines
2.6 KiB
PHP
<?php
|
|
|
|
use classes\xlvask_usage_logs_schema_bootstrap;
|
|
use xlvask\migrations\migration_20260804_xlvask_ai_auto_policy_v2;
|
|
|
|
require_once WD . '/classes/xlvask_usage_logs_schema_bootstrap.php';
|
|
require_once WD . '/classes/xlvask_autopilot_service.php';
|
|
require_once WD . '/modules/xlvask/migrations/20260804_xlvask_ai_auto_policy_v2.php';
|
|
|
|
if (!defined('WD')) {
|
|
exit;
|
|
}
|
|
|
|
$dbTarget = strtolower(trim((string)(getenv('CONFIG_DB_TARGET') ?: 'live')));
|
|
$startedAt = date('Y-m-d H:i:s');
|
|
echo "[{$startedAt}][XLVASK] Target database: {$dbTarget}" . PHP_EOL;
|
|
|
|
$result = [
|
|
'success' => false,
|
|
'db_target' => $dbTarget,
|
|
'preflight' => null,
|
|
'applied' => false,
|
|
'postflight' => null,
|
|
'wash_id_uniqueness_ready' => false,
|
|
'wash_id_uniqueness_activated' => false,
|
|
'wash_id_uniqueness_blocked' => false,
|
|
'error' => null,
|
|
];
|
|
|
|
try {
|
|
$preflight = migration_20260804_xlvask_ai_auto_policy_v2::preflight();
|
|
$result['preflight'] = $preflight;
|
|
|
|
if (!(bool)($preflight['ready'] ?? false)) {
|
|
$result['postflight'] = migration_20260804_xlvask_ai_auto_policy_v2::apply();
|
|
$result['applied'] = true;
|
|
} else {
|
|
$result['postflight'] = $preflight;
|
|
}
|
|
|
|
$postflight = (array)$result['postflight'];
|
|
if (!(bool)($postflight['ready'] ?? false)) {
|
|
throw new RuntimeException('XL Vask automation schema is still not ready after apply.');
|
|
}
|
|
|
|
if (xlvask_usage_logs_schema_bootstrap::washIdUniquenessReady()) {
|
|
$result['wash_id_uniqueness_ready'] = true;
|
|
} else {
|
|
$activated = xlvask_usage_logs_schema_bootstrap::applyWashIdUniquenessMigration();
|
|
$result['wash_id_uniqueness_ready'] = $activated && xlvask_usage_logs_schema_bootstrap::washIdUniquenessReady();
|
|
$result['wash_id_uniqueness_activated'] = $result['wash_id_uniqueness_ready'];
|
|
$result['wash_id_uniqueness_blocked'] = !$result['wash_id_uniqueness_ready'];
|
|
}
|
|
|
|
$result['success'] = (bool)$result['wash_id_uniqueness_ready'];
|
|
if (!$result['success']) {
|
|
$result['error'] = 'Wash-id uniqueness is blocked, likely due duplicate normalized wash_id values.';
|
|
}
|
|
} catch (Throwable $throwable) {
|
|
// The wrapper cron entry may swallow the runtime exception that is
|
|
// re-thrown below, so emit a container-log breadcrumb here too.
|
|
error_log('[cron-ensure-xlvask-automation-schema] apply failed: ' . $throwable->getMessage());
|
|
$result['error'] = $throwable->getMessage();
|
|
}
|
|
|
|
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
|
|
|
|
if (!$result['success']) {
|
|
throw new RuntimeException((string)($result['error'] ?: 'XL Vask schema readiness failed.'));
|
|
}
|
|
|