feat(licenses): enhance Stripe webhook handling for license creation

- Added support for the "invoice.paid" event to create licenses upon successful payment.
- Refactored the handling of subscription updates to improve license deactivation logic.
- Removed the expiration date from the LicenseEmail component to simplify email content.
- Improved error handling for customer retrieval and subscription status checks.
This commit is contained in:
Mauricio Siu
2025-03-23 12:58:40 -06:00
parent c8e6df4c29
commit 5180c785b4
2 changed files with 50 additions and 68 deletions

View File

@@ -34,8 +34,6 @@ router.use(
}), }),
); );
console.log(process.env.DATABASE_URL);
const validateSchema = z.object({ const validateSchema = z.object({
licenseKey: z.string(), licenseKey: z.string(),
serverIp: z.string(), serverIp: z.string(),
@@ -161,6 +159,7 @@ router.post("/stripe/webhook", async (c) => {
"invoice.payment_succeeded", "invoice.payment_succeeded",
"invoice.payment_failed", "invoice.payment_failed",
"customer.subscription.deleted", "customer.subscription.deleted",
"invoice.paid",
]; ];
if (!allowedEvents.includes(event.type)) { if (!allowedEvents.includes(event.type)) {
@@ -169,44 +168,54 @@ router.post("/stripe/webhook", async (c) => {
try { try {
switch (event.type) { switch (event.type) {
case "checkout.session.completed": { case "customer.subscription.updated": {
const session = event.data.object as Stripe.Checkout.Session; const subscription = event.data.object as Stripe.Subscription;
const customerResponse = await stripe.customers.retrieve( const customerResponse = await stripe.customers.retrieve(
session.customer as string, subscription.customer as string,
);
if (subscription.status !== "active" || customerResponse.deleted) {
await deactivateLicense(subscription.id);
}
break;
}
case "invoice.paid": {
const invoice = event.data.object as Stripe.Invoice;
if (!invoice.subscription) break;
if (invoice.billing_reason === "subscription_create") {
const customerResponse = await stripe.customers.retrieve(
invoice.customer as string,
); );
if (customerResponse.deleted) { if (customerResponse.deleted) {
throw new Error("Customer was deleted"); throw new Error("Customer was deleted");
} }
const lineItems = await stripe.checkout.sessions.listLineItems( const subscriptionId = invoice.subscription as string;
session.id, const subscription =
); await stripe.subscriptions.retrieve(subscriptionId);
const priceId = lineItems.data[0].price?.id; const priceId = subscription.items.data[0].price.id;
const { type, billingType } = getLicenseTypeFromPriceId(priceId);
const { type, billingType } = getLicenseTypeFromPriceId(priceId!);
const license = await createLicense({ const license = await createLicense({
productId: session.id, productId: subscriptionId,
type, type,
billingType, billingType,
email: session.customer_details?.email!, email: customerResponse.email!,
stripeCustomerId: customerResponse.id, stripeCustomerId: customerResponse.id,
stripeSubscriptionId: session.subscription as string, stripeSubscriptionId: subscriptionId,
}); });
console.log("License created", license);
const features = getLicenseFeatures(type); const features = getLicenseFeatures(type);
console.log("Features", features);
const emailHtml = await render( const emailHtml = await render(
LicenseEmail({ LicenseEmail({
customerName: customerResponse.name || "Customer", customerName: customerResponse.name || "Customer",
licenseKey: license.licenseKey, licenseKey: license.licenseKey,
productName: `Dokploy Self Hosted ${type}`, productName: `Dokploy Self Hosted ${type}`,
// TODO: Add expiration date
expirationDate: new Date(),
features: features, features: features,
}), }),
); );
@@ -217,19 +226,6 @@ router.post("/stripe/webhook", async (c) => {
subject: "Your Dokploy License Key ", subject: "Your Dokploy License Key ",
html: emailHtml, html: emailHtml,
}); });
break;
}
case "customer.subscription.updated": {
const subscription = event.data.object as Stripe.Subscription;
const customerResponse = await stripe.customers.retrieve(
subscription.customer as string,
);
if (subscription.status !== "active" || customerResponse.deleted) {
await deactivateLicense(subscription.id);
} }
break; break;

View File

@@ -18,7 +18,6 @@ interface LicenseEmailProps {
customerName: string; customerName: string;
licenseKey: string; licenseKey: string;
productName: string; productName: string;
expirationDate: Date;
features: string[]; features: string[];
} }
@@ -28,15 +27,8 @@ export const LicenseEmail = ({
customerName = "John Doe", customerName = "John Doe",
licenseKey = "1234567890", licenseKey = "1234567890",
productName = "Dokploy", productName = "Dokploy",
expirationDate = new Date(),
features = ["Feature 1", "Feature 2", "Feature 3"], features = ["Feature 1", "Feature 2", "Feature 3"],
}: LicenseEmailProps): React.ReactElement => { }: LicenseEmailProps): React.ReactElement => {
const formattedDate = expirationDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
return ( return (
<Html> <Html>
<Head /> <Head />
@@ -69,12 +61,6 @@ export const LicenseEmail = ({
<Text style={licenseKeyStyle}>{licenseKey}</Text> <Text style={licenseKeyStyle}>{licenseKey}</Text>
</Section> </Section>
<Section style={validitySection}>
<Text style={validityText}>
🗓️ Next billing date: <strong>{formattedDate}</strong>
</Text>
</Section>
<Section style={featuresContainer}> <Section style={featuresContainer}>
<Heading as="h2" style={h2}> <Heading as="h2" style={h2}>
🎉 Your Premium Features 🎉 Your Premium Features