Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
59 changes: 51 additions & 8 deletions apps/web/app/(ee)/api/appsflyer/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { captureWebhookLog } from "@/lib/api-logs/capture-webhook-log";
import { trackLead } from "@/lib/api/conversions/track-lead";
import { trackSale } from "@/lib/api/conversions/track-sale";
import { isLocalDev } from "@/lib/api/environment";
Expand All @@ -10,7 +11,9 @@ import { isIpInRange } from "@/lib/middleware/utils/is-ip-in-range";
import { trackLeadRequestSchema } from "@/lib/zod/schemas/leads";
import { trackSaleRequestSchema } from "@/lib/zod/schemas/sales";
import { prisma } from "@dub/prisma";
import { Project } from "@dub/prisma/client";
import { APPSFLYER_INTEGRATION_ID, getSearchParams } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
import { NextResponse } from "next/server";
import * as z from "zod/v4";

Expand All @@ -22,6 +25,14 @@ const querySchema = z.object({

// GET /api/appsflyer/webhook – listen to Postback events from AppsFlyer
export const GET = withAxiom(async (req) => {
const startTime = Date.now();
let response = "OK";
let queryParams: Record<string, string> | null = null;
let workspace: Pick<
Project,
"id" | "stripeConnectId" | "webhookEnabled"
> | null = null;

try {
if (!isLocalDev) {
const ip = await getIP();
Expand All @@ -37,7 +48,7 @@ export const GET = withAxiom(async (req) => {
}
}

const queryParams = getSearchParams(req.url);
queryParams = getSearchParams(req.url);

const { appId, partnerEventId } = querySchema.parse(queryParams);

Expand Down Expand Up @@ -72,6 +83,8 @@ export const GET = withAxiom(async (req) => {
});
}

workspace = installation.project;

// Track lead event
if (partnerEventId === "lead") {
const {
Expand All @@ -93,15 +106,15 @@ export const GET = withAxiom(async (req) => {
eventQuantity: undefined,
mode: undefined,
metadata: null,
workspace: installation.project,
workspace,
rawBody: queryParams,
});

return NextResponse.json("Lead event tracked successfully.");
response = "Lead event tracked successfully.";
}

// Track sale event
if (partnerEventId === "sale") {
else if (partnerEventId === "sale") {
const amountInCents = appsflyerAmountToDubCents(queryParams.amount);
const { eventName, customerExternalId, amount, currency, invoiceId } =
trackSaleRequestSchema.parse({
Expand All @@ -118,16 +131,46 @@ export const GET = withAxiom(async (req) => {
invoiceId,
leadEventName: undefined,
metadata: null,
workspace: installation.project,
workspace,
rawBody: queryParams,
});

return NextResponse.json("Sale event tracked successfully.");
response = "Sale event tracked successfully.";
}

return NextResponse.json("OK");
waitUntil(
captureWebhookLog({
workspaceId: workspace.id,
method: req.method,
path: "/appsflyer/webhook",
statusCode: 200,
duration: Date.now() - startTime,
requestBody: queryParams,
responseBody: response,
userAgent: req.headers.get("user-agent"),
}),
);

return NextResponse.json(response);
} catch (error) {
return handleAndReturnErrorResponse(error);
const errorResponse = handleAndReturnErrorResponse(error);

if (workspace) {
waitUntil(
captureWebhookLog({
workspaceId: workspace.id,
method: req.method,
path: "/appsflyer/webhook",
statusCode: errorResponse.status,
duration: Date.now() - startTime,
requestBody: queryParams,
responseBody: errorResponse,
userAgent: req.headers.get("user-agent"),
}),
);
}

return errorResponse;
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { recomputePartnerPayoutState } from "@/lib/payouts/recompute-partner-pay
import { prisma } from "@dub/prisma";
import type Stripe from "stripe";

export async function accountApplicationDeauthorized(event: Stripe.Event) {
export async function accountApplicationDeauthorized(
event: Stripe.AccountApplicationDeauthorizedEvent,
) {
const stripeAccount = event.account;

if (!stripeAccount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const balanceAvailableQueue = qstash.queue({
queueName: "handle-balance-available",
});

export async function accountUpdated(event: Stripe.Event) {
const account = event.data.object as Stripe.Account;
export async function accountUpdated(event: Stripe.AccountUpdatedEvent) {
const account = event.data.object;
const { country, business_type } = account;

const partner = await prisma.partner.findUnique({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ const queue = qstash.queue({
queueName: "handle-balance-available",
});

export async function balanceAvailable(event: Stripe.Event) {
export async function balanceAvailable(
event:
| Stripe.AccountExternalAccountUpdatedEvent
| Stripe.BalanceAvailableEvent,
) {
const stripeAccount = event.account;

if (!stripeAccount) {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/(ee)/api/stripe/connect/webhook/payout-failed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ const queue = qstash.queue({
queueName: "handle-payout-failed",
});

export async function payoutFailed(event: Stripe.Event) {
export async function payoutFailed(event: Stripe.PayoutFailedEvent) {
const stripeAccount = event.account;

if (!stripeAccount) {
return "No stripeConnectId found in event. Skipping...";
}

const stripePayout = event.data.object as Stripe.Payout;
const stripePayout = event.data.object;

const response = await queue.enqueueJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/payouts/payout-failed`,
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ const queue = qstash.queue({
queueName: "handle-payout-paid",
});

export async function payoutPaid(event: Stripe.Event) {
export async function payoutPaid(event: Stripe.PayoutPaidEvent) {
const stripeAccount = event.account;

if (!stripeAccount) {
return "No stripeConnectId found in event. Skipping...";
}

const stripePayout = event.data.object as Stripe.Payout;
const stripePayout = event.data.object;
const stripePayoutTraceId = stripePayout.trace_id?.value ?? null;

const response = await queue.enqueueJSON({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import type Stripe from "stripe";

// Handle event "account.application.deauthorized"
export async function accountApplicationDeauthorized(
event: Stripe.Event,
event: Stripe.AccountApplicationDeauthorizedEvent,
mode: StripeMode,
) {
const stripeAccountId = event.account;

if (mode === "test") {
return `Stripe Connect account ${stripeAccountId} deauthorized in test mode. Skipping...`;
return {
response: `Stripe Connect account ${stripeAccountId} deauthorized in test mode. Skipping...`,
};
}

const workspace = await prisma.project.findUnique({
Expand All @@ -24,7 +26,9 @@ export async function accountApplicationDeauthorized(
});

if (!workspace) {
return `Stripe Connect account ${stripeAccountId} deauthorized.`;
return {
response: `Workspace not found for Stripe account ${stripeAccountId}, skipping...`,
};
}

await prisma.project.update({
Expand All @@ -46,5 +50,8 @@ export async function accountApplicationDeauthorized(
},
});

return `Stripe Connect account ${stripeAccountId} deauthorized for workspace ${workspace.id}`;
return {
response: `Stripe Connect account ${stripeAccountId} deauthorized for workspace ${workspace.id}`,
workspaceId: workspace.id,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { prisma } from "@dub/prisma";
import type Stripe from "stripe";

// Handle event "charge.refunded"
export async function chargeRefunded(event: Stripe.Event, mode: StripeMode) {
const charge = event.data.object as Stripe.Charge;
export async function chargeRefunded(
event: Stripe.ChargeRefundedEvent,
mode: StripeMode,
) {
const charge = event.data.object;
const stripeAccountId = event.account as string;

const stripe = stripeAppClient({
Expand All @@ -31,7 +34,9 @@ export async function chargeRefunded(event: Stripe.Event, mode: StripeMode) {
invoicePayments.data.length > 0 ? invoicePayments.data[0] : null;

if (!invoicePayment || !invoicePayment.invoice) {
return `Charge ${charge.id} has no invoice, skipping...`;
return {
response: `Charge ${charge.id} has no invoice, skipping...`,
};
}

const workspace = await prisma.project.findUnique({
Expand All @@ -45,11 +50,18 @@ export async function chargeRefunded(event: Stripe.Event, mode: StripeMode) {
});

if (!workspace) {
return `Workspace not found for stripe account ${stripeAccountId}`;
return {
response: `Workspace not found for Stripe account ${stripeAccountId}, skipping...`,
};
}

const workspaceId = workspace.id;

if (!workspace.programs.length) {
return `Workspace ${workspace.id} for stripe account ${stripeAccountId} has no programs, skipping...`;
return {
response: `Workspace ${workspaceId} for stripe account ${stripeAccountId} has no programs, skipping...`,
workspaceId,
};
}

const commission = await prisma.commission.findUnique({
Expand All @@ -71,11 +83,17 @@ export async function chargeRefunded(event: Stripe.Event, mode: StripeMode) {
});

if (!commission) {
return `Commission not found for invoice ${invoicePayment.invoice}`;
return {
response: `Commission not found for invoice ${invoicePayment.invoice}`,
workspaceId,
};
}

if (commission.status === "paid") {
return `Commission ${commission.id} is already paid, skipping...`;
return {
response: `Commission ${commission.id} is already paid, skipping...`,
workspaceId,
};
}

// if the commission is processed and has a payout, we need to update the payout total
Expand Down Expand Up @@ -122,5 +140,8 @@ export async function chargeRefunded(event: Stripe.Event, mode: StripeMode) {
newStatus: "refunded",
});

return `Commission ${commission.id} updated to status "refunded"`;
return {
response: `Commission ${commission.id} updated to status "refunded"`,
workspaceId,
};
}
Loading
Loading