prefix . 'wizup_forms_queue'; } public static function install(): void { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = self::table(); dbDelta("CREATE TABLE $table ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, request_key varchar(64) NOT NULL, connection varchar(64) NOT NULL, source varchar(32) NOT NULL, form_id varchar(100) NOT NULL, payload longtext NOT NULL, workflow longtext NULL, status varchar(20) NOT NULL DEFAULT 'pending', attempts int unsigned NOT NULL DEFAULT 0, http_code int unsigned NOT NULL DEFAULT 0, lead_id bigint(20) unsigned NOT NULL DEFAULT 0, note text NOT NULL, created_at datetime NOT NULL, updated_at datetime NOT NULL, next_at datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY request_key (request_key), KEY due (status,next_at), KEY retention (created_at) ) " . $wpdb->get_charset_collate() . ';'); update_option('wizup_forms_schema', WIZUP_FORMS_VERSION, false); if (!wp_next_scheduled('wizup_forms_tick')) { wp_schedule_event(time() + 60, 'wizup_forms_minute', 'wizup_forms_tick'); } } public static function enqueue(array $payload, string $source, string $form_id) { global $wpdb; $connection = Settings::fingerprint(); if (!$connection) { return new \WP_Error('connection', 'החיבור ל-Wizup אינו מוגדר.'); } try { $sealed = Crypto::seal(wp_json_encode($payload)); } catch (\Throwable $e) { return new \WP_Error('encryption', 'לא ניתן לשמור את הפנייה בבטחה.'); } $now = gmdate('Y-m-d H:i:s'); $ok = $wpdb->insert(self::table(), [ 'request_key' => 'wf-' . wp_generate_uuid4(), 'connection' => $connection, 'source' => sanitize_key($source), 'form_id' => substr(sanitize_text_field($form_id), 0, 100), 'payload' => $sealed, 'status' => 'pending', 'note' => 'ממתין לשליחה.', 'created_at' => $now, 'updated_at' => $now, 'next_at' => $now, ]); if (!$ok) { return new \WP_Error('storage', 'לא ניתן לשמור את הפנייה. נסו שוב בעוד רגע.'); } $id = (int) $wpdb->insert_id; if (!wp_next_scheduled('wizup_forms_send')) { wp_schedule_single_event(time() + 1, 'wizup_forms_send'); } return $id; } public static function tick(): void { global $wpdb; $table = self::table(); $now = gmdate('Y-m-d H:i:s'); // Recover a crashed worker using the same idempotency key. The HTTP timeout is 12 seconds. $wpdb->query($wpdb->prepare("UPDATE %i SET status='retry' WHERE status='sending' AND next_at < %s", $table, $now)); $wpdb->query($wpdb->prepare("DELETE FROM %i WHERE created_at < %s AND status != 'sending'", $table, gmdate('Y-m-d H:i:s', time() - 14 * DAY_IN_SECONDS))); $ids = $wpdb->get_col($wpdb->prepare("SELECT id FROM %i WHERE status IN ('pending','retry') AND next_at <= %s ORDER BY id LIMIT 3", $table, $now)); foreach ($ids as $id) { self::dispatch((int) $id); } } public static function dispatch(int $id): void { global $wpdb; $table = self::table(); $now = gmdate('Y-m-d H:i:s'); $claim = $wpdb->query($wpdb->prepare("UPDATE %i SET status='sending', next_at=%s, updated_at=%s WHERE id=%d AND status IN ('pending','retry') AND next_at<=%s", $table, gmdate('Y-m-d H:i:s', time() + 180), $now, $id, $now)); if ($claim !== 1) { return; } $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM %i WHERE id=%d", $table, $id), ARRAY_A); if (!$row) { return; } if (!hash_equals($row['connection'], Settings::fingerprint())) { self::finish($id, ['status' => 'attention', 'note' => 'החיבור השתנה או נותק. הפנייה לא תועבר לחשבון אחר.']); return; } try { $payload = json_decode(Crypto::open($row['payload']), true, 512, JSON_THROW_ON_ERROR); if (!is_array($payload)) { throw new \RuntimeException(); } $workflow = !empty($row['workflow']) ? json_decode(Crypto::open($row['workflow']), true, 512, JSON_THROW_ON_ERROR) : []; if (!is_array($workflow)) { throw new \RuntimeException(); } } catch (\Throwable $e) { self::finish($id, ['status' => 'failed', 'note' => 'לא ניתן לפענח את הפנייה. ייתכן שמפתחות האבטחה של WordPress השתנו.']); return; } $attempt = (int) $row['attempts'] + 1; try { $result = Client::send($payload, $row['request_key'], $workflow, static function (array $context) use ($wpdb, $table, $id): void { $stored = $wpdb->update($table, ['workflow' => Crypto::seal(wp_json_encode($context))], ['id' => $id]); if ($stored === false) { throw new \RuntimeException('Unable to persist delivery progress.'); } }, ['created_at' => $row['created_at'], 'source' => $row['source'], 'form_id' => $row['form_id']]); } catch (\Throwable $e) { $result = ['state' => 'retry', 'code' => 0, 'note' => 'השליחה נקטעה; יבוצע ניסיון חוזר.']; } $state = $result['state']; if ($state === 'retry' && $attempt >= 6) { $state = 'failed'; $result['note'] = 'ניסיונות השליחה מוצו. יש לבדוק את החיבור ואת יומן Wizup.'; } $delays = [60, 300, 900, 3600, 21600, 86400]; $data = [ 'status' => $state, 'attempts' => !empty($result['reset_attempts']) ? 0 : $attempt, 'http_code' => $result['code'], 'lead_id' => $result['lead_id'] ?? 0, 'note' => $result['note'], 'next_at' => gmdate('Y-m-d H:i:s', time() + ($state === 'pending' ? 1 : max($delays[min($attempt - 1, 5)], $result['delay'] ?? 0))), ]; if (in_array($state, ['sent', 'warning', 'noted'], true)) { $data['payload'] = ''; $data['workflow'] = ''; } self::finish($id, $data); } private static function finish(int $id, array $data): void { global $wpdb; $data['updated_at'] = gmdate('Y-m-d H:i:s'); $wpdb->update(self::table(), $data, ['id' => $id]); } public static function retry(int $id): bool { global $wpdb; $table = self::table(); // Explicit administrator retry keeps the original body, connection and action keys. // It can diagnose an old 422 or save a confirmed duplicate through the optional notes flow. return 1 === $wpdb->query($wpdb->prepare("UPDATE %i SET status='retry', next_at=%s, attempts=0 WHERE id=%d AND status IN ('failed','attention') AND payload != '' AND connection=%s AND created_at>=%s", $table, gmdate('Y-m-d H:i:s'), $id, Settings::fingerprint(), gmdate('Y-m-d H:i:s', time() - 14 * DAY_IN_SECONDS))); } public static function recent(): array { global $wpdb; return $wpdb->get_results($wpdb->prepare('SELECT id,source,form_id,status,attempts,http_code,lead_id,note,created_at FROM %i ORDER BY id DESC LIMIT 50', self::table()), ARRAY_A) ?: []; } }