feat: add stripe webhooks

This commit is contained in:
Mauricio Siu
2024-10-20 15:08:44 -06:00
parent fe0a662afd
commit ffe7b04bea
9 changed files with 4476 additions and 214 deletions

View File

@@ -37,16 +37,23 @@ export const ReviewPayment = ({ isAnnual, serverQuantity }: Props) => {
}, },
); );
// const { data: calculateNewMonthlyCost } = const { data: calculateNewMonthlyCost } =
// api.stripe.calculateNewMonthlyCost.useQuery( api.stripe.calculateNewMonthlyCost.useQuery(
// { {
// serverQuantity, serverQuantity,
// isAnnual, isAnnual,
// }, },
// { {
// enabled: !!serverQuantity && isOpen, enabled: !!serverQuantity && isOpen,
// }, },
// ); );
const isSameServersQty =
Number(billingSubscription?.totalServers) === serverQuantity;
const isSameCost =
Number(calculateNewMonthlyCost) ===
Number(billingSubscription?.monthlyAmount);
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -86,7 +93,6 @@ export const ReviewPayment = ({ isAnnual, serverQuantity }: Props) => {
{billingSubscription?.nextPaymentDate {billingSubscription?.nextPaymentDate
? format(billingSubscription?.nextPaymentDate, "MMM d, yyyy") ? format(billingSubscription?.nextPaymentDate, "MMM d, yyyy")
: "-"} : "-"}
{/* {format(billingSubscription?.nextPaymentDate, "MMM d, yyyy")} */}
</span> </span>
</div> </div>
</div> </div>
@@ -112,19 +118,15 @@ export const ReviewPayment = ({ isAnnual, serverQuantity }: Props) => {
<div className="grid flex-1 gap-2"> <div className="grid flex-1 gap-2">
<Label>Difference</Label> <Label>Difference</Label>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{Number(billingSubscription?.totalServers) === serverQuantity {isSameServersQty ? "-" : `$${calculateUpgradeCost} USD`}{" "}
? "-"
: `$${calculateUpgradeCost} USD`}{" "}
</span> </span>
</div> </div>
{/* <div className="grid flex-1 gap-2"> <div className="grid flex-1 gap-2">
<Label>New {isAnnual ? "annual" : "monthly"} cost</Label> <Label>New {isAnnual ? "annual" : "monthly"} cost</Label>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{Number(billingSubscription?.totalServers) === serverQuantity {isSameCost ? "-" : `$${calculateNewMonthlyCost} USD`}{" "}
? "-"
: `${calculateNewMonthlyCost} USD`}{" "}
</span> </span>
</div> */} </div>
</div> </div>
</div> </div>

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { NumberInput } from "@/components/ui/input"; import { NumberInput } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -6,6 +7,7 @@ import { api } from "@/utils/api";
import { loadStripe } from "@stripe/stripe-js"; import { loadStripe } from "@stripe/stripe-js";
import clsx from "clsx"; import clsx from "clsx";
import { CheckIcon, MinusIcon, PlusIcon } from "lucide-react"; import { CheckIcon, MinusIcon, PlusIcon } from "lucide-react";
import { useRouter } from "next/router";
import React, { useState } from "react"; import React, { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { ReviewPayment } from "./review-payment"; import { ReviewPayment } from "./review-payment";
@@ -25,35 +27,88 @@ export const calculatePrice = (count: number, isAnnual = false) => {
return 7.99 + (count - 3) * 3.5; return 7.99 + (count - 3) * 3.5;
}; };
export const calculateYearlyCost = (serverQuantity: number) => {
const count = serverQuantity;
if (count === 1) return 4.0 * 12;
if (count <= 3) return 7.99 * 12;
return (7.99 + (count - 3) * 3.5) * 12;
};
export const ShowBilling = () => { export const ShowBilling = () => {
const router = useRouter();
const { data: billingSubscription } =
api.stripe.getBillingSubscription.useQuery(undefined);
const { data: servers } = api.server.all.useQuery(undefined);
const { data: admin } = api.admin.one.useQuery(); const { data: admin } = api.admin.one.useQuery();
const { data, refetch } = api.stripe.getProducts.useQuery(); const { data, refetch } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } = const { mutateAsync: createCheckoutSession } =
api.stripe.createCheckoutSession.useMutation(); api.stripe.createCheckoutSession.useMutation();
const { mutateAsync: createCustomerPortalSession } =
api.stripe.createCustomerPortalSession.useMutation();
const [serverQuantity, setServerQuantity] = useState(3); const [serverQuantity, setServerQuantity] = useState(3);
const { mutateAsync: upgradeSubscription } = const { mutateAsync: upgradeSubscriptionMonthly } =
api.stripe.upgradeSubscription.useMutation(); api.stripe.upgradeSubscriptionMonthly.useMutation();
const { mutateAsync: upgradeSubscriptionAnnual } =
api.stripe.upgradeSubscriptionAnnual.useMutation();
const [isAnnual, setIsAnnual] = useState(false); const [isAnnual, setIsAnnual] = useState(false);
// useEffect(() => {
// if (billingSubscription) {
// setIsAnnual(
// (prevIsAnnual) =>
// billingSubscription.billingInterval === "year" &&
// prevIsAnnual !== true,
// );
// }
// }, [billingSubscription]);
const handleCheckout = async (productId: string) => { const handleCheckout = async (productId: string) => {
const stripe = await stripePromise; const stripe = await stripePromise;
if (data && admin?.stripeSubscriptionId && data.subscriptions.length > 0) { if (data && admin?.stripeSubscriptionId && data.subscriptions.length > 0) {
upgradeSubscription({ if (isAnnual) {
subscriptionId: admin?.stripeSubscriptionId, upgradeSubscriptionAnnual({
serverQuantity, subscriptionId: admin?.stripeSubscriptionId,
isAnnual, serverQuantity,
})
.then(async (subscription) => {
toast.success("Subscription upgraded successfully");
await refetch();
}) })
.catch((error) => { .then(async (subscription) => {
toast.error("Error to upgrade the subscription"); if (subscription.type === "new") {
console.error(error); await stripe?.redirectToCheckout({
}); sessionId: subscription.sessionId,
});
return;
}
toast.success("Subscription upgraded successfully");
await refetch();
})
.catch((error) => {
toast.error("Error to upgrade the subscription");
console.error(error);
});
} else {
upgradeSubscriptionMonthly({
subscriptionId: admin?.stripeSubscriptionId,
serverQuantity,
})
.then(async (subscription) => {
if (subscription.type === "new") {
await stripe?.redirectToCheckout({
sessionId: subscription.sessionId,
});
return;
}
toast.success("Subscription upgraded successfully");
await refetch();
})
.catch((error) => {
toast.error("Error to upgrade the subscription");
console.error(error);
});
}
} else { } else {
createCheckoutSession({ createCheckoutSession({
productId, productId,
@@ -66,25 +121,31 @@ export const ShowBilling = () => {
}); });
} }
}; };
const products = data?.products.filter((product) => {
const interval = product?.default_price?.recurring?.interval;
return isAnnual ? interval === "year" : interval === "month";
});
return ( return (
<div className="flex flex-col gap-4 w-full justify-center"> <div className="flex flex-col gap-4 w-full justify-center">
<Badge>{admin?.stripeSubscriptionStatus}</Badge>
<Tabs <Tabs
defaultValue="monthly" defaultValue="monthly"
value={isAnnual ? "annual" : "monthly"}
className="w-full" className="w-full"
onValueChange={(e) => { onValueChange={(e) => setIsAnnual(e === "annual")}
console.log(e);
setIsAnnual(e === "annual");
}}
> >
<TabsList> <TabsList>
<TabsTrigger value="monthly">Monthly</TabsTrigger> <TabsTrigger value="monthly">Monthly</TabsTrigger>
<TabsTrigger value="annual">Annual</TabsTrigger> <TabsTrigger value="annual">Annual</TabsTrigger>
</TabsList> </TabsList>
</Tabs> </Tabs>
{data?.products?.map((product) => { {products?.map((product) => {
const featured = true; // const suscripcion = data?.subscriptions.find((subscription) =>
// subscription.items.data.find((item) => item.pr === product.id),
// );
const featured = true;
return ( return (
<div key={product.id}> <div key={product.id}>
<section <section
@@ -95,6 +156,23 @@ export const ShowBilling = () => {
: "lg:py-8", : "lg:py-8",
)} )}
> >
{isAnnual ? (
<div className="flex flex-row gap-2 items-center">
<p className=" text-2xl font-semibold tracking-tight text-primary ">
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
</p>
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
${" "}
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(2)}{" "}
/ Month USD
</p>
</div>
) : (
<p className=" text-2xl font-semibold tracking-tight text-primary ">
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
</p>
)}
<h3 className="mt-5 font-medium text-lg text-white"> <h3 className="mt-5 font-medium text-lg text-white">
{product.name} {product.name}
</h3> </h3>
@@ -106,9 +184,6 @@ export const ShowBilling = () => {
> >
{product.description} {product.description}
</p> </p>
<p className="order-first text-3xl font-semibold tracking-tight text-primary">
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
</p>
<ul <ul
role="list" role="list"
@@ -123,7 +198,8 @@ export const ShowBilling = () => {
"Self-hosted on your own infrastructure", "Self-hosted on your own infrastructure",
"Full access to all deployment features", "Full access to all deployment features",
"Dokploy integration", "Dokploy integration",
"Free", "Backups",
"All Incoming features",
].map((feature) => ( ].map((feature) => (
<li key={feature} className="flex text-muted-foreground"> <li key={feature} className="flex text-muted-foreground">
<CheckIcon /> <CheckIcon />
@@ -181,21 +257,34 @@ export const ShowBilling = () => {
</div> </div>
<div <div
className={cn( className={cn(
data.subscriptions.length > 0 data?.subscriptions && data?.subscriptions?.length > 0
? "justify-between" ? "justify-between"
: "justify-end", : "justify-end",
"flex flex-row items-center gap-2 mt-4", "flex flex-row items-center gap-2 mt-4",
)} )}
> >
{data.subscriptions.length > 0 && ( {data &&
<ReviewPayment data?.subscriptions?.length > 0 &&
isAnnual={isAnnual} billingSubscription?.billingInterval === "year" &&
serverQuantity={serverQuantity} isAnnual && (
/> <ReviewPayment
)} isAnnual={true}
serverQuantity={serverQuantity}
/>
)}
{data &&
data?.subscriptions?.length > 0 &&
billingSubscription?.billingInterval === "month" &&
!isAnnual && (
<ReviewPayment
isAnnual={false}
serverQuantity={serverQuantity}
/>
)}
<div className="justify-end"> <div className="justify-end w-full">
<Button <Button
className="w-full"
onClick={async () => { onClick={async () => {
handleCheckout(product.id); handleCheckout(product.id);
}} }}
@@ -211,18 +300,21 @@ export const ShowBilling = () => {
); );
})} })}
{/* <Button <Button
variant="destructive" variant="secondary"
onClick={async () => { onClick={async () => {
// Crear una sesión del portal del cliente // Crear una sesión del portal del cliente
const session = await createCustomerPortalSession(); const session = await createCustomerPortalSession();
// Redirigir al portal del cliente en Stripe // router.push(session.url,"",{});
window.location.href = session.url; window.open(session.url);
}}
> // Redirigir al portal del cliente en Stripe
Manage Subscription // window.location.href = session.url;
</Button> */} }}
>
Manage Subscription
</Button>
</div> </div>
); );
}; };

View File

@@ -0,0 +1 @@
ALTER TABLE "admin" ADD COLUMN "stripeSubscriptionStatus" text;

File diff suppressed because it is too large Load Diff

View File

@@ -295,6 +295,13 @@
"when": 1729314952330, "when": 1729314952330,
"tag": "0041_small_aaron_stack", "tag": "0041_small_aaron_stack",
"breakpoints": true "breakpoints": true
},
{
"idx": 42,
"version": "6",
"when": 1729455812207,
"tag": "0042_smooth_swordsman",
"breakpoints": true
} }
] ]
} }

