fix(instanda): rate limiting, UTC timestamps, and close silent-failure paths

- F-06: throttle the public create per IP (20/h) and per email (5/h) before the
  paid API call and the PII write.
- F-11: wrap the create path in try/catch - on an unexpected error fail closed for
  that submission (no quote-less entry), alert admins, keep the form working.
- F-14: abort the StartQuote when the write-ahead insert fails (no unaudited call);
  fire the failure alert even when storage is disabled (build an in-memory tx).
- F-10/F-17: treat a 2xx with a missing/empty quoteRef as ambiguous
  (possible_duplicate=1), not a hard failure - a quote may exist.
- F-07: write created_at/delivered_at/event timestamps in UTC to match the gmdate()
  retention/dedupe cutoffs; display in site-local via get_date_from_gmt.
- F-09: log (no silent return) when async record_feed_result cannot resolve a
  delivered transaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:13:06 -06:00
co-authored by Claude Fable 5
parent 2b1c226d46
commit 0732dd662d
5 changed files with 127 additions and 28 deletions
@@ -392,7 +392,14 @@ JS;
}
}
if ($tx === null || $tx->quote_ref === null) {
return; // nothing delivered to record (defensive)
// No-silent-failure: an async feed with no resolvable delivered transaction means
// entry meta / note / notification are being skipped - make that visible.
Logger::error(sprintf(
'record_feed_result: no delivered transaction for entry #%d (dedupe %s); entry meta/notification not recorded.',
$entry_id,
(string) (Submission::current_dedupe() ?? 'n/a')
));
return;
}
Entry_Integration::record($entry_id, $tx->quote_ref, (string) $tx->quote_url, 'delivered', $tx->environment);
@@ -487,7 +494,8 @@ JS;
echo '<h2>Event log</h2><ul class="espinstanda-timeline">';
foreach ($tx->event_log as $e) {
echo '<li><code>' . esc_html($e['ts'] ?? '') . '</code> ' . esc_html($e['message'] ?? '') . '</li>';
$ts = isset($e['ts']) && $e['ts'] !== '' ? get_date_from_gmt((string) $e['ts'], 'Y-m-d H:i') : '';
echo '<li><code>' . esc_html($ts) . '</code> ' . esc_html($e['message'] ?? '') . '</li>';
}
echo '</ul>';
@@ -499,7 +507,8 @@ JS;
'Status' => ucfirst($tx->status), 'Environment' => strtoupper($tx->environment),
'quoteRef' => $tx->quote_ref ?? '—', 'HTTP' => $tx->http_status ?? '—',
'Retries' => $tx->retry_count, 'Dedupe key' => $tx->dedupe_key,
'Customer' => $tx->customer_email, 'Created' => $tx->created_at,
'Customer' => $tx->customer_email,
'Created' => $tx->created_at !== '' ? get_date_from_gmt($tx->created_at, 'Y-m-d H:i') : '—',
];
foreach ($rows as $k => $v) {
echo '<tr><th>' . esc_html($k) . '</th><td>' . esc_html((string) $v) . '</td></tr>';