Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/web/app/(ee)/api/cron/import/rewardful/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { importAffiliateCoupons } from "@/lib/rewardful/import-affiliate-coupons";
import { importCampaigns } from "@/lib/rewardful/import-campaigns";
import { importCommissions } from "@/lib/rewardful/import-commissions";
import { importCustomers } from "@/lib/rewardful/import-customers";
Expand All @@ -23,6 +24,9 @@ export async function POST(req: Request) {
case "import-partners":
await importPartners(payload);
break;
case "import-affiliate-coupons":
await importAffiliateCoupons(payload);
break;
case "import-customers":
await importCustomers(payload);
break;
Expand Down
13 changes: 13 additions & 0 deletions apps/web/lib/rewardful/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DubApiError } from "@/lib/api/errors";
import {
RewardfulAffiliate,
RewardfulAffiliateCoupon,
RewardfulCampaign,
RewardfulCommission,
RewardfulReferral,
Expand Down Expand Up @@ -93,4 +94,16 @@ export class RewardfulApi {

return data;
}

async listAffiliateCoupons({ page = 1 }: { page?: number }) {
const searchParams = new URLSearchParams();
searchParams.append("page", page.toString());
searchParams.append("limit", PAGE_LIMIT.toString());

const { data } = await this.fetch<{ data: RewardfulAffiliateCoupon[] }>(
`${this.baseUrl}/affiliate_coupons?${searchParams.toString()}`,
);

return data;
}
}
114 changes: 114 additions & 0 deletions apps/web/lib/rewardful/import-affiliate-coupons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { prisma } from "@dub/prisma";
import { bulkCreateLinks } from "../api/links";
import { redis } from "../upstash";
import { RewardfulApi } from "./api";
import { MAX_BATCHES, rewardfulImporter } from "./importer";
import { RewardfulImportPayload } from "./types";

export async function importAffiliateCoupons(payload: RewardfulImportPayload) {
const { programId, userId, page = 1 } = payload;

const program = await prisma.program.findUniqueOrThrow({
where: {
id: programId,
},
select: {
id: true,
workspaceId: true,
domain: true,
url: true,
defaultFolderId: true,
},
});

const { token } = await rewardfulImporter.getCredentials(program.workspaceId);

const rewardfulApi = new RewardfulApi({ token });

let currentPage = page;
let hasMore = true;
let processedBatches = 0;

while (hasMore && processedBatches < MAX_BATCHES) {
const affiliateCoupons = await rewardfulApi.listAffiliateCoupons({
page: currentPage,
});

if (affiliateCoupons.length === 0) {
hasMore = false;
break;
}

const affiliateIds = affiliateCoupons.map(
(affiliateCoupon) => affiliateCoupon.affiliate_id,
);

const results = await redis.hmget<Record<string, string>>(
`rewardful:affiliates:${program.id}`,
...affiliateIds,
);

const filteredPartners = Object.fromEntries(
Object.entries(results ?? {}).filter(
([_, value]) => value !== null && value !== undefined,
),
);

const affiliateIdToCouponsMap = affiliateCoupons.reduce(
(acc, coupon) => {
if (!acc[coupon.affiliate_id]) {
acc[coupon.affiliate_id] = [];
}

acc[coupon.affiliate_id].push(coupon);
return acc;
},
{} as Record<string, typeof affiliateCoupons>,
);

if (Object.keys(filteredPartners).length > 0) {
const linksToCreate: any[] = [];

for (const [affiliateId, partnerId] of Object.entries(filteredPartners)) {
const coupons = affiliateIdToCouponsMap[affiliateId];

if (!coupons) {
continue;
}

linksToCreate.push(
...coupons.map((coupon) => ({
domain: program.domain,
key: coupon.token.toLowerCase(),
url: program.url,
trackConversion: true,
programId,
partnerId,
folderId: program.defaultFolderId,
userId,
projectId: program.workspaceId,
})),
);
}

await bulkCreateLinks({
links: linksToCreate,
});
}

currentPage++;
processedBatches++;
}

if (!hasMore) {
await redis.del(`rewardful:affiliates:${program.id}`);
}

const action = hasMore ? "import-affiliate-coupons" : "import-customers";

await rewardfulImporter.queue({
...payload,
action,
page: hasMore ? currentPage : undefined,
});
}
2 changes: 2 additions & 0 deletions apps/web/lib/rewardful/import-campaigns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export async function importCampaigns(payload: RewardfulImportPayload) {
new Date(),
createdGroup.createdAt,
);

console.log(
`This group was created ${createdSecondsAgo} seconds ago (most likely ${createdSecondsAgo < 10 ? "created" : "upserted"})`,
);
Expand Down Expand Up @@ -90,6 +91,7 @@ export async function importCampaigns(payload: RewardfulImportPayload) {
: commission_percent,
},
});

console.log(
`Since group was newly created, also created reward ${createdReward.id} with amount ${createdReward.amount} and type ${createdReward.type}`,
);
Expand Down
20 changes: 18 additions & 2 deletions apps/web/lib/rewardful/import-partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { nanoid } from "@dub/utils";
import { createId } from "../api/create-id";
import { bulkCreateLinks } from "../api/links";
import { logImportError } from "../tinybird/log-import-error";
import { redis } from "../upstash";
import { RewardfulApi } from "./api";
import { MAX_BATCHES, rewardfulImporter } from "./importer";
import { RewardfulAffiliate, RewardfulImportPayload } from "./types";
Expand Down Expand Up @@ -72,10 +73,11 @@ export async function importPartners(payload: RewardfulImportPayload) {
}

if (activeAffiliates.length > 0) {
await Promise.all(
const partners = await Promise.all(
activeAffiliates.map((affiliate) => {
const groupId = campaignIdToGroupMap[affiliate.campaign.id];
const group = program.groups.find((group) => group.id === groupId);

if (!group) {
console.error(
`Group not found for campaign ${affiliate.campaign.id}`,
Expand All @@ -102,6 +104,15 @@ export async function importPartners(payload: RewardfulImportPayload) {
});
}),
);

await redis.hset(
`rewardful:affiliates:${program.id}`,
Object.fromEntries(
partners
.filter((p): p is NonNullable<typeof p> => p !== undefined)
.map((p) => [p.rewardfulAffiliateId, p.dubPartnerId]),
),
);
}

if (notImportedAffiliates.length > 0) {
Expand All @@ -119,7 +130,7 @@ export async function importPartners(payload: RewardfulImportPayload) {
processedBatches++;
}

const action = hasMore ? "import-partners" : "import-customers";
const action = hasMore ? "import-partners" : "import-affiliate-coupons";

await rewardfulImporter.queue({
...payload,
Expand Down Expand Up @@ -206,4 +217,9 @@ async function createPartnerAndLinks({
partnerGroupDefaultLinkId: idx === 0 ? partnerGroupDefaultLinkId : null,
})),
});

return {
rewardfulAffiliateId: affiliate.id,
dubPartnerId: partner.id,
};
}
1 change: 1 addition & 0 deletions apps/web/lib/rewardful/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from "zod";
export const rewardfulImportSteps = z.enum([
"import-campaigns",
"import-partners",
"import-affiliate-coupons",
"import-customers",
"import-commissions",
]);
Expand Down
9 changes: 9 additions & 0 deletions apps/web/lib/rewardful/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ export interface RewardfulCommission {
sale: RewardfulCommissionSale;
}

export interface RewardfulAffiliateCoupon {
id: string;
external_id: string;
archived: boolean;
archived_at: string;
token: string;
affiliate_id: string;
}

export type RewardfulImportPayload = z.infer<
typeof rewardfulImportPayloadSchema
>;