Skip to content
Merged
10 changes: 3 additions & 7 deletions apps/web/app/api/customers/[id]/activity/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,9 @@ export const GET = withWorkspace(async ({ workspace, params, session }) => {
}

let [events, link] = await Promise.all([
getCustomerEvents(
{ customerId: customer.id, clickId: customer.clickId },
{
sortOrder: "desc",
interval: "1y",
},
),
getCustomerEvents({
customerId: customer.id,
}),

prisma.link.findUniqueOrThrow({
where: {
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/api/embed/referrals/earnings/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { withReferralsEmbedToken } from "@/lib/embed/referrals/auth";
import { SALES_PAGE_SIZE } from "@/lib/partners/constants";
import { REFERRALS_EMBED_EARNINGS_LIMIT } from "@/lib/partners/constants";
import z from "@/lib/zod";
import { PartnerEarningsSchema } from "@/lib/zod/schemas/partner-profile";
import { prisma } from "@dub/prisma";
Expand Down Expand Up @@ -45,8 +45,8 @@ export const GET = withReferralsEmbedToken(
},
},
},
take: SALES_PAGE_SIZE,
skip: (page - 1) * SALES_PAGE_SIZE,
take: REFERRALS_EMBED_EARNINGS_LIMIT,
skip: (page - 1) * REFERRALS_EMBED_EARNINGS_LIMIT,
orderBy: {
createdAt: "desc",
},
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getCustomerEvents } from "@/lib/analytics/get-customer-events";
import { transformCustomer } from "@/lib/api/customers/transform-customer";
import { DubApiError } from "@/lib/api/errors";
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
Expand All @@ -11,67 +12,78 @@ import { NextResponse } from "next/server";
export const GET = withPartnerProfile(async ({ partner, params }) => {
const { customerId, programId } = params;

const { program } = await getProgramEnrollmentOrThrow({
const { program, links } = await getProgramEnrollmentOrThrow({
partnerId: partner.id,
programId: programId,
});

if (program.slug === "framer") {
throw new DubApiError({
code: "forbidden",
message: "Framer program does not support customer profile",
});
}

const customer = await prisma.customer.findUnique({
where: {
id: customerId,
},
include: {
link: {
include: {
programEnrollment: {
include: {
partner: true,
program: true,
},
},
},
},
},
});

if (
!customer ||
![
customer?.link?.programEnrollment?.programId,
customer?.link?.programEnrollment?.program.slug,
].includes(program.id)
) {
if (!customer || customer?.projectId !== program.workspaceId) {
throw new DubApiError({
code: "not_found",
message:
"Customer not found. Make sure you're using the correct customer ID (e.g. `cus_3TagGjzRzmsFJdH8od2BNCsc`).",
message: "Customer is not part of this program.",
});
}

customer.avatar = null;
customer.email;
const events = await getCustomerEvents({
customerId: customer.id,
linkIds: links.map((link) => link.id),
});

if (events.length === 0) {
throw new DubApiError({
code: "not_found",
message: "Customer is not attributed to any links by this partner.",
});
}

// get the first partner link that this customer interacted with
const firstLinkId = events[events.length - 1].link_id;
const link = links.find((link) => link.id === firstLinkId);

// Find the LTV of the customer
// TODO: Calculate this from all events, not limited
const ltv = events.reduce((acc, event) => {
if (event.event === "sale" && event.saleAmount) {
acc += Number(event.saleAmount);
}

return acc;
}, 0);

// Find the time to lead of the customer
const timeToLead =
customer.clickedAt && customer.createdAt
? customer.createdAt.getTime() - customer.clickedAt.getTime()
: null;

// Find the time to first sale of the customer
// TODO: Calculate this from all events, not limited
const firstSale = events.filter(({ event }) => event === "sale").pop();

const timeToSale =
firstSale && customer.createdAt
? new Date(firstSale.timestamp).getTime() - customer.createdAt.getTime()
: null;

return NextResponse.json(
PartnerProfileCustomerSchema.parse(
transformCustomer({
PartnerProfileCustomerSchema.parse({
...transformCustomer({
...customer,
email: customer.email || customer.name || generateRandomName(),
link: customer.link
? {
...customer.link,
programEnrollment: customer.link.programEnrollment
? { ...customer.link.programEnrollment, program: undefined }
: null,
}
: null,
}),
),
activity: {
ltv,
timeToLead,
timeToSale,
events,
link,
},
}),
);
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { CUSTOMER_PAGE_EVENTS_LIMIT } from "@/lib/partners/constants";
import useCustomer from "@/lib/swr/use-customer";
import useWorkspace from "@/lib/swr/use-workspace";
import {
Expand Down Expand Up @@ -165,7 +166,7 @@ const SalesTable = memo(({ customerId }: { customerId: string }) => {
const { id: workspaceId, slug } = useWorkspace();

const { data: salesData, isLoading: isSalesLoading } = useSWR<SaleEvent[]>(
`/api/events?event=sales&interval=all&limit=8&customerId=${customerId}&workspaceId=${workspaceId}`,
`/api/events?event=sales&interval=all&limit=${CUSTOMER_PAGE_EVENTS_LIMIT}&customerId=${customerId}&workspaceId=${workspaceId}`,
fetcher,
{
keepPreviousData: true,
Expand All @@ -175,7 +176,9 @@ const SalesTable = memo(({ customerId }: { customerId: string }) => {
const { data: totalSales, isLoading: isTotalSalesLoading } = useSWR<{
sales: number;
}>(
`/api/analytics?event=sales&interval=all&groupBy=count&customerId=${customerId}&workspaceId=${workspaceId}`,
// Only fetch total sales count if the sales data is equal to the limit
salesData?.length === CUSTOMER_PAGE_EVENTS_LIMIT &&
`/api/analytics?event=sales&interval=all&groupBy=count&customerId=${customerId}&workspaceId=${workspaceId}`,
fetcher,
{
keepPreviousData: true,
Expand All @@ -185,9 +188,11 @@ const SalesTable = memo(({ customerId }: { customerId: string }) => {
return (
<CustomerSalesTable
sales={salesData}
totalSales={totalSales?.sales}
totalSales={
isTotalSalesLoading ? undefined : totalSales?.sales ?? salesData?.length
}
viewAllHref={`/${slug}/events?event=sales&interval=all&customerId=${customerId}`}
isLoading={isSalesLoading || isTotalSalesLoading}
isLoading={isSalesLoading}
/>
);
});
Expand All @@ -199,22 +204,28 @@ const PartnerEarningsTable = memo(
const { data: commissions, isLoading: isComissionsLoading } = useSWR<
CommissionResponse[]
>(
`/api/programs/${programId}/commissions?customerId=${customerId}&workspaceId=${workspaceId}&pageSize=8`,
`/api/programs/${programId}/commissions?customerId=${customerId}&workspaceId=${workspaceId}&pageSize=${CUSTOMER_PAGE_EVENTS_LIMIT}`,
fetcher,
);

const { data: totalCommissions, isLoading: isTotalCommissionsLoading } =
useSWR<{ all: { count: number } }>(
`/api/programs/${programId}/commissions/count?customerId=${customerId}&workspaceId=${workspaceId}`,
// Only fetch total earnings count if the earnings data is equal to the limit
commissions?.length === CUSTOMER_PAGE_EVENTS_LIMIT &&
`/api/programs/${programId}/commissions/count?customerId=${customerId}&workspaceId=${workspaceId}`,
fetcher,
);

return (
<CustomerPartnerEarningsTable
commissions={commissions}
totalCommissions={totalCommissions?.all?.count}
totalCommissions={
isTotalCommissionsLoading
? undefined
: totalCommissions?.all?.count ?? commissions?.length
}
viewAllHref={`/${slug}/programs/${programId}/commissions?customerId=${customerId}`}
isLoading={isComissionsLoading || isTotalCommissionsLoading}
isLoading={isComissionsLoading}
/>
);
},
Expand Down
6 changes: 4 additions & 2 deletions apps/web/app/app.dub.co/embed/referrals/earnings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SALES_PAGE_SIZE } from "@/lib/partners/constants";
import { REFERRALS_EMBED_EARNINGS_LIMIT } from "@/lib/partners/constants";
import { PartnerEarningsResponse } from "@/lib/types";
import { CommissionStatusBadges } from "@/ui/partners/commission-status-badges";
import { Gift, StatusBadge, Table, usePagination, useTable } from "@dub/ui";
Expand All @@ -13,7 +13,9 @@ import { motion } from "framer-motion";
import useSWR from "swr";

export function ReferralsEmbedEarnings({ salesCount }: { salesCount: number }) {
const { pagination, setPagination } = usePagination(SALES_PAGE_SIZE);
const { pagination, setPagination } = usePagination(
REFERRALS_EMBED_EARNINGS_LIMIT,
);
const { data: earnings, isLoading } = useSWR<PartnerEarningsResponse[]>(
`/api/embed/referrals/earnings?page=${pagination.pageIndex}`,
fetcher,
Expand Down
Loading