View File

@@ -0,0 +1,129 @@
import { db } from "@/server/db";
import { admins, github } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { buffer } from "node:stream/consumers";
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia",
});
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET || "";
export const config = {
api: {
bodyParser: false, // Deshabilitar el body parser de Next.js
},
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const buf = await buffer(req); // Leer el raw body como un Buffer
const sig = req.headers["stripe-signature"] as string;
let event: Stripe.Event;
try {
// Verificar el evento usando el raw body (buf)
event = stripe.webhooks.constructEvent(buf, sig, endpointSecret);
const newSubscription = event.data.object as Stripe.Subscription;
console.log(event.type);
switch (event.type) {
case "customer.subscription.created":
await db
.update(admins)
.set({
stripeSubscriptionId: newSubscription.id,
stripeSubscriptionStatus: newSubscription.status,
})
.where(
eq(
admins.stripeCustomerId,
typeof newSubscription.customer === "string"
? newSubscription.customer
: "",
),
)
.returning();
break;
case "customer.subscription.deleted":
await db
.update(admins)
.set({
stripeSubscriptionStatus: "canceled",
})
.where(
eq(
admins.stripeCustomerId,
typeof newSubscription.customer === "string"
? newSubscription.customer
: "",
),
);
break;
case "customer.subscription.updated":
console.log(newSubscription.status);
// Suscripción actualizada (upgrade, downgrade, cambios)
await db
.update(admins)
.set({
stripeSubscriptionStatus: newSubscription.status,
})
.where(
eq(
admins.stripeCustomerId,
typeof newSubscription.customer === "string"
? newSubscription.customer
: "",
),
);
break;
case "invoice.payment_succeeded":
console.log(newSubscription.customer);
await db
.update(admins)
.set({
stripeSubscriptionStatus: "active",
})
.where(
eq(
admins.stripeCustomerId,
typeof newSubscription.customer === "string"
? newSubscription.customer
: "",
),
);
break;
case "invoice.payment_failed":
// Pago fallido
await db
.update(admins)
.set({
stripeSubscriptionStatus: "payment_failed",
})
.where(
eq(
admins.stripeCustomerId,
typeof newSubscription.customer === "string"
? newSubscription.customer
: "",
),
);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).json({ received: true });
} catch (err) {
console.error("Webhook signature verification failed.", err.message);
return res.status(400).send("Webhook Error: ");
}
}

