veza/veza-backend-api/internal/config
senke 7e180a2c08 feat(workers): hyperswitch reconciliation sweep for stuck pending states — v1.0.7 item C
New ReconcileHyperswitchWorker sweeps for pending orders and refunds
whose terminal webhook never arrived. Pulls live PSP state for each
stuck row and synthesises a webhook payload to feed the normal
ProcessPaymentWebhook / ProcessRefundWebhook dispatcher. The existing
terminal-state guards on those handlers make reconciliation
idempotent against real webhooks — a late webhook after the reconciler
resolved the row is a no-op.

Three stuck-state classes covered:
  1. Stuck orders (pending > 30m, non-empty payment_id) → GetPaymentStatus
     + synthetic payment.<status> webhook.
  2. Stuck refunds with PSP id (pending > 30m, non-empty
     hyperswitch_refund_id) → GetRefundStatus + synthetic
     refund.<status> webhook (error_message forwarded).
  3. Orphan refunds (pending > 5m, EMPTY hyperswitch_refund_id) →
     mark failed + roll order back to completed + log ERROR. This
     is the "we crashed between Phase 1 and Phase 2 of RefundOrder"
     case, operator-attention territory.

New interfaces:
  * marketplace.HyperswitchReadClient — read-only PSP surface the
    worker depends on (GetPaymentStatus, GetRefundStatus). The
    worker never calls CreatePayment / CreateRefund.
  * hyperswitch.Client.GetRefund + RefundStatus struct added.
  * hyperswitch.Provider gains GetRefundStatus + GetPaymentStatus
    pass-throughs that satisfy the marketplace interface.

Configuration (all env-var tunable with sensible defaults):
  * RECONCILE_WORKER_ENABLED=true
  * RECONCILE_INTERVAL=1h (ops can drop to 5m during incident
    response without a code change)
  * RECONCILE_ORDER_STUCK_AFTER=30m
  * RECONCILE_REFUND_STUCK_AFTER=30m
  * RECONCILE_REFUND_ORPHAN_AFTER=5m (shorter because "app crashed"
    is a different signal from "network hiccup")

Operational details:
  * Batch limit 50 rows per phase per tick so a 10k-row backlog
    doesn't hammer Hyperswitch. Next tick picks up the rest.
  * PSP read errors leave the row untouched — next tick retries.
    Reconciliation is always safe to replay.
  * Structured log on every action so `grep reconcile` tells the
    ops story: which order/refund got synced, against what status,
    how long it was stuck.
  * Worker wired in cmd/api/main.go, gated on
    HyperswitchEnabled + HyperswitchAPIKey. Graceful shutdown
    registered.
  * RunOnce exposed as public API for ad-hoc ops trigger during
    incident response.

Tests — 10 cases, all green (sqlite :memory:):
  * TestReconcile_StuckOrder_SyncsViaSyntheticWebhook
  * TestReconcile_RecentOrder_NotTouched
  * TestReconcile_CompletedOrder_NotTouched
  * TestReconcile_OrderWithEmptyPaymentID_NotTouched
  * TestReconcile_PSPReadErrorLeavesRowIntact
  * TestReconcile_OrphanRefund_AutoFails_OrderRollsBack
  * TestReconcile_RecentOrphanRefund_NotTouched
  * TestReconcile_StuckRefund_SyncsViaSyntheticWebhook
  * TestReconcile_StuckRefund_FailureStatus_PassesErrorMessage
  * TestReconcile_AllTerminalStates_NoOp

CHANGELOG v1.0.7-rc1 updated with the full item C section between D
and the existing E block, matching the order convention (ship order:
A → D → B → E → C, CHANGELOG order follows).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 03:08:15 +02:00
..
config.go feat(workers): hyperswitch reconciliation sweep for stuck pending states — v1.0.7 item C 2026-04-18 03:08:15 +02:00
config_test.go fix(config): refuse boot in production when HYPERSWITCH_ENABLED=false 2026-04-16 14:55:18 +02:00
cors.go feat: backend, stream server & infra improvements 2026-03-18 11:36:06 +01:00
db_init.go refactor(config): découper config.go par domaine (audit 2.7) 2026-02-15 14:44:33 +01:00
defaults.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
defaults_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
docs.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
docs_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
env_detection.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
env_detection_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
env_helpers.go feat(backend): OAuth FRONTEND_URL from config, docs update 2026-02-17 16:42:23 +01:00
env_loader.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
env_loader_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
middlewares_init.go v0.9.4 2026-03-05 23:03:43 +01:00
rabbitmq.go feat: backend — config, handlers, services, logging, migration 2026-03-23 15:46:57 +01:00
rate_limit.go feat: backend, stream server & infra improvements 2026-03-18 11:36:06 +01:00
redis_init.go refactor(config): découper config.go par domaine (audit 2.7) 2026-02-15 14:44:33 +01:00
reloader.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
reloader_test.go v0.9.4 2026-03-05 23:03:43 +01:00
secrets.go [BE-SEC-014] be-sec: Implement secrets management 2025-12-24 12:30:18 +01:00
secrets_test.go STABILISATION: phase 3–5 – API contract, tests & chat-server hardening 2025-12-06 17:21:59 +01:00
services_init.go v0.9.1 2026-03-05 19:22:31 +01:00
testutils.go fix(backend): use explicit DISABLE_RATE_LIMIT_FOR_TESTS flag instead of env-based bypass 2026-02-15 15:56:53 +01:00
testutils_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
upload_limits.go feat(backend,web): single source of truth for upload-size limits 2026-04-16 19:37:37 +02:00
upload_limits_test.go feat(backend,web): single source of truth for upload-size limits 2026-04-16 19:37:37 +02:00
validation.go refactor(backend): split config into domain modules (P2) 2026-02-16 11:12:21 +01:00
validation_test.go fix(chat,config): require REDIS_URL in prod + error on in-memory fallback 2026-04-16 14:56:47 +02:00
validator.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
validator_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
watcher.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
watcher_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00