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