View File

@@ -1,7 +1,10 @@
import { admins } from "@/server/db/schema"; import { admins } from "@/server/db/schema";
import { import {
ADDITIONAL_PRICE_YEARLY_ID,
BASE_PRICE_MONTHLY_ID, BASE_PRICE_MONTHLY_ID,
BASE_PRICE_YEARLY_ID,
GROWTH_PRICE_MONTHLY_ID, GROWTH_PRICE_MONTHLY_ID,
GROWTH_PRICE_YEARLY_ID,
SERVER_ADDITIONAL_PRICE_MONTHLY_ID, SERVER_ADDITIONAL_PRICE_MONTHLY_ID,
getStripeItems, getStripeItems,
getStripePrices, getStripePrices,
@@ -66,12 +69,14 @@ export const stripeRouter = createTRPCRouter({
const session = await stripe.checkout.sessions.create({ const session = await stripe.checkout.sessions.create({
// payment_method_types: ["card"], // payment_method_types: ["card"],
mode: "subscription", mode: "subscription",
line_items: [...items], line_items: items,
// subscription_data: { // subscription_data: {
// trial_period_days: 0, // trial_period_days: 0,
// }, // },
metadata: { subscription_data: {
serverQuantity: input.serverQuantity, metadata: {
serverQuantity: input.serverQuantity,
},
}, },
success_url: success_url:
"http://localhost:3000/api/stripe.success?sessionId={CHECKOUT_SESSION_ID}", "http://localhost:3000/api/stripe.success?sessionId={CHECKOUT_SESSION_ID}",
@@ -81,12 +86,11 @@ export const stripeRouter = createTRPCRouter({
return { sessionId: session.id }; return { sessionId: session.id };
}), }),
upgradeSubscription: adminProcedure upgradeSubscriptionMonthly: adminProcedure
.input( .input(
z.object({ z.object({
subscriptionId: z.string(), // ID de la suscripción actual subscriptionId: z.string(),
serverQuantity: z.number().min(1), serverQuantity: z.number().min(1),
isAnnual: z.boolean(),
}), }),
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -94,32 +98,53 @@ export const stripeRouter = createTRPCRouter({
apiVersion: "2024-09-30.acacia", apiVersion: "2024-09-30.acacia",
}); });
const { subscriptionId, serverQuantity, isAnnual } = input; const { subscriptionId, serverQuantity } = input;
const admin = await findAdminById(ctx.user.adminId);
const suscription = await stripe.subscriptions.retrieve(subscriptionId);
const currentItems = suscription.items.data;
// If have a monthly plan, we need to create a new subscription
const haveMonthlyPlan = currentItems.find(
(item) =>
item.price.id === BASE_PRICE_YEARLY_ID ||
item.price.id === GROWTH_PRICE_YEARLY_ID ||
item.price.id === ADDITIONAL_PRICE_YEARLY_ID,
);
// Price IDs if (haveMonthlyPlan) {
// const price1ServerId = "price_1QBk3bF3cxQuHeOzCmSlyFB3"; // $4.00 const items = getStripeItems(serverQuantity, false);
// const priceUpToThreeId = "price_1QBkPiF3cxQuHeOzceNiM2OJ"; // $7.99 const session = await stripe.checkout.sessions.create({
// const priceAdditionalId = "price_1QBkr9F3cxQuHeOzTBo46Bmy"; // $3.50 line_items: items,
mode: "subscription",
...(admin.stripeCustomerId && {
customer: admin.stripeCustomerId,
}),
subscription_data: {
metadata: {
serverQuantity: input.serverQuantity,
},
},
success_url:
"http://localhost:3000/api/stripe.success?sessionId={CHECKOUT_SESSION_ID}",
cancel_url: "http://localhost:3000/dashboard/settings/billing",
});
return {
type: "new",
success: true,
sessionId: session.id,
};
}
const basePriceId = BASE_PRICE_MONTHLY_ID;
const growthPriceId = GROWTH_PRICE_MONTHLY_ID;
const additionalPriceId = SERVER_ADDITIONAL_PRICE_MONTHLY_ID;
// Obtener suscripción actual // Obtener suscripción actual
const { baseItem, additionalItem } = await getStripeSubscriptionItems( const { baseItem, additionalItem } = await getStripeSubscriptionItems(
subscriptionId, subscriptionId,
isAnnual, false,
); );
// const updateBasePlan = async (newPriceId: string) => {
// await stripe.subscriptions.update(subscriptionId, {
// items: [
// {
// id: baseItem?.id,
// price: newPriceId,
// quantity: 1,
// },
// ],
// proration_behavior: "always_invoice",
// });
// };
const deleteAdditionalItem = async () => { const deleteAdditionalItem = async () => {
if (additionalItem) { if (additionalItem) {
await stripe.subscriptionItems.del(additionalItem.id); await stripe.subscriptionItems.del(additionalItem.id);
@@ -137,7 +162,7 @@ export const stripeRouter = createTRPCRouter({
await stripe.subscriptions.update(subscriptionId, { await stripe.subscriptions.update(subscriptionId, {
items: [ items: [
{ {
price: SERVER_ADDITIONAL_PRICE_MONTHLY_ID, price: additionalPriceId,
quantity: additionalServers, quantity: additionalServers,
}, },
], ],
@@ -148,38 +173,137 @@ export const stripeRouter = createTRPCRouter({
if (serverQuantity === 1) { if (serverQuantity === 1) {
await deleteAdditionalItem(); await deleteAdditionalItem();
if ( if (baseItem?.price.id !== basePriceId && baseItem?.price.id) {
baseItem?.price.id !== BASE_PRICE_MONTHLY_ID && await updateBasePlan(subscriptionId, baseItem?.id, basePriceId);
baseItem?.price.id
) {
await updateBasePlan(
subscriptionId,
baseItem?.id,
BASE_PRICE_MONTHLY_ID,
);
} }
} else if (serverQuantity >= 2 && serverQuantity <= 3) { } else if (serverQuantity >= 2 && serverQuantity <= 3) {
await deleteAdditionalItem(); await deleteAdditionalItem();
if ( if (baseItem?.price.id !== growthPriceId && baseItem?.price.id) {
baseItem?.price.id !== GROWTH_PRICE_MONTHLY_ID && await updateBasePlan(subscriptionId, baseItem?.id, growthPriceId);
baseItem?.price.id
) {
await updateBasePlan(
subscriptionId,
baseItem?.id,
GROWTH_PRICE_MONTHLY_ID,
);
} }
} else if (serverQuantity > 3) { } else if (serverQuantity > 3) {
if ( if (baseItem?.price.id !== growthPriceId && baseItem?.price.id) {
baseItem?.price.id !== GROWTH_PRICE_MONTHLY_ID && await updateBasePlan(subscriptionId, baseItem?.id, growthPriceId);
baseItem?.price.id }
) { const additionalServers = serverQuantity - 3;
await updateBasePlan( await updateOrCreateAdditionalItem(additionalServers);
subscriptionId, }
baseItem?.id,
GROWTH_PRICE_MONTHLY_ID, await stripe.subscriptions.update(subscriptionId, {
); metadata: {
serverQuantity: serverQuantity.toString(),
},
});
return { success: true };
}),
upgradeSubscriptionAnnual: adminProcedure
.input(
z.object({
subscriptionId: z.string(),
serverQuantity: z.number().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia",
});
const { subscriptionId, serverQuantity } = input;
const currentSubscription =
await stripe.subscriptions.retrieve(subscriptionId);
if (!currentSubscription) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Subscription not found",
});
}
const admin = await findAdminById(ctx.user.adminId);
const currentItems = currentSubscription.items.data;
// If have a monthly plan, we need to create a new subscription
const haveMonthlyPlan = currentItems.find(
(item) =>
item.price.id === BASE_PRICE_MONTHLY_ID ||
item.price.id === GROWTH_PRICE_MONTHLY_ID ||
item.price.id === SERVER_ADDITIONAL_PRICE_MONTHLY_ID,
);
if (haveMonthlyPlan) {
const items = getStripeItems(serverQuantity, true);
const session = await stripe.checkout.sessions.create({
line_items: items,
mode: "subscription",
...(admin.stripeCustomerId && {
customer: admin.stripeCustomerId,
}),
subscription_data: {
metadata: {
serverQuantity: input.serverQuantity,
},
},
success_url:
"http://localhost:3000/api/stripe.success?sessionId={CHECKOUT_SESSION_ID}",
cancel_url: "http://localhost:3000/dashboard/settings/billing",
});
return {
type: "new",
success: true,
sessionId: session.id,
};
}
const basePriceId = BASE_PRICE_YEARLY_ID;
const growthPriceId = GROWTH_PRICE_YEARLY_ID;
const additionalPriceId = ADDITIONAL_PRICE_YEARLY_ID;
// Obtener suscripción actual
const { baseItem, additionalItem } = await getStripeSubscriptionItems(
subscriptionId,
true,
);
const deleteAdditionalItem = async () => {
if (additionalItem) {
await stripe.subscriptionItems.del(additionalItem.id);
}
};
const updateOrCreateAdditionalItem = async (
additionalServers: number,
) => {
if (additionalItem) {
await stripe.subscriptionItems.update(additionalItem.id, {
quantity: additionalServers,
});
} else {
await stripe.subscriptions.update(subscriptionId, {
items: [
{
price: additionalPriceId,
quantity: additionalServers,
},
],
proration_behavior: "always_invoice",
});
}
};
if (serverQuantity === 1) {
await deleteAdditionalItem();
if (baseItem?.price.id !== basePriceId && baseItem?.price.id) {
await updateBasePlan(subscriptionId, baseItem?.id, basePriceId);
}
} else if (serverQuantity >= 2 && serverQuantity <= 3) {
await deleteAdditionalItem();
if (baseItem?.price.id !== growthPriceId && baseItem?.price.id) {
await updateBasePlan(subscriptionId, baseItem?.id, growthPriceId);
}
} else if (serverQuantity > 3) {
if (baseItem?.price.id !== growthPriceId && baseItem?.price.id) {
await updateBasePlan(subscriptionId, baseItem?.id, growthPriceId);
} }
const additionalServers = serverQuantity - 3; const additionalServers = serverQuantity - 3;
await updateOrCreateAdditionalItem(additionalServers); await updateOrCreateAdditionalItem(additionalServers);
@@ -231,6 +355,18 @@ export const stripeRouter = createTRPCRouter({
const session = await stripe.checkout.sessions.retrieve(sessionId); const session = await stripe.checkout.sessions.retrieve(sessionId);
if (session.payment_status === "paid") { if (session.payment_status === "paid") {
const admin = await findAdminById(ctx.user.adminId);
if (admin.stripeSubscriptionId) {
const subscription = await stripe.subscriptions.retrieve(
admin.stripeSubscriptionId,
);
if (subscription.status === "active") {
await stripe.subscriptions.update(admin.stripeSubscriptionId, {
cancel_at_period_end: true,
});
}
}
console.log("Payment successful!"); console.log("Payment successful!");
const stripeCustomerId = session.customer as string; const stripeCustomerId = session.customer as string;
@@ -267,8 +403,9 @@ export const stripeRouter = createTRPCRouter({
const subscription = const subscription =
await stripe.subscriptions.retrieve(stripeSubscriptionId); await stripe.subscriptions.retrieve(stripeSubscriptionId);
let billingInterval: Stripe.Price.Recurring.Interval | undefined;
const totalServers = subscription.metadata.serverQuantity; const totalServers = subscription.metadata.serverQuantity;
console.log(subscription.metadata);
let totalAmount = 0; let totalAmount = 0;
for (const item of subscription.items.data) { for (const item of subscription.items.data) {
@@ -276,12 +413,14 @@ export const stripeRouter = createTRPCRouter({
const amountPerUnit = item.price.unit_amount / 100; const amountPerUnit = item.price.unit_amount / 100;
totalAmount += quantity * amountPerUnit; totalAmount += quantity * amountPerUnit;
billingInterval = item.price.recurring?.interval;
} }
return { return {
nextPaymentDate: new Date(subscription.current_period_end * 1000), nextPaymentDate: new Date(subscription.current_period_end * 1000),
monthlyAmount: `${totalAmount.toFixed(2)} USD`, monthlyAmount: totalAmount.toFixed(2),
totalServers, totalServers,
billingInterval,
}; };
}), }),
@@ -306,20 +445,19 @@ export const stripeRouter = createTRPCRouter({
} }
const subscriptionId = admin.stripeSubscriptionId; const subscriptionId = admin.stripeSubscriptionId;
const items = await getStripeSubscriptionItemsCalculate(
subscriptionId,
input.serverQuantity,
input.isAnnual,
);
console.log(items);
if (!subscriptionId) { if (!subscriptionId) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Subscription not found", message: "Subscription not found",
}); });
} }
const items = await getStripeSubscriptionItemsCalculate(
subscriptionId,
input.serverQuantity,
input.isAnnual,
);
const upcomingInvoice = await stripe.invoices.retrieveUpcoming({ const upcomingInvoice = await stripe.invoices.retrieveUpcoming({
subscription: subscriptionId, subscription: subscriptionId,
subscription_items: items, subscription_items: items,

View File

@@ -78,99 +78,57 @@ export const getStripeSubscriptionItemsCalculate = async (
const currentItems = subscription.items.data; const currentItems = subscription.items.data;
const items = []; const items = [];
if (isAnnual) { const basePriceId = isAnnual ? BASE_PRICE_YEARLY_ID : BASE_PRICE_MONTHLY_ID;
const baseItem = currentItems.find( const growthPriceId = isAnnual
(item) => ? GROWTH_PRICE_YEARLY_ID
item.price.id === BASE_PRICE_YEARLY_ID || : GROWTH_PRICE_MONTHLY_ID;
item.price.id === GROWTH_PRICE_YEARLY_ID, const additionalPriceId = isAnnual
); ? ADDITIONAL_PRICE_YEARLY_ID
const additionalItem = currentItems.find( : SERVER_ADDITIONAL_PRICE_MONTHLY_ID;
(item) => item.price.id === ADDITIONAL_PRICE_YEARLY_ID,
);
if (serverQuantity === 1) {
if (baseItem) {
items.push({
id: baseItem.id,
price: BASE_PRICE_YEARLY_ID,
quantity: 1,
});
}
} else if (serverQuantity <= 3) {
if (baseItem) {
items.push({
id: baseItem.id,
price: GROWTH_PRICE_YEARLY_ID,
quantity: 1,
});
}
} else {
if (baseItem) {
items.push({
id: baseItem.id,
price: GROWTH_PRICE_YEARLY_ID,
quantity: 1,
});
}
if (additionalItem) { const baseItem = currentItems.find(
items.push({ (item) => item.price.id === basePriceId || item.price.id === growthPriceId,
id: additionalItem.id, );
price: ADDITIONAL_PRICE_YEARLY_ID, const additionalItem = currentItems.find(
quantity: serverQuantity - 3, (item) => item.price.id === additionalPriceId,
}); );
} else {
items.push({ if (serverQuantity === 1) {
price: ADDITIONAL_PRICE_YEARLY_ID, if (baseItem) {
quantity: serverQuantity - 3, items.push({
}); id: baseItem.id,
} price: basePriceId,
quantity: 1,
});
}
} else if (serverQuantity <= 3) {
if (baseItem) {
items.push({
id: baseItem.id,
price: growthPriceId,
quantity: 1,
});
} }
} else { } else {
const baseItem = currentItems.find( if (baseItem) {
(item) => items.push({
item.price.id === BASE_PRICE_MONTHLY_ID || id: baseItem.id,
item.price.id === GROWTH_PRICE_MONTHLY_ID, price: growthPriceId,
); quantity: 1,
const additionalItem = currentItems.find( });
(item) => item.price.id === SERVER_ADDITIONAL_PRICE_MONTHLY_ID, }
);
if (serverQuantity === 1) {
if (baseItem) {
items.push({
id: baseItem.id,
price: BASE_PRICE_MONTHLY_ID,
quantity: 1,
});
}
} else if (serverQuantity <= 3) {
if (baseItem) {
items.push({
id: baseItem.id,
price: GROWTH_PRICE_MONTHLY_ID,
quantity: 1,
});
}
} else {
if (baseItem) {
items.push({
id: baseItem.id,
price: GROWTH_PRICE_MONTHLY_ID,
quantity: 1,
});
}
if (additionalItem) { if (additionalItem) {
items.push({ items.push({
id: additionalItem.id, id: additionalItem.id,
price: SERVER_ADDITIONAL_PRICE_MONTHLY_ID, price: additionalPriceId,
quantity: serverQuantity - 3, quantity: serverQuantity - 3,
}); });
} else { } else {
items.push({ items.push({
price: SERVER_ADDITIONAL_PRICE_MONTHLY_ID, price: additionalPriceId,
quantity: serverQuantity - 3, quantity: serverQuantity - 3,
}); });
}
} }
} }

View File

@@ -30,6 +30,7 @@ export const admins = pgTable("admin", {
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
stripeCustomerId: text("stripeCustomerId"), stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"), stripeSubscriptionId: text("stripeSubscriptionId"),
stripeSubscriptionStatus: text("stripeSubscriptionStatus"),
totalServers: integer("totalServers").notNull().default(0), totalServers: integer("totalServers").notNull().default(0),
}); });