Skip to content

Commit 5650041

Browse files
committed
feat: added revenue attribution
1 parent 63843df commit 5650041

3 files changed

Lines changed: 105 additions & 53 deletions

File tree

apps/web/lib/ads/service.ts

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
CreateAdCampaignResponse,
2525
} from "./types";
2626
import { verifyAdLogoExistsAndMetadata } from "./blob";
27+
import { cookies } from "next/headers";
2728

2829
type StripeUserRow = {
2930
id: string;
@@ -276,7 +277,10 @@ async function upsertPendingCampaign(options: {
276277
const now = new Date();
277278
const existingLiveCampaign = await findLiveCampaignByUserId(options.userId);
278279

279-
if (existingLiveCampaign && existingLiveCampaign.status !== "checkout_pending") {
280+
if (
281+
existingLiveCampaign &&
282+
existingLiveCampaign.status !== "checkout_pending"
283+
) {
280284
throw new AdsServiceError(
281285
"You already have an active advertising campaign. Cancel it before creating a new one.",
282286
{
@@ -288,7 +292,8 @@ async function upsertPendingCampaign(options: {
288292

289293
await assertCapacityAvailable({
290294
now,
291-
hasExistingPendingCampaign: existingLiveCampaign?.status === "checkout_pending",
295+
hasExistingPendingCampaign:
296+
existingLiveCampaign?.status === "checkout_pending",
292297
});
293298

294299
const values = {
@@ -374,7 +379,9 @@ function requireStripePlatformWebhookSecret() {
374379
return secret;
375380
}
376381

377-
async function markWebhookEventProcessed(event: Stripe.Event): Promise<boolean> {
382+
async function markWebhookEventProcessed(
383+
event: Stripe.Event,
384+
): Promise<boolean> {
378385
try {
379386
await db.insert(stripeWebhookEvent).values({
380387
eventId: event.id,
@@ -447,6 +454,7 @@ export async function createAdCampaignCheckout(options: {
447454
baseUrl: string;
448455
}): Promise<CreateAdCampaignResponse> {
449456
const now = new Date();
457+
const cookieStore = await cookies();
450458
await cleanupExpiredCheckoutPendingCampaigns(now);
451459

452460
const logo = await verifyAdLogoExistsAndMetadata({
@@ -491,6 +499,10 @@ export async function createAdCampaignCheckout(options: {
491499
adCampaignId: campaignId,
492500
userId: options.userId,
493501
placement: options.input.placement,
502+
datafast_visitor_id:
503+
cookieStore.get("datafast_visitor_id")?.value || null,
504+
datafast_session_id:
505+
cookieStore.get("datafast_session_id")?.value || null,
494506
},
495507
},
496508
});
@@ -506,10 +518,13 @@ export async function createAdCampaignCheckout(options: {
506518

507519
const campaign = await getCampaignByUserId(options.userId);
508520
if (!campaign) {
509-
throw new AdsServiceError("Unable to load campaign after checkout creation.", {
510-
code: "CAMPAIGN_FETCH_FAILED",
511-
status: 500,
512-
});
521+
throw new AdsServiceError(
522+
"Unable to load campaign after checkout creation.",
523+
{
524+
code: "CAMPAIGN_FETCH_FAILED",
525+
status: 500,
526+
},
527+
);
513528
}
514529

515530
if (!checkoutSession.url) {
@@ -525,7 +540,9 @@ export async function createAdCampaignCheckout(options: {
525540
};
526541
}
527542

528-
export async function cancelAdCampaignForUser(userId: string): Promise<CancelAdCampaignResponse> {
543+
export async function cancelAdCampaignForUser(
544+
userId: string,
545+
): Promise<CancelAdCampaignResponse> {
529546
await cleanupExpiredCheckoutPendingCampaigns();
530547

531548
const [campaignRow] = await db
@@ -558,9 +575,12 @@ export async function cancelAdCampaignForUser(userId: string): Promise<CancelAdC
558575
}
559576

560577
const stripe = getStripeClient();
561-
const subscription = await stripe.subscriptions.update(campaignRow.stripeSubscriptionId, {
562-
cancel_at_period_end: true,
563-
});
578+
const subscription = await stripe.subscriptions.update(
579+
campaignRow.stripeSubscriptionId,
580+
{
581+
cancel_at_period_end: true,
582+
},
583+
);
564584

565585
const now = new Date();
566586
const nextStatus = resolveCampaignStatusFromSubscription({
@@ -595,7 +615,9 @@ export async function cancelAdCampaignForUser(userId: string): Promise<CancelAdC
595615
};
596616
}
597617

598-
async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) {
618+
async function handleCheckoutSessionCompleted(
619+
session: Stripe.Checkout.Session,
620+
) {
599621
const stripe = getStripeClient();
600622
const campaignIdFromMetadata =
601623
typeof session.metadata?.adCampaignId === "string"
@@ -716,7 +738,8 @@ async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
716738
await db
717739
.update(adCampaign)
718740
.set({
719-
status: campaign.status === "suspended_policy" ? "suspended_policy" : "ended",
741+
status:
742+
campaign.status === "suspended_policy" ? "suspended_policy" : "ended",
720743
stripeSubscriptionStatus: subscription.status,
721744
cancelAtPeriodEnd: true,
722745
currentPeriodStart: period.currentPeriodStart,
@@ -779,10 +802,14 @@ export async function processPlatformStripeWebhookEvent(event: Stripe.Event) {
779802

780803
switch (event.type) {
781804
case "checkout.session.completed":
782-
await handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session);
805+
await handleCheckoutSessionCompleted(
806+
event.data.object as Stripe.Checkout.Session,
807+
);
783808
return;
784809
case "checkout.session.expired":
785-
await handleCheckoutSessionExpired(event.data.object as Stripe.Checkout.Session);
810+
await handleCheckoutSessionExpired(
811+
event.data.object as Stripe.Checkout.Session,
812+
);
786813
return;
787814
case "customer.subscription.updated":
788815
await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
@@ -798,7 +825,9 @@ export async function processPlatformStripeWebhookEvent(event: Stripe.Event) {
798825
}
799826
}
800827

801-
export async function getAdCampaignByUserIdOrThrow(userId: string): Promise<AdCampaignDTO> {
828+
export async function getAdCampaignByUserIdOrThrow(
829+
userId: string,
830+
): Promise<AdCampaignDTO> {
802831
const campaign = await getCampaignByUserId(userId);
803832

804833
if (!campaign) {

apps/web/lib/categories.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ export const RESERVED_CATEGORY_SLUGS = DISCOVERY_PAGES.map(
140140
(page) => page.slug,
141141
) as readonly string[];
142142

143-
const categorySlugSet = new Set<string>(CATEGORIES.map((category) => category.slug));
143+
const categorySlugSet = new Set<string>(
144+
CATEGORIES.map((category) => category.slug),
145+
);
144146
const discoverySlugSet = new Set<string>(
145147
DISCOVERY_PAGES.map((page) => page.slug),
146148
);

apps/web/lib/purchases/service.ts

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { randomUUID } from "node:crypto";
44
import { and, eq } from "drizzle-orm";
55
import type Stripe from "stripe";
66
import { db } from "@/lib/db";
7-
import { commissionOverride, purchase, purchaseEvent, user } from "@/lib/db/schema";
7+
import {
8+
commissionOverride,
9+
purchase,
10+
purchaseEvent,
11+
user,
12+
} from "@/lib/db/schema";
813
import { templatePath } from "@/lib/routes";
914
import { getStripeClient } from "@/lib/stripe";
1015
import {
@@ -16,6 +21,7 @@ import { requireTemplateRecordBySlug } from "@/lib/templates/repository";
1621
import { PurchaseServiceError } from "./errors";
1722
import type { CreatePurchaseCheckoutInput } from "./schemas";
1823
import type { CreatePurchaseCheckoutResponse } from "./types";
24+
import { cookies } from "next/headers";
1925

2026
type PurchaseStatus = "pending" | "completed" | "failed";
2127
type PurchaseEventType =
@@ -115,10 +121,7 @@ function getResourceId(value: unknown): string | null {
115121
return null;
116122
}
117123

118-
function resolveSaleType(input: {
119-
sellerUsername: string;
120-
ref?: string;
121-
}): {
124+
function resolveSaleType(input: { sellerUsername: string; ref?: string }): {
122125
saleType: TemplateSaleType;
123126
referralCode: string | null;
124127
} {
@@ -163,7 +166,9 @@ async function findPurchaseByBuyerTemplate(
163166
stripeCheckoutSessionId: purchase.stripeCheckoutSessionId,
164167
})
165168
.from(purchase)
166-
.where(and(eq(purchase.buyerId, buyerId), eq(purchase.templateId, templateId)))
169+
.where(
170+
and(eq(purchase.buyerId, buyerId), eq(purchase.templateId, templateId)),
171+
)
167172
.limit(1);
168173

169174
return record ?? null;
@@ -593,7 +598,10 @@ export async function createTemplatePurchaseCheckout(options: {
593598
input: CreatePurchaseCheckoutInput;
594599
baseUrl: string;
595600
}): Promise<CreatePurchaseCheckoutResponse> {
596-
const templateRow = await requireTemplateRecordBySlug(options.input.templateSlug);
601+
const templateRow = await requireTemplateRecordBySlug(
602+
options.input.templateSlug,
603+
);
604+
const cookieStore = await cookies();
597605

598606
if (
599607
templateRow.status !== "published" ||
@@ -655,7 +663,10 @@ export async function createTemplatePurchaseCheckout(options: {
655663
});
656664
}
657665

658-
const commissionRate = await resolveCommissionRate(templateRow.sellerId, sale.saleType);
666+
const commissionRate = await resolveCommissionRate(
667+
templateRow.sellerId,
668+
sale.saleType,
669+
);
659670
const split = calculateSplitFromRate(templateRow.priceCents, commissionRate);
660671
const pending = await upsertPendingPurchase({
661672
buyerId: options.buyerId,
@@ -683,43 +694,53 @@ export async function createTemplatePurchaseCheckout(options: {
683694

684695
let session: Stripe.Checkout.Session;
685696
try {
686-
session = await stripe.checkout.sessions.create({
687-
mode: "payment",
688-
customer_email: stripeUser.email,
689-
line_items: [
690-
{
691-
quantity: 1,
692-
price_data: {
693-
currency: toStripeCurrencyCode(templateRow.currency),
694-
unit_amount: templateRow.priceCents,
695-
product_data: {
696-
name: templateRow.title,
697-
description: deriveTemplateExcerptFromMarkdown(templateRow.description, 180),
697+
session = await stripe.checkout.sessions.create(
698+
{
699+
mode: "payment",
700+
customer_email: stripeUser.email,
701+
line_items: [
702+
{
703+
quantity: 1,
704+
price_data: {
705+
currency: toStripeCurrencyCode(templateRow.currency),
706+
unit_amount: templateRow.priceCents,
707+
product_data: {
708+
name: templateRow.title,
709+
description: deriveTemplateExcerptFromMarkdown(
710+
templateRow.description,
711+
180,
712+
),
713+
},
698714
},
699715
},
700-
},
701-
],
702-
success_url: `${baseUrl}${templatePath(templateRow.slug)}?checkout=success&session_id={CHECKOUT_SESSION_ID}`,
703-
cancel_url: `${baseUrl}${templatePath(templateRow.slug)}?checkout=cancel`,
704-
metadata: {
705-
kind: "template_purchase",
706-
purchaseId: pending.purchaseId,
707-
buyerId: options.buyerId,
708-
templateId: templateRow.id,
709-
},
710-
payment_intent_data: {
711-
application_fee_amount: split.platformFeeCents,
716+
],
717+
success_url: `${baseUrl}${templatePath(templateRow.slug)}?checkout=success&session_id={CHECKOUT_SESSION_ID}`,
718+
cancel_url: `${baseUrl}${templatePath(templateRow.slug)}?checkout=cancel`,
712719
metadata: {
713720
kind: "template_purchase",
714721
purchaseId: pending.purchaseId,
715722
buyerId: options.buyerId,
716723
templateId: templateRow.id,
717724
},
725+
payment_intent_data: {
726+
application_fee_amount: split.platformFeeCents,
727+
metadata: {
728+
kind: "template_purchase",
729+
purchaseId: pending.purchaseId,
730+
buyerId: options.buyerId,
731+
templateId: templateRow.id,
732+
datafast_visitor_id:
733+
cookieStore.get("datafast_visitor_id")?.value || null,
734+
datafast_session_id:
735+
cookieStore.get("datafast_session_id")?.value || null,
736+
},
737+
},
738+
client_reference_id: pending.purchaseId,
718739
},
719-
client_reference_id: pending.purchaseId,
720-
}, {
721-
stripeAccount: sellerRow.stripeAccountId,
722-
});
740+
{
741+
stripeAccount: sellerRow.stripeAccountId,
742+
},
743+
);
723744
} catch (error) {
724745
await db
725746
.update(purchase)

0 commit comments

Comments
 (0)