Skip to content

fix(stripe): idempotent customer lookup for wallet-auth + stuck pending_payment cleanup#50

Open
aimerdoux wants to merge 19 commits into
mainfrom
fix/stripe-wallet-auth-164
Open

fix(stripe): idempotent customer lookup for wallet-auth + stuck pending_payment cleanup#50
aimerdoux wants to merge 19 commits into
mainfrom
fix/stripe-wallet-auth-164

Conversation

@aimerdoux

Copy link
Copy Markdown
Owner

Summary

Fixes three issues from WAVAAAAAAAA-164 affecting Omar Hernandez (wallet-auth user with 6 stuck pending_payment booking intents).

  • Root cause: stripe.customers.list({ email }) used the wallet proxy email (@wallet.wavex.app) — never matched an existing customer, creating a new one each session
  • Idempotent lookup: both checkout edge functions now search by supabase_user_id metadata first, then fall back to real_email from user_metadata, then create
  • Stuck intents: new RPC wavex_os_cancel_expired_pending_payment_intents cancels pending_payment intents older than 25 h (Stripe session TTL = 24 h). Runs alongside the existing stale-pending cleanup every 5 min — will unstick the 6 intents on first deploy
  • cancelled_at invariant: BEFORE trigger on booking_intents back-fills cancelled_at = now() on any status → 'cancelled' transition, regardless of calling path
  • Retry flow: wavex_os_set_booking_intent_pending_payment now accepts pending_payment → pending_payment re-entry so callers can refresh the checkout session ID without resetting the intent first

Test plan

  • Create a wallet-auth test user; start two checkout sessions — confirm only one Stripe customer is created (lookup by supabase_user_id metadata)
  • Leave a pending_payment intent for >25 h; verify cleanup job cancels it with cancelled_at set
  • Manually UPDATE a booking_intent to status='cancelled' without cancelled_at; confirm trigger back-fills it
  • Call create-booking-checkout-session for an intent already in pending_payment with expired session; confirm new URL returned and session ID updated in DB

🤖 Generated with Claude Code

aimerdoux and others added 19 commits June 10, 2026 08:16
- GET/POST /api/unsubscribe?email=... or ?token={base64url(email)}
- Calls wavex_os_unsubscribe_contact RPC to mark leads.unsubscribed=true
- Migration adds unsubscribed/unsubscribed_at to public.leads + public.genesis_leads
- HTML confirmation page for email click-throughs; JSON for POST/programmatic

Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add Booking.tsx: experience catalog + Stripe checkout redirect flow (/book)
- Add booking-intent.ts: POST /api/booking/intent server route
- Add "Book Experiences" CTA button to MissionControl header nav
- Add signInLabel prop to SignInWidget; use "Sign in to book" on /book page

Root cause: /book route existed in router but was unreachable — no page
linked to it. 15k page_views on / with 0 path to booking = 0 booking_intents.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…e-issue guard

- run-governor.ts: live Anthropic quota windows -> allowance tiers
  (open/conserve/critical_only/frozen); DEFER-UNTIL cadence gate;
  3-strike same-error circuit breaker with auto-pause at 6
- claimQueuedRun consults governor (defer keeps runs queued, never cancels work)
- GET /api/governor/status for dashboard quota bar
- issue create: 409 duplicate_open_issue on normalized-title match
  (allowDuplicate: true to bypass); audited 15 dup clusters in one afternoon
- docs: optimization plan for the 5 fleet-audit failure modes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ota card

QA findings (e2e, 2026-06-10): Composio key VALID (1000 toolkits, mode live);
github OAuth initiate returns real redirect URL end-to-end. The 'Composio is
disabled' banner was a mislabel — toolkits without Composio-managed auth
(e.g. amplitude) threw 'No Default auth config found' and every failure was
shown as 'disabled'. initOAuth now classifies: disabled /
requires_custom_credentials / authorize_failed; worker + UI surface the
right message. Dashboard Month Spend card now shows live provider quota %
and governor tier when available.

