One-time purchase
Manage the orders of your product.
When a new purchase occurs, the code stores the event in a table called Order
.
Here are the steps needed to support it.
Database
Add this table definition to your prisma/schema.prisma
file:
schema.prisma
model Order {
id Int @id @default(autoincrement())
email String
lemonOrderId String?
lemonProductId String
lemonVariantId String?
lemonVariantName String?
lemonPlanName String?
lemonPlanPrice String?
lemonSubscriptionId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
validUntil DateTime?
cancelUrl String?
updateUrl String?
status String?
}
Push the new table to your database:
terminal
npx prisma db push
Then open the file src/app/api/webhooks/lemonsqueezy/route.ts
and replace its content with this:
src/app/api/webhooks/lemonsqueezy/route.ts
import { NextResponse } from "next/server";
import rawBody from "raw-body";
import crypto from "crypto";
import { Readable } from "stream";
import { headers } from "next/headers";
import { prismaClient } from "@/prisma/db";
type ISODate = string;
export type WebhookRequest = {
meta: {
event_name:
| "order_created"
| "order_refunded"
| "subscription_created"
| "subscription_updated"
| "subscription_cancelled"
| "subscription_resumed"
| "subscription_expired"
| "subscription_paused"
| "subscription_unpaused"
| "subscription_payment_failed"
| "subscription_payment_success"
| "subscription_payment_recovered"
| "license_key_created"
| "license_key_updated";
};
data: {
type: "subscriptions";
id: string;
attributes: {
store_id: number;
customer_id: number;
order_id: number;
order_item_id: number;
product_id: number;
variant_id: number;
product_name: string;
variant_name: string;
user_name: string;
user_email: string;
status: string;
status_formatted: string;
card_brand: string;
card_last_four: string;
pause: null;
cancelled: boolean;
trial_ends_at: ISODate;
billing_anchor: number;
urls: {
update_payment_method: string;
};
renews_at: "2023-01-24T12:43:48.000000Z";
ends_at: null;
created_at: "2023-01-17T12:43:50.000000Z";
updated_at: "2023-01-17T12:43:51.000000Z";
test_mode: false;
first_order_item: {
id: number;
price: number;
order_id: number;
price_id: number;
test_mode: boolean;
created_at: ISODate;
product_id: number;
updated_at: ISODate;
variant_id: number;
product_name: string;
variant_name: string;
};
};
relationships: {
store: {
links: {
related: string;
self: string;
};
};
customer: {
links: {
related: string;
self: string;
};
};
order: {
links: {
related: string;
self: string;
};
};
"order-item": {
links: {
related: string;
self: string;
};
};
product: {
links: {
related: string;
self: string;
};
};
variant: {
links: {
related: string;
self: string;
};
};
"subscription-invoices": {
links: {
related: string;
self: string;
};
};
};
links: {
self: string;
};
};
};
export async function POST(request: Request) {
const body = await rawBody(Readable.from(Buffer.from(await request.text())));
const headersList = headers();
const payload: WebhookRequest = JSON.parse(body.toString());
console.log(">>> payload", payload);
const sigString = headersList.get("x-signature");
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET as string;
const hmac = crypto.createHmac("sha256", secret);
const digest = Buffer.from(hmac.update(body).digest("hex"), "utf8");
const signature = Buffer.from(
Array.isArray(sigString) ? sigString.join("") : sigString || "",
"utf8"
);
// validate signature
if (!crypto.timingSafeEqual(digest, signature)) {
return NextResponse.json({ message: "Invalid signature" }, { status: 403 });
}
const userEmail = payload.data.attributes.user_email;
const eventName = payload.meta.event_name;
const userOrder = await prismaClient.order.findFirst({
where: {
email: userEmail,
status: "CREATED",
},
});
if (eventName === "order_created") {
await prismaClient.order.create({
data: {
email: userEmail,
lemonOrderId:
payload.data.attributes.first_order_item.order_id.toString(),
lemonProductId:
payload.data.attributes.first_order_item.product_id.toString(),
lemonVariantId:
payload.data.attributes.first_order_item.variant_id.toString(),
lemonVariantName: payload.data.attributes.first_order_item.variant_name,
lemonPlanName: payload.data.attributes.first_order_item.product_name,
lemonPlanPrice: null,
lemonSubscriptionId: null,
validUntil: new Date(),
updateUrl: null,
status: "CREATED",
},
});
}
if (eventName === "order_refunded") {
if (userOrder) {
await prismaClient.order.update({
where: {
id: userOrder.id,
},
data: {
status: "REFUNDED",
},
});
}
}
return NextResponse.json({ result: true }, { status: 200 });
}
Lemon Squeezy
Enable the order_created order_refunded events on Lemon Squeezy, in the Webhook configuration.
Settings > Webhooks > Edit the WebHook
Now, whenever a customer buys your product, a new record will be created in the table public.Order
Last updated