dev #118

Merged
root merged 5 commits from dev into main 2026-06-09 18:12:44 +03:00

5 Commits

Author SHA1 Message Date
8e732628bc feat(capi): send Meta Conversions API Purchase on subscription activation
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The existing CAPI only sent CompleteRegistration (signup). For a B2B funnel where
trials are cheap (~25 TRY) but paid is rare (~1% of trials), the highest-value
signal Meta can optimize on is the realized-revenue Purchase. Add
MetaCapiService.sendPurchase and fire it from activateSubscription — the shared
chokepoint for BOTH Stripe (webhook) and EFT/manual activation — so all paid
revenue is sent regardless of method. Hashed-email Advanced Matching (no browser
fbp/fbc in the webhook); event_id = purchase_<subscriptionId> dedupes a browser
Purchase. Awaited so it ships before the short request returns; fail-open.

This is the "teach Meta to find payers, not end-users" lever from adsOpt.md Phase 0.
Still gated on activating CAPI in prod (merge + META_CAPI_PIXEL_ID/ACCESS_TOKEN env).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:59:37 +03:00
42f8036b22 fix(subscriptions): expire trial subs past end_date, not just active
The subscription-expiry cron filtered status="active" only, so trials
never transitioned out of "trial" after end_date. Every access gate
keys off status, so trials past end_date kept full product access for
free (revenue leak) and inflated the active-trial count. On prod: 105
stuck trials, 3528 brand grants still live.

- expiry processor now sweeps status IN (active, trial) past end_date
  (lt() still skips NULL end_date, so perpetual subs are untouched)
- add "trial" to SubscriptionStatus union — it was used in the DB and
  code but missing from the type (both subscription.ts and user.ts)

Proven read-only on prod: old WHERE caught 0, fixed catches 105.
Revocation uses the existing set-expired + delete-userBrands path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 17:47:56 +03:00
9b9986a149 test(catalog): assert loadError is reported to Sentry (deduped)
Mock @sentry/nestjs and assert that a getCategoryWithParts loadError drives the
redis dedup key + Sentry.captureMessage("…drill-load-error…"), so the silent-
failure reporting can't regress unnoticed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:43:39 +03:00
e8e9771759 feat(observability): report silent catalog UX degradations to Sentry
The catalog failures that hurt UX — a drill/parts fetch that fails into an empty
"couldn't load" panel, or a decoded vehicle whose category tree comes back empty
("model var ama parça yok") — all return HTTP 200 with a degraded body. Nothing
throws, so the global Sentry exception filter never sees them and they go
unnoticed (serkan's complaint was exactly this class). Report them explicitly.

- new common/catalog-degradation.ts: reportCatalogDegradation(kind, ctx),
  fingerprinted by kind+source+brand so each failure mode collapses into one
  countable Sentry issue (e.g. "drill-load-error · pl24/Ford — N events, M users").
- categories.service: capture on getCategoryWithParts loadError and on an empty
  getCategoryTree, Redis-deduped to <=1 event/hour per category/vehicle so a
  broken catalog can't flood the stream; telemetry never throws into the request.

tsc + biome clean, categories suite 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:41:51 +03:00
5903151692 fix(api): await PostHog flush at conversion chokepoints (webhook events were lost)
payment_success / payment_failed / subscription_activated are captured inside
the Stripe webhook handler and activateSubscription — short requests that return
immediately. posthog-node's fire-and-forget flush was abandoned before the send
completed, so these events were written to the DB but never reached PostHog
(DB had 4 completed Stripe payments in 30d; PostHog had 1 payment_success and 0
payment_failed). payment_initiated, fired in a normal user request, landed fine —
which is what isolated the cause to the webhook/short-request context.

Add PostHogService.flush() and await it at the end of handleWebhook and after the
subscription_activated capture in activateSubscription (the shared Stripe+EFT
chokepoint). Restores server-side paid-conversion visibility so trial→paid ROI is
measurable in PostHog instead of only the DB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:30:16 +03:00