Skip to content

Commit 867da4e

Browse files
committed
partner-profile guardrails
1 parent 37458c1 commit 867da4e

7 files changed

Lines changed: 75 additions & 106 deletions

File tree

apps/web/app/api/partner-profile/programs/[programId]/customers/[customerId]/activity/route.ts

Lines changed: 0 additions & 75 deletions
This file was deleted.

apps/web/app/api/partner-profile/programs/[programId]/customers/[customerId]/route.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getCustomerEvents } from "@/lib/analytics/get-customer-events";
12
import { transformCustomer } from "@/lib/api/customers/transform-customer";
23
import { DubApiError } from "@/lib/api/errors";
34
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
@@ -11,7 +12,7 @@ import { NextResponse } from "next/server";
1112
export const GET = withPartnerProfile(async ({ partner, params }) => {
1213
const { customerId, programId } = params;
1314

14-
const { program } = await getProgramEnrollmentOrThrow({
15+
const { program, links } = await getProgramEnrollmentOrThrow({
1516
partnerId: partner.id,
1617
programId: programId,
1718
});
@@ -25,20 +26,64 @@ export const GET = withPartnerProfile(async ({ partner, params }) => {
2526
if (!customer || customer?.projectId !== program.workspaceId) {
2627
throw new DubApiError({
2728
code: "not_found",
28-
message:
29-
"Customer not found. Make sure you're using the correct customer ID (e.g. `cus_3TagGjzRzmsFJdH8od2BNCsc`).",
29+
message: "Customer is not part of this program.",
3030
});
3131
}
3232

33-
customer.avatar = null;
34-
customer.email;
33+
const events = await getCustomerEvents({
34+
customerId: customer.id,
35+
linkIds: links.map((link) => link.id),
36+
});
37+
38+
if (events.length === 0) {
39+
throw new DubApiError({
40+
code: "not_found",
41+
message: "Customer is not attributed to any links by this partner.",
42+
});
43+
}
44+
45+
// get the first partner link that this customer interacted with
46+
const firstLinkId = events[events.length - 1].link_id;
47+
const link = links.find((link) => link.id === firstLinkId);
48+
49+
// Find the LTV of the customer
50+
// TODO: Calculate this from all events, not limited
51+
const ltv = events.reduce((acc, event) => {
52+
if (event.event === "sale" && event.saleAmount) {
53+
acc += Number(event.saleAmount);
54+
}
55+
56+
return acc;
57+
}, 0);
58+
59+
// Find the time to lead of the customer
60+
const timeToLead =
61+
customer.clickedAt && customer.createdAt
62+
? customer.createdAt.getTime() - customer.clickedAt.getTime()
63+
: null;
64+
65+
// Find the time to first sale of the customer
66+
// TODO: Calculate this from all events, not limited
67+
const firstSale = events.filter(({ event }) => event === "sale").pop();
68+
69+
const timeToSale =
70+
firstSale && customer.createdAt
71+
? new Date(firstSale.timestamp).getTime() - customer.createdAt.getTime()
72+
: null;
3573

3674
return NextResponse.json(
37-
PartnerProfileCustomerSchema.parse(
38-
transformCustomer({
75+
PartnerProfileCustomerSchema.parse({
76+
...transformCustomer({
3977
...customer,
4078
email: customer.email || customer.name || generateRandomName(),
4179
}),
42-
),
80+
activity: {
81+
ltv,
82+
timeToLead,
83+
timeToSale,
84+
events,
85+
link,
86+
},
87+
}),
4388
);
4489
});

apps/web/app/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
import { CUSTOMER_PAGE_EVENTS_LIMIT } from "@/lib/partners/constants";
44
import useProgramEnrollment from "@/lib/swr/use-program-enrollment";
55
import {
6-
CustomerActivityResponse,
7-
CustomerEnriched,
86
PartnerEarningsResponse,
7+
PartnerProfileCustomerProps,
98
} from "@/lib/types";
109
import { CustomerActivityList } from "@/ui/customers/customer-activity-list";
1110
import { CustomerDetailsColumn } from "@/ui/customers/customer-details-column";
@@ -25,23 +24,12 @@ export function ProgramCustomerPageClient() {
2524
customerId: string;
2625
}>();
2726

28-
const {
29-
data: customer,
30-
isLoading,
31-
error,
32-
} = useSWR<CustomerEnriched>(
27+
const { data: customer, isLoading } = useSWR<PartnerProfileCustomerProps>(
3328
`/api/partner-profile/programs/${programSlug}/customers/${customerId}`,
3429
fetcher,
3530
);
3631

37-
const { data: customerActivity, isLoading: isCustomerActivityLoading } =
38-
useSWR<CustomerActivityResponse>(
39-
customer &&
40-
`/api/partner-profile/programs/${programSlug}/customers/${customer.id}/activity`,
41-
fetcher,
42-
);
43-
44-
if (!customer && !isLoading && !error) notFound();
32+
if (!customer && !isLoading) notFound();
4533

4634
return (
4735
<div className="mb-10 mt-2">
@@ -102,8 +90,8 @@ export function ProgramCustomerPageClient() {
10290
Activity
10391
</h2>
10492
<CustomerActivityList
105-
activity={customerActivity}
106-
isLoading={!customer || isCustomerActivityLoading}
93+
activity={customer?.activity}
94+
isLoading={!customer}
10795
/>
10896
</section>
10997
</div>
@@ -112,8 +100,8 @@ export function ProgramCustomerPageClient() {
112100
<div className="-order-1 lg:order-1">
113101
<CustomerDetailsColumn
114102
customer={customer}
115-
customerActivity={customerActivity}
116-
isCustomerActivityLoading={!customer || isCustomerActivityLoading}
103+
customerActivity={customer?.activity}
104+
isCustomerActivityLoading={!customer}
117105
/>
118106
</div>
119107
</div>

apps/web/lib/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import z from "@/lib/zod";
22
import { metaTagsSchema } from "@/lib/zod/schemas/metatags";
33
import {
44
PartnerEarningsSchema,
5+
PartnerProfileCustomerSchema,
56
PartnerProfileLinkSchema,
67
} from "@/lib/zod/schemas/partner-profile";
78
import { DirectorySyncProviders } from "@boxyhq/saml-jackson";
@@ -388,6 +389,10 @@ export type PartnerProps = z.infer<typeof PartnerSchema>;
388389

389390
export type ProgramPartnerLinkProps = z.infer<typeof ProgramPartnerLinkSchema>;
390391

392+
export type PartnerProfileCustomerProps = z.infer<
393+
typeof PartnerProfileCustomerSchema
394+
>;
395+
391396
export type PartnerProfileLinkProps = z.infer<typeof PartnerProfileLinkSchema>;
392397

393398
export type EnrolledPartnerProps = z.infer<

apps/web/lib/zod/schemas/partner-profile.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getCommissionsCountQuerySchema,
99
getCommissionsQuerySchema,
1010
} from "./commissions";
11+
import { customerActivityResponseSchema } from "./customer-activity";
1112
import { CustomerEnrichedSchema } from "./customers";
1213
import { LinkSchema } from "./links";
1314

@@ -80,10 +81,11 @@ export const PartnerProfileLinkSchema = LinkSchema.pick({
8081

8182
export const PartnerProfileCustomerSchema = CustomerEnrichedSchema.pick({
8283
id: true,
83-
createdAt: true,
8484
country: true,
85+
createdAt: true,
8586
}).extend({
8687
email: z
8788
.string()
8889
.transform((email) => email.replace(/(?<=^.).+(?=.@)/, "****")),
90+
activity: customerActivityResponseSchema,
8991
});

apps/web/ui/customers/customer-details-column.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ export function CustomerDetailsColumn({
1818
customerActivity,
1919
isCustomerActivityLoading,
2020
}: {
21-
customer?: CustomerProps;
21+
customer?: Omit<CustomerProps, "name" | "externalId"> & {
22+
name?: string;
23+
externalId?: string;
24+
};
2225
customerActivity?: CustomerActivityResponse;
2326
isCustomerActivityLoading: boolean;
2427
}) {
@@ -129,7 +132,7 @@ export function CustomerDetailsColumn({
129132
)}
130133
</div>
131134

132-
{customer && (customer?.externalId ?? null) !== null && (
135+
{customer?.externalId && (
133136
<div className="flex flex-col gap-2">
134137
<DetailHeading>External ID</DetailHeading>
135138
{

packages/tinybird/pipes/v2_customer_events.pipe

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ SQL >
158158
SELECT *
159159
FROM sale_events
160160
)
161-
ORDER BY timestamp {% if order == 'asc' %} ASC {% else %} DESC {% end %}
161+
ORDER BY
162+
timestamp DESC, CASE event WHEN 'click' THEN 1 WHEN 'lead' THEN 2 WHEN 'sale' THEN 3 END DESC
162163

163164

0 commit comments

Comments
 (0)