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
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>
This commit is contained in:
@@ -56,8 +56,13 @@ function createMockDb(overrides: Record<string, unknown> = {}) {
|
||||
* Creates the service with a given mock db injected via reflection.
|
||||
*/
|
||||
function createService(db: unknown): SubscriptionsService {
|
||||
const posthog = { captureForUser: vi.fn(), capture: vi.fn() };
|
||||
const service = new SubscriptionsService(db as any, posthog as any);
|
||||
const posthog = {
|
||||
captureForUser: vi.fn(),
|
||||
capture: vi.fn(),
|
||||
flush: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const metaCapi = { sendPurchase: vi.fn().mockResolvedValue(undefined) };
|
||||
const service = new SubscriptionsService(db as any, posthog as any, metaCapi as any);
|
||||
return service;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
userSubscriptions,
|
||||
users,
|
||||
} from "../database/schema/core";
|
||||
import { MetaCapiService } from "../meta-capi/meta-capi.service";
|
||||
import { PostHogService } from "../posthog/posthog.service";
|
||||
|
||||
@Injectable()
|
||||
@@ -22,6 +23,7 @@ export class SubscriptionsService {
|
||||
constructor(
|
||||
@Inject(DATABASE) private db: Database,
|
||||
private posthog: PostHogService,
|
||||
private metaCapi: MetaCapiService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -192,6 +194,23 @@ export class SubscriptionsService {
|
||||
referral_credit_days: creditDays,
|
||||
});
|
||||
|
||||
// Realized-revenue signal to Meta (server-side Purchase) so the ad algorithm
|
||||
// optimizes toward — and builds lookalikes of — accounts that actually PAY,
|
||||
// not just trial. Shared chokepoint → covers Stripe + EFT. Awaited so the
|
||||
// event ships before the short webhook/request returns. Fail-open inside.
|
||||
const [capiUser] = await this.db
|
||||
.select({ email: users.email })
|
||||
.from(users)
|
||||
.where(eq(users.id, sub.userId))
|
||||
.limit(1);
|
||||
await this.metaCapi.sendPurchase({
|
||||
userId: sub.userId,
|
||||
email: capiUser?.email,
|
||||
subscriptionId,
|
||||
valueKurus: priceKurus,
|
||||
currency: "TRY",
|
||||
});
|
||||
|
||||
// Await delivery: activateSubscription is the shared chokepoint for Stripe
|
||||
// (webhook) AND EFT/manual activation, both of which run in short requests
|
||||
// whose fire-and-forget flush was dropping this revenue event.
|
||||
|
||||
Reference in New Issue
Block a user