KNOWN FOLLOW-UP: @composio/core 0.9.1 uses deprecated connectedAccounts.
initiate() — sunset 2026-07-03; upgrade to 0.10.0 before then.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…strip

The 441-line MissionControlDeliverablesWidget (and its registered
mission-control-deliverables worker RPC) existed but was never rendered —
the deliverables inbox was built and unmounted. Adds it as a first-class
tab after Decisions. Adds an always-visible GovernorQuotaStrip reading
GET /api/governor/status (same signal the run scheduler obeys): progress
bar, tier color, window label, reset time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stale-run reaper only honored a live processPid for SESSIONED_LOCAL_ADAPTERS;
plateau_local runs (external adapter, pid reported via onSpawn) were reaped
as process_lost at exactly staleThreshold (~5m) while the child was alive —
reproduced 3x on CEO canary runs. Orphaned children kept running unbilled^W
untracked. Adding plateau_local lets the reaper mark them detached-but-alive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Toolkits without Composio-managed OAuth (e.g. Amplitude) dead-ended with
'requires_custom_credentials'. Full path added:
- shim: getCredentialRequirements (merges auth-config creation + connected-
  account initiation fields; picks API_KEY > BEARER_TOKEN > BASIC > OAUTH2)
  and connectWithCredentials (use_custom_auth config + initiate with
  {authScheme, val} for key-style schemes)
- server: GET credential-requirements/:slug + POST connect-with-credentials
- plugin worker: two actions proxying the routes
- Directory UI: inline credential form opens on requires_custom_credentials
  (password-masked secret fields, required validation)

Live-verified: GET credential-requirements/amplitude serves real fields
(Base URL, API Key…). Write path follows the SDK contract validated by the
github authorize e2e; first real credential submit confirms it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… pre-OAuth fields

googleads/whatsapp failed authorize() with ConnectedAccount_MissingRequiredFields
(Customer ID / WABA ID). New classification routes them to the same credential
form; connectWithCredentials uses the Composio-MANAGED auth config for
OAuth-style schemes and carries the user's fields on initiate ({authScheme,val}),
returning the OAuth redirect. fieldsFor=initiation scopes the form to only the
fields the managed app actually needs (no client id/secret over-ask).
Live-verified: googleads initiate -> requires_initiation_fields; initiation
fields = [Customer ID]; whatsapp -> API_KEY [System User Token, WABA ID].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wake a fixer agent

run_failed + run_completed emitted at setRunStatus; breaker_paused from the
governor. Hot-read JSON config, per-signature dedup (30m), hourly wake cap,
fixer self-trigger guard, fire-and-forget. Fixer wakes ride the normal queue
so the run governor still applies. Pool B doc: docs/inference-hook-manager.md.
Also fixes TS closure-narrowing in run-governor scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ission Control KPIs

