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
3 changes: 3 additions & 0 deletions apps/web/app/(ee)/api/customers/[id]/stripe-invoices/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { getCustomerOrThrow } from "@/lib/api/customers/get-customer-or-throw";
import { getCustomerStripeInvoices } from "@/lib/api/customers/get-customer-stripe-invoices";
import { DubApiError } from "@/lib/api/errors";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { withWorkspace } from "@/lib/auth";
import { NextResponse } from "next/server";

export const GET = withWorkspace(async ({ workspace, params }) => {
const { id: customerId } = params;

Expand Down Expand Up @@ -30,6 +32,7 @@ export const GET = withWorkspace(async ({ workspace, params }) => {
const stripeCustomerInvoices = await getCustomerStripeInvoices({
stripeCustomerId: customer.stripeCustomerId,
stripeConnectId: workspace.stripeConnectId,
programId: getDefaultProgramIdOrThrow(workspace),
});

return NextResponse.json(stripeCustomerInvoices);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ function CreateCommissionSheetContent({
);

const stripeInvoices = stripeInvoicesData?.invoices ?? [];
const unimportedStripeInvoices = stripeInvoices.filter(
(inv) => !inv.dubCommissionId,
);
const noStripeCustomerId = stripeInvoicesData?.noStripeCustomerId ?? false;
const noStripeCustomerMessage = stripeInvoicesData?.message;

Expand Down Expand Up @@ -279,8 +282,8 @@ function CreateCommissionSheetContent({
return "This customer doesn't have a Stripe customer ID. Add one in the customer profile before proceeding.";
}

if (stripeInvoices.length === 0) {
return "No paid Stripe invoices found for this customer.";
if (unimportedStripeInvoices.length === 0) {
return "No unimported Stripe invoices found for this customer.";
}
} else {
if (!saleAmount) {
Expand All @@ -299,7 +302,7 @@ function CreateCommissionSheetContent({
saleAmount, // sale commission amount
useExistingEvents,
noStripeCustomerId,
stripeInvoices.length,
unimportedStripeInvoices.length,
isStripeInvoicesLoading,
]);

Expand Down Expand Up @@ -652,22 +655,47 @@ function CreateCommissionSheetContent({
{stripeInvoices.map((inv) => (
<div
key={inv.id}
className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5"
className={cn(
"flex items-center justify-between gap-3 rounded-md px-3 py-2.5",
inv.dubCommissionId &&
"bg-neutral-50/80 opacity-75",
)}
>
<div className="min-w-0 flex-1">
<a
href={`https://dashboard.stripe.com/invoices/${inv.id}`}
target="_blank"
rel="noopener noreferrer"
className="cursor-alias font-mono text-sm font-medium text-neutral-800 decoration-dotted underline-offset-2 hover:underline"
className={cn(
"cursor-alias font-mono text-sm font-medium decoration-dotted underline-offset-2 hover:underline",
inv.dubCommissionId
? "text-neutral-500"
: "text-neutral-800",
)}
>
{inv.id}
</a>
<p className="mt-0.5 text-xs text-neutral-500">
<p className="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-neutral-500">
{formatDate(inv.createdAt)}
{inv.dubCommissionId && (
<a
href={`/${slug}/program/commissions?partnerId=${partnerId}&customerId=${customerId}`}
target="_blank"
className="rounded bg-neutral-200/80 px-1.5 py-0.5 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-900"
>
Already imported
</a>
)}
</p>
</div>
<span className="shrink-0 text-sm font-medium text-neutral-700">
<span
className={cn(
"shrink-0 text-sm font-medium",
inv.dubCommissionId
? "text-neutral-500"
: "text-neutral-700",
)}
>
{currencyFormatter(inv.amount)}
</span>
</div>
Expand Down Expand Up @@ -978,7 +1006,7 @@ function CreateCommissionSheetContent({
<Button
type="submit"
variant="primary"
text={`Create commission${stripeInvoices.length > 0 ? "s" : ""}`}
text={`Create commission${unimportedStripeInvoices.length > 0 ? "s" : ""}`}
className="w-fit"
loading={isPending}
disabledTooltip={submitDisabledMessage}
Expand Down
16 changes: 8 additions & 8 deletions apps/web/lib/actions/partners/create-manual-commission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,18 @@ export const createManualCommissionAction = authActionClient
const stripeCustomerInvoices = await getCustomerStripeInvoices({
stripeCustomerId: customer.stripeCustomerId,
stripeConnectId: workspace.stripeConnectId,
}).then(
(
invoices, // sort invoices by created date ascending
) =>
invoices.sort(
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
),
programId,
}).then((invoices) =>
invoices
// filter out invoices that are already associated with a commission on Dub
.filter((invoice) => !invoice.dubCommissionId)
// sort invoices by created date ascending
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()),
);

if (stripeCustomerInvoices.length === 0) {
throw new Error(
`No paid Stripe invoices found for customer ${customer.email} (${customer.stripeCustomerId}).`,
`No unimported Stripe invoices found for customer ${customer.email} (${customer.stripeCustomerId}).`,
);
}

Expand Down
27 changes: 26 additions & 1 deletion apps/web/lib/api/customers/get-customer-stripe-invoices.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { stripeAppClient } from "@/lib/stripe";
import { StripeCustomerInvoiceSchema } from "@/lib/zod/schemas/customers";
import { prisma } from "@dub/prisma";

const stripe = stripeAppClient({
...(process.env.VERCEL_ENV && { mode: "live" }),
Expand All @@ -8,9 +9,11 @@ const stripe = stripeAppClient({
export async function getCustomerStripeInvoices({
stripeCustomerId,
stripeConnectId,
programId,
}: {
stripeCustomerId: string;
stripeConnectId: string;
programId: string;
}) {
const { data } = await stripe.invoices.list(
{
Expand All @@ -22,13 +25,35 @@ export async function getCustomerStripeInvoices({
stripeAccount: stripeConnectId,
},
);
const validInvoices = data.filter(
(invoice): invoice is (typeof data)[number] & { id: string } =>
typeof invoice.id === "string",
);

const commissions = await prisma.commission.findMany({
where: {
invoiceId: {
in: validInvoices.map((invoice) => invoice.id),
},
programId: programId,
},
});

const invoiceIdCommissionIdMap = commissions.reduce(
(acc, commission) => {
acc[commission.invoiceId!] = commission.id;
return acc;
},
{} as Record<string, string>,
);

const stripeCustomerInvoices = data.map((invoice) =>
const stripeCustomerInvoices = validInvoices.map((invoice) =>
StripeCustomerInvoiceSchema.parse({
id: invoice.id,
amount: invoice.amount_paid,
createdAt: new Date(invoice.created * 1000),
metadata: invoice,
dubCommissionId: invoiceIdCommissionIdMap[invoice.id],
}),
);

Expand Down
9 changes: 6 additions & 3 deletions apps/web/lib/partners/create-partner-commission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ export const createPartnerCommission = async ({
});

const subscriptionDurationMonths = firstCommission
? differenceInMonths(new Date(), firstCommission.createdAt)
? differenceInMonths(
createdAt ?? new Date(), // account for custom commission creation date
firstCommission.createdAt,
)
: 0;

context = {
Expand Down Expand Up @@ -233,13 +236,13 @@ export const createPartnerCommission = async ({
// Recurring sale reward (maxDuration > 0)
else {
const subscriptionDurationMonths = differenceInMonths(
new Date(),
createdAt ?? new Date(), // account for custom commission creation date
firstCommission.createdAt,
);

if (subscriptionDurationMonths >= reward.maxDuration) {
console.log(
`Partner ${partnerId} has reached max duration for ${event} event, skipping commission creation...`,
`Partner ${partnerId} has reached max duration for ${event} event (subscription duration: ${subscriptionDurationMonths} months, max duration: ${reward.maxDuration} months), skipping commission creation...`,
);

return {
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/zod/schemas/customers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,5 @@ export const StripeCustomerInvoiceSchema = z.object({
amount: z.number(),
createdAt: z.date(),
metadata: z.any(),
dubCommissionId: z.string().nullable(),
});