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
@@ -137,7 +137,7 @@ final class Transaction_Store {
/** Write-ahead insert. Returns the new row id (0 on failure). */
public function create_pending(Transaction $t): int {
global $wpdb;
$now = current_time('mysql');
$now = current_time('mysql', true); // UTC - matches the gmdate() retention/dedupe cutoffs
$t->event_log[] = self::event('Write-ahead record created (status: pending)');
$ok = $wpdb->insert(self::table_name(), [
@@ -159,14 +159,14 @@ final class Transaction_Store {
}
public function mark_success(int $id, string $quote_ref, ?string $quote_url, ?int $http_status): void {
$delivered = current_time('mysql');
$delivered = current_time('mysql', true); // UTC
$this->update($id, [
'status' => Tx_Status::Delivered->value,
'quote_ref' => $quote_ref,
'quote_url' => $quote_url,
'http_status' => $http_status,
'delivered_at' => $delivered,
'expiry' => $this->compute_expiry($delivered),
'expiry' => $this->compute_expiry(),
], 'Delivered - quoteRef ' . $quote_ref);
}
@@ -375,14 +375,14 @@ final class Transaction_Store {
$wpdb->update(self::table_name(), $data, ['id' => $id]);
}
private function compute_expiry(string $delivered_at): string {
/** Expiry = now + retention window, in UTC (delivered_at is written at ~the same instant). */
private function compute_expiry(): string {
$days = (int) get_option('espinstanda_retention_days', 7);
$ts = strtotime($delivered_at) ?: time();
return gmdate('Y-m-d H:i:s', $ts + ($days * DAY_IN_SECONDS));
return gmdate('Y-m-d H:i:s', time() + ($days * DAY_IN_SECONDS));
}
/** @return array{ts:string,level:string,message:string} */
private static function event(string $message, string $level = 'info'): array {
return ['ts' => current_time('mysql'), 'level' => $level, 'message' => Logger::redact($message)];
return ['ts' => current_time('mysql', true), 'level' => $level, 'message' => Logger::redact($message)];
}
}