Merge pull request 'dev' (#86) from dev into main

Reviewed-on: #86
This commit was merged in pull request #86.
This commit is contained in:
2026-06-03 14:20:40 +00:00
3 changed files with 52 additions and 3 deletions

View File

@@ -56,7 +56,8 @@ function createMockDb(overrides: Record<string, unknown> = {}) {
* Creates the service with a given mock db injected via reflection.
*/
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;
}

View File

@@ -7,11 +7,22 @@ import {
} from "@nestjs/common";
import { and, desc, eq, inArray, or } from "drizzle-orm";
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()
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)
@@ -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;
}

View File

@@ -65,6 +65,10 @@ services:
- NOVU_API_KEY=${NOVU_API_KEY:-}
- APP_PUBLIC_URL=${APP_PUBLIC_URL:-https://sase.tr}
- MAILTRACK_SECRET=${MAILTRACK_SECRET:-}
# PostHog server-side analytics (payments, subscription lifecycle, $revenue).
# Empty key → PostHogService no-ops (server-side analytics disabled).
- POSTHOG_API_KEY=${POSTHOG_API_KEY:-}
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
- OTEL_ENABLED=${OTEL_ENABLED:-false}
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
- OTEL_EXPORTER_OTLP_HEADERS=${OTEL_EXPORTER_OTLP_HEADERS:-}
@@ -144,6 +148,10 @@ services:
- NOVU_API_KEY=${NOVU_API_KEY:-}
- APP_PUBLIC_URL=${APP_PUBLIC_URL:-https://sase.tr}
- MAILTRACK_SECRET=${MAILTRACK_SECRET:-}
# PostHog server-side analytics (payments, subscription lifecycle, $revenue).
# Empty key → PostHogService no-ops (server-side analytics disabled).
- POSTHOG_API_KEY=${POSTHOG_API_KEY:-}
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
- OTEL_ENABLED=${OTEL_ENABLED:-false}
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
- OTEL_EXPORTER_OTLP_HEADERS=${OTEL_EXPORTER_OTLP_HEADERS:-}