bridgeKpis now lands manifest.goal {kpiId, current, target, days} as a
first-class scoreboard KPI: canonical KPIs get the user's target/window
stamped; non-canonical goals get their own company_kpis row labeled with
meta_goal; baseline kpi_snapshot uses the user's stated CURRENT value
(micros) instead of zero. Mission Control's observer surface now starts
with the mission the user actually typed during onboarding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Writer/metrics targeted /rest/v1/wavex_os.wizard_events — PostgREST doesn't
take schema.table in the path, so every call 404'd and onboarding funnel
telemetry was never recorded (QA finding #1, 2026-06-10). Table created in
the wavex experiences Supabase project (migration create_wizard_events, RLS
on). E2E verified: POST persists (event dcc57c7f), metrics returns funnel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ingestion

Concurrency is the real burn-rate control: a freshly incepted fleet ran 13
high-priority runs concurrently and burned the 5h window 50%->68% in ~25min
(priority gating passed them all). Caps: open 6 / conserve 3 / critical 1 /
frozen 0 (env-tunable). POST /api/hooks/emit lets sibling processes
(onboarding server :3101, edge fns) land errors on the inference surface;
new onboarding_error hook type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…meric suffixes

The collision handler appended an 'A' per attempt, yielding WAVAAAAAAAA by
the ninth wavex-os/* company. Now derives from the last name segment
(wavex-os/wavexcard -> WAV, WAVE, WAVEX, WAVEXC, WVXC consonant skeleton),
then numeric suffixes (WAV2..). Existing companies keep their prefixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, goal target, stated baseline

The scoreboard read only expected_kpi_impacts: a fresh company rendered EMPTY
and the user's onboarding goal (MRR 5000 -> 15000/90d) never reached the
observer surface. Now: seeded company_kpis rows appear immediately;
target falls back to company_kpis.targetMicros when no impacts exist;
current prefers the user's stated baseline (kpi_snapshots) over the
sampler's no-recorder zero until a real non-zero measurement lands.
Verified live for wavexcard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion, ignition charter, honest Monte Carlo

- Roadmap/workflows map: new mission-control-roadmap RPC (Paperclip-direct) +
  MissionControlRoadmapWidget + tab; renders ignition's [Roadmap] workflows
  with owner, status, KPI tags, child progress (live-verified: 5 lanes).
- Inference surface over onboarding: /api/hooks/emit resolves wavex slugs;
  wavex-os-server emits onboarding_error (manifest cloud-sync) and
  connector_failed (composio authorize) via inference-hooks-client
  (e2e verified into company activity).
- Silent ignition programmer: ignition now seeds the MIDDLEWARE supervisor
  charter assigned to the Chief of Staff (step middleware_charter) —
  previously installed by hand after every onboarding.
- Monte Carlo honesty: the simulation was real (activation growth 24-46%
  differentiated strategies) but pre_scale mode freezes MRR by design and
  every surface narrated the frozen MRR stats as zeros. Winner now carries
  mean_activation_growth + model_mode; imprint leads with activation lift +
  burn (verified: RETENTION_FIRST 45.7% / burn 1.31); MonteCarloRace chart
  races on activation when MRR is degenerate instead of flatlining.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…AA-154)

Adds infrastructure for the two-part referral messaging sequence:

- Migration: wavex_os.referral_sends audit table, blast/nurture candidate
  query RPCs with lazy referral code creation, idempotent record_send RPC
- referral-blast.ts: one-shot startup job sending launch blast to all active
  members reachable via Telegram; dry-run by default (WAVEX_REFERRAL_BLAST_DRY_RUN)
- referral-nurture.ts: hourly scheduler sending Day-14 nurture to members
  with 0 converts once 14 days post-blast; dry-run by default (WAVEX_REFERRAL_NURTURE_DRY_RUN)
- index.ts: wires both jobs into server startup

Co-Authored-By: Paperclip <noreply@paperclip.ing>
… pending_payment cleanup (WAVAAAAAAAA-164)

- create-booking-checkout-session: resolve Stripe customer by supabase_user_id
  metadata first, then real_email from user_metadata, to prevent duplicate
  customer creation for wallet-auth users (@wallet.wavex.app proxy emails)
- create-checkout-session: same fix for subscription checkout path
- booking-intent-cleanup: also call wavex_os_cancel_expired_pending_payment_intents
  every 5 min to unstick intents whose checkout.session.expired webhook was missed
- migration 20260610000008: (1) BEFORE trigger enforces cancelled_at = now() on
  any status → 'cancelled' transition regardless of calling path; (2) update
  wavex_os_set_booking_intent_pending_payment to accept pending_payment → pending_payment
  re-entry so expired-session retries work; (3) new RPC
  wavex_os_cancel_expired_pending_payment_intents cancels pending_payment intents
  older than 25 h (Stripe session TTL = 24 h)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant