veza/veza-backend-api/internal/core
senke c10d73da4e
Some checks failed
Veza CI / Rust (Stream Server) (push) Successful in 4m18s
Security Scan / Secret Scanning (gitleaks) (push) Successful in 1m22s
Veza CI / Frontend (Web) (push) Failing after 19m45s
E2E Playwright / e2e (full) (push) Failing after 20m45s
Veza CI / Backend (Go) (push) Failing after 22m38s
Veza CI / Notify on failure (push) Successful in 7s
feat(subscription): webhook handler closes pending_payment state machine (v1.0.9 item G — Phase 2)
Phase 1 (commit 2a96766a) opened the pending_payment status: a paid-plan
subscribe path creates a UserSubscription row in pending_payment +
subscription_invoices row carrying the Hyperswitch payment_id, then hands
the client_secret back to the SPA. Phase 2 lands the webhook side: the
PSP-driven state transition that closes the loop.

State machine:
  - pending_payment + status=succeeded  →  invoice paid (paid_at=now), sub active
  - pending_payment + status=failed     →  invoice failed,            sub expired
  - already terminal                    →  idempotent no-op (paid_at NOT bumped)
  - payment_id not in subscription_invoices → marketplace.ErrNotASubscription
    (caller falls through to the order webhook flow)

The processor only flips a subscription out of pending_payment. Rows that
have already transitioned (concurrent flow, manual admin action, plan
upgrade) are left alone — the invoice still gets the terminal status
update so the audit trail stays consistent.

New surface:
  - hyperswitch.SubscriptionWebhookProcessor — the actual handler. Reads
    subscription_invoices by hyperswitch_payment_id, looks up the parent
    user_subscriptions row, applies the transition in a single tx.
  - hyperswitch.IsSubscriptionEventType — exported helper for callers
    that want to skip the DB hit on clearly non-subscription events.
  - marketplace.SubscriptionWebhookHandler (interface) +
    marketplace.ErrNotASubscription (sentinel) — keeps marketplace from
    importing the hyperswitch package while still allowing
    ProcessPaymentWebhook to dispatch typed.
  - marketplace.WithSubscriptionWebhookHandler (option) — wired by
    routes_webhooks.getMarketplaceService so the prod webhook handler
    routes subscription events instead of swallowing them as "order not
    found".

Dispatcher in ProcessPaymentWebhook: try subscription first, fall through
to the order flow on ErrNotASubscription. Order events are unchanged.

Tests (4, sqlite in-memory, all green):
  - Succeeded: pending_payment → active+paid, paid_at set
  - Failed:    pending_payment → expired+failed
  - Idempotent replay: second succeeded webhook is a no-op, paid_at NOT
    re-stamped (locks down Hyperswitch's at-least-once delivery contract)
  - Unknown payment_id: returns marketplace.ErrNotASubscription so the
    dispatcher falls through to ProcessPaymentWebhook's order flow

Removes the v1.0.6.2 "active row without PSP linkage" fantôme pattern
that hasEffectivePayment had to filter retroactively — the Phase 1 +
Phase 2 pair is now the canonical paid-plan creation path.

E2E + recovery endpoint (POST /api/v1/subscriptions/complete/:id) +
distribution gate land in Phase 3 (Day 3 of ROADMAP_V1.0_LAUNCH.md).

SKIP_TESTS=1 rationale: this commit is backend-only (Go); the husky
pre-commit hook only runs frontend typecheck/lint/vitest. Backend tests
verified manually:
  $ go test -short -count=1 ./internal/services/hyperswitch/... ./internal/core/marketplace/... ./internal/core/subscription/...
  ok  veza-backend-api/internal/services/hyperswitch
  ok  veza-backend-api/internal/core/marketplace
  ok  veza-backend-api/internal/core/subscription

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 05:39:59 +02:00
..
admin feat(v0.11.3): F421-F424 admin platform handler and routes 2026-03-10 18:19:45 +01:00
analytics fix(v0.12.6): apply all pentest remediations — 36 findings across 36 files 2026-03-14 00:44:46 +01:00
auth feat(auth): defer JWT to post-verify + verify-email header (v1.0.9 items 1.3+1.4) 2026-04-26 22:56:31 +02:00
collaboration adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
connecterrors feat(marketplace): stripe reversal error disambiguation + CHECK constraint + E2E — v1.0.7 item B day 3 2026-04-18 02:12:03 +02:00
discover style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
distribution fix(distribution,audit): propagate ErrSubscriptionNoPayment to handler + P0.12 closure date + E2E regression TODO 2026-04-17 12:43:21 +02:00
education style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
feed feat(v0.10.1): Tags & Genres discover - F351-F355 2026-03-09 01:52:56 +01:00
marketplace feat(subscription): webhook handler closes pending_payment state machine (v1.0.9 item G — Phase 2) 2026-04-27 05:39:59 +02:00
moderation feat(v0.11.2): F411-F420 moderation handler and routes 2026-03-10 17:49:51 +01:00
social fix(v0.12.6): apply all pentest remediations — 36 findings across 36 files 2026-03-14 00:44:46 +01:00
subscription feat(subscription): pending_payment state machine + mandatory provider (v1.0.9 item G — Phase 1) 2026-04-26 10:02:00 +02:00
track feat(track): native S3 multipart for chunked uploads (v1.0.9 item 1.5) 2026-04-26 23:12:56 +02:00