feat(analytics): canonical subscription_activated revenue event ()
No PostHog event carried realized revenue, and EFT/havale activations fired nothing at all — so total paid revenue / MRR was unmeasurable (a Stripe DWH connector alone would also miss EFT). activateSubscription is the shared chokepoint for both Stripe (stripe.service) and EFT/manual (billing.service) activation, so emit one canonical subscription_activated there with PostHog revenue props: $revenue (major TRY), currency, mrr (yearly amortised /12), plan, plan_id, brand_count, billing_period, method (looked up from the latest payment row), referral_credit_days. Funnel steps keep their kuruş 'amount' but intentionally carry no $revenue, so revenue isn't double-counted. Unblocks trial->paid, MRR/ARPU and revenue-by-plan/channel across ALL payment methods. Injected PostHogService (PostHogModule is @Global). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,8 @@ function createMockDb(overrides: Record<string, unknown> = {}) {
|
|||||||
* Creates the service with a given mock db injected via reflection.
|
* Creates the service with a given mock db injected via reflection.
|
||||||
*/
|
*/
|
||||||
function createService(db: unknown): SubscriptionsService {
|
function createService(db: unknown): SubscriptionsService {
|
||||||
const service = new SubscriptionsService(db as any);
|
const posthog = { captureForUser: vi.fn(), capture: vi.fn() };
|
||||||
|
const service = new SubscriptionsService(db as any, posthog as any);
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,22 @@ import {
|
|||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { and, desc, eq, inArray, or } from "drizzle-orm";
|
import { and, desc, eq, inArray, or } from "drizzle-orm";
|
||||||
import { DATABASE, type Database } from "../database/database.provider";
|
import { DATABASE, type Database } from "../database/database.provider";
|
||||||
import { brands, plans, userBrands, userSubscriptions, users } from "../database/schema/core";
|
import {
|
||||||
|
brands,
|
||||||
|
payments,
|
||||||
|
plans,
|
||||||
|
userBrands,
|
||||||
|
userSubscriptions,
|
||||||
|
users,
|
||||||
|
} from "../database/schema/core";
|
||||||
|
import { PostHogService } from "../posthog/posthog.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubscriptionsService {
|
export class SubscriptionsService {
|
||||||
constructor(@Inject(DATABASE) private db: Database) {}
|
constructor(
|
||||||
|
@Inject(DATABASE) private db: Database,
|
||||||
|
private posthog: PostHogService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumes any banked referral reward days for the user (zeroing the balance)
|
* Consumes any banked referral reward days for the user (zeroing the balance)
|
||||||
@@ -152,6 +163,35 @@ export class SubscriptionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Canonical realized-revenue event. activateSubscription is the shared
|
||||||
|
// chokepoint for BOTH Stripe (stripe.service) and EFT/manual (billing.service)
|
||||||
|
// activation, so total paid revenue becomes measurable in PostHog regardless
|
||||||
|
// of payment method (a Stripe DWH connector alone would miss EFT/havale).
|
||||||
|
// $revenue is in major TRY (PostHog revenue convention); plan prices are kuruş.
|
||||||
|
// This is the single source of $revenue — funnel steps (payment_initiated etc.)
|
||||||
|
// intentionally do NOT carry $revenue so revenue isn't double-counted.
|
||||||
|
const priceKurus =
|
||||||
|
sub.billingPeriod === "yearly" ? (plan[0]?.priceYearly ?? 0) : (plan[0]?.priceMonthly ?? 0);
|
||||||
|
const [lastPayment] = await this.db
|
||||||
|
.select({ method: payments.method })
|
||||||
|
.from(payments)
|
||||||
|
.where(eq(payments.subscriptionId, subscriptionId))
|
||||||
|
.orderBy(desc(payments.createdAt))
|
||||||
|
.limit(1);
|
||||||
|
this.posthog.captureForUser(sub.userId, "subscription_activated", {
|
||||||
|
$revenue: priceKurus / 100,
|
||||||
|
currency: "TRY",
|
||||||
|
amount_kurus: priceKurus,
|
||||||
|
// Normalised monthly recurring revenue (yearly amortised over 12 months).
|
||||||
|
mrr: sub.billingPeriod === "yearly" ? priceKurus / 12 / 100 : priceKurus / 100,
|
||||||
|
plan: plan[0]?.name ?? null,
|
||||||
|
plan_id: sub.planId,
|
||||||
|
brand_count: plan[0]?.brandCount ?? null,
|
||||||
|
billing_period: sub.billingPeriod,
|
||||||
|
method: lastPayment?.method ?? "unknown",
|
||||||
|
referral_credit_days: creditDays,
|
||||||
|
});
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user