mirror of
https://github.com/sqlchat/sqlchat.git
synced 2025-09-23 18:43:18 +08:00
feat: separate subscription from payment
This commit is contained in:
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `amount` on the `subscription` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `currency` on the `subscription` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `customer_id` on the `subscription` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `description` on the `subscription` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `payment_id` on the `subscription` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `receipt` on the `subscription` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "subscription_payment_id_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "subscription" DROP COLUMN "amount",
|
||||||
|
DROP COLUMN "currency",
|
||||||
|
DROP COLUMN "customer_id",
|
||||||
|
DROP COLUMN "description",
|
||||||
|
DROP COLUMN "payment_id",
|
||||||
|
DROP COLUMN "receipt";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "payment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL DEFAULT '',
|
||||||
|
"email" TEXT NOT NULL DEFAULT '',
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"payment_id" TEXT NOT NULL DEFAULT '',
|
||||||
|
"customer_id" TEXT NOT NULL DEFAULT '',
|
||||||
|
"description" TEXT NOT NULL DEFAULT '',
|
||||||
|
"amount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"currency" TEXT NOT NULL DEFAULT '',
|
||||||
|
"receipt" TEXT NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
CONSTRAINT "payment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "payment_user_id_idx" ON "payment"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "payment_email_idx" ON "payment"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "payment_payment_id_key" ON "payment"("payment_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "payment" ADD CONSTRAINT "payment_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `status` on the `subscription` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "subscription" DROP COLUMN "status",
|
||||||
|
ADD COLUMN "canceled_at" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- DropEnum
|
||||||
|
DROP TYPE "SubscriptionStatus";
|
@ -58,38 +58,46 @@ model Usage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Subscription {
|
model Subscription {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
userId String @default("") @map("user_id")
|
userId String @default("") @map("user_id")
|
||||||
// Denormalize to avoid join with the user table
|
// Denormalize to avoid join with the user table
|
||||||
email String @default("") @map("email")
|
email String @default("") @map("email")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
status SubscriptionStatus @default(ACTIVE)
|
canceledAt DateTime? @map("canceled_at")
|
||||||
startAt DateTime @default(now()) @map("start_at")
|
startAt DateTime @default(now()) @map("start_at")
|
||||||
expireAt DateTime @default(now()) @map("expire_at")
|
expireAt DateTime @default(now()) @map("expire_at")
|
||||||
paymentId String @default("") @map("payment_id")
|
plan SubscriptionPlan
|
||||||
customerId String @default("") @map("customer_id")
|
|
||||||
plan SubscriptionPlan
|
|
||||||
description String @default("")
|
|
||||||
amount Int @default(0)
|
|
||||||
currency String @default("")
|
|
||||||
receipt String @default("")
|
|
||||||
|
|
||||||
@@unique([paymentId])
|
|
||||||
@@index([userId], map: "subscription_user_id_idx")
|
@@index([userId], map: "subscription_user_id_idx")
|
||||||
@@index([email], map: "subscription_email_idx")
|
@@index([email], map: "subscription_email_idx")
|
||||||
@@map("subscription")
|
@@map("subscription")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SubscriptionStatus {
|
|
||||||
ACTIVE
|
|
||||||
CANCELED
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SubscriptionPlan {
|
enum SubscriptionPlan {
|
||||||
PRO
|
PRO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Payment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId String @default("") @map("user_id")
|
||||||
|
// Denormalize to avoid join with the user table
|
||||||
|
email String @default("") @map("email")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
paymentId String @default("") @map("payment_id")
|
||||||
|
customerId String @default("") @map("customer_id")
|
||||||
|
description String @default("")
|
||||||
|
amount Int @default(0)
|
||||||
|
currency String @default("")
|
||||||
|
receipt String @default("")
|
||||||
|
|
||||||
|
@@unique([paymentId])
|
||||||
|
@@index([userId], map: "payment_user_id_idx")
|
||||||
|
@@index([email], map: "payment_email_idx")
|
||||||
|
@@map("payment")
|
||||||
|
}
|
||||||
|
|
||||||
// NextAuth Prisma Schema Begin
|
// NextAuth Prisma Schema Begin
|
||||||
// Below are the auth related prisma schema to support NextAuth.js. In partiular, the email flow
|
// Below are the auth related prisma schema to support NextAuth.js. In partiular, the email flow
|
||||||
// requires this. https://authjs.dev/reference/adapter/prisma
|
// requires this. https://authjs.dev/reference/adapter/prisma
|
||||||
@ -133,6 +141,7 @@ model User {
|
|||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
|
||||||
subscriptions Subscription[]
|
subscriptions Subscription[]
|
||||||
|
payments Payment[]
|
||||||
// The stripe customer id corresponds to this user
|
// The stripe customer id corresponds to this user
|
||||||
stripeId String? @unique @map("stripe_id")
|
stripeId String? @unique @map("stripe_id")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SubscriptionHistoryTable from "./SubscriptionHistoryTable";
|
import PaymentHistoryTable from "./PaymentHistoryTable";
|
||||||
import { getDateString } from "@/utils";
|
import { getDateString } from "@/utils";
|
||||||
|
|
||||||
const AccountView = () => {
|
const AccountView = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
const expired = session?.user?.subscription?.expireAt && session?.user?.subscription?.expireAt < Date.now();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!session && (
|
{!session && (
|
||||||
@ -38,6 +40,11 @@ const AccountView = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex text-base font-semibold tracking-tight items-center">
|
<div className="flex text-base font-semibold tracking-tight items-center">
|
||||||
|
{!!expired && (
|
||||||
|
<span className="mr-2 rounded-full bg-yellow-50 px-3 py-1 font-medium text-yellow-700 ring-1 ring-inset ring-yellow-600/20">
|
||||||
|
{t("setting.plan.expired")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${
|
||||||
session?.user.subscription.plan == "PRO"
|
session?.user.subscription.plan == "PRO"
|
||||||
@ -61,7 +68,7 @@ const AccountView = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SubscriptionHistoryTable />
|
<PaymentHistoryTable />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -2,18 +2,18 @@ import axios from "axios";
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SubscriptionPurchase } from "@/types";
|
import { Payment } from "@/types";
|
||||||
import { getCurrencySymbol, getDateString } from "@/utils";
|
import { getCurrencySymbol, getDateString } from "@/utils";
|
||||||
|
|
||||||
const SubscriptionHistoryTable = () => {
|
const PaymentHistoryTable = () => {
|
||||||
const [list, setList] = useState<SubscriptionPurchase[]>([]);
|
const [list, setList] = useState<Payment[]>([]);
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const refreshSubscriptionList = async (userId: string) => {
|
const refreshPaymentList = async (userId: string) => {
|
||||||
let list: SubscriptionPurchase[] = [];
|
let list: Payment[] = [];
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get("/api/subscription", {
|
const { data } = await axios.get("/api/payment", {
|
||||||
headers: { Authorization: `Bearer ${userId}` },
|
headers: { Authorization: `Bearer ${userId}` },
|
||||||
});
|
});
|
||||||
list = data;
|
list = data;
|
||||||
@ -24,7 +24,7 @@ const SubscriptionHistoryTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (session?.user.id) {
|
if (session?.user.id) {
|
||||||
refreshSubscriptionList(session.user.id);
|
refreshPaymentList(session.user.id);
|
||||||
}
|
}
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
@ -53,18 +53,18 @@ const SubscriptionHistoryTable = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{list.map((subscription) => (
|
{list.map((payment) => (
|
||||||
<tr key={subscription.id}>
|
<tr key={payment.id}>
|
||||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
|
||||||
{getDateString(subscription.createdAt)}
|
{getDateString(payment.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{subscription.description}</td>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{payment.description}</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
{getCurrencySymbol(subscription.currency.toLocaleUpperCase())}
|
{getCurrencySymbol(payment.currency.toLocaleUpperCase())}
|
||||||
{subscription.amount / 100}
|
{payment.amount / 100}
|
||||||
</td>
|
</td>
|
||||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||||
<a href={subscription.receipt} target="_blank" className="text-indigo-600 hover:text-indigo-900">
|
<a href={payment.receipt} target="_blank" className="text-indigo-600 hover:text-indigo-900">
|
||||||
{t("setting.subscription.view-receipt")}
|
{t("setting.subscription.view-receipt")}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@ -79,4 +79,4 @@ const SubscriptionHistoryTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SubscriptionHistoryTable;
|
export default PaymentHistoryTable;
|
@ -1,6 +1,6 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getEndUser } from "./auth/end-user";
|
import { getEndUser } from "./auth/end-user";
|
||||||
import { getSubscriptionListByEmail } from "./utils/subscription";
|
import { getPaymentListByEmail } from "./utils/payment";
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (req.method !== "GET") {
|
if (req.method !== "GET") {
|
||||||
@ -8,9 +8,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const endUser = await getEndUser(req, res);
|
const endUser = await getEndUser(req, res);
|
||||||
const subscriptionList = await getSubscriptionListByEmail(endUser);
|
const paymentList = await getPaymentListByEmail(endUser);
|
||||||
|
|
||||||
res.status(200).json(subscriptionList);
|
res.status(200).json(paymentList);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
25
src/pages/api/utils/payment.ts
Normal file
25
src/pages/api/utils/payment.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Payment } from "@/types";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export const getPaymentListByEmail = async (email: string): Promise<Payment[]> => {
|
||||||
|
const payments = await prisma.payment.findMany({
|
||||||
|
where: { email: email },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: Payment[] = [];
|
||||||
|
for (const payment of payments) {
|
||||||
|
result.push({
|
||||||
|
id: payment.id,
|
||||||
|
email: payment.email,
|
||||||
|
createdAt: payment.createdAt.getTime(),
|
||||||
|
amount: payment.amount,
|
||||||
|
currency: payment.currency,
|
||||||
|
receipt: payment.receipt,
|
||||||
|
description: payment.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { PlanConfig, PlanType, Subscription, SubscriptionPurchase } from "@/types";
|
import { PlanConfig, PlanType, Subscription } from "@/types";
|
||||||
import { PrismaClient, SubscriptionStatus } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
@ -9,65 +9,39 @@ export const getSubscriptionByEmail = async (email: string): Promise<Subscriptio
|
|||||||
orderBy: { expireAt: "desc" },
|
orderBy: { expireAt: "desc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return the latest ACTIVE, not-expired subscription if exists.
|
// Return the latest active subscription if exists.
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
const result = {
|
const result: Subscription = {
|
||||||
|
id: subscription.id,
|
||||||
plan: subscription.plan as PlanType,
|
plan: subscription.plan as PlanType,
|
||||||
quota: PlanConfig[subscription.plan].quota,
|
quota: PlanConfig[subscription.plan].quota,
|
||||||
status: subscription.status,
|
|
||||||
startAt: subscription.startAt.getTime(),
|
startAt: subscription.startAt.getTime(),
|
||||||
expireAt: subscription.expireAt.getTime(),
|
expireAt: subscription.expireAt.getTime(),
|
||||||
|
canceledAt: subscription.canceledAt?.getTime(),
|
||||||
};
|
};
|
||||||
if (subscription.status === SubscriptionStatus.ACTIVE && subscription.expireAt.getTime() > Date.now()) {
|
if (!result.canceledAt && result.expireAt > Date.now()) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the latest ACTIVE, expired subscripion if exists.
|
// Return the latest subscripion if exists.
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
const result = {
|
return {
|
||||||
|
id: subscription.id,
|
||||||
plan: subscription.plan as PlanType,
|
plan: subscription.plan as PlanType,
|
||||||
quota: PlanConfig["FREE"].quota,
|
quota: PlanConfig["FREE"].quota,
|
||||||
status: subscription.status,
|
|
||||||
startAt: subscription.startAt.getTime(),
|
startAt: subscription.startAt.getTime(),
|
||||||
expireAt: subscription.expireAt.getTime(),
|
expireAt: subscription.expireAt.getTime(),
|
||||||
|
canceledAt: subscription.canceledAt?.getTime(),
|
||||||
};
|
};
|
||||||
if (subscription.status === SubscriptionStatus.ACTIVE) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a FREE subscription.
|
// Return a FREE subscription.
|
||||||
return {
|
return {
|
||||||
|
id: "",
|
||||||
plan: "FREE",
|
plan: "FREE",
|
||||||
quota: PlanConfig["FREE"].quota,
|
quota: PlanConfig["FREE"].quota,
|
||||||
status: SubscriptionStatus.ACTIVE,
|
|
||||||
startAt: 0,
|
startAt: 0,
|
||||||
expireAt: 0,
|
expireAt: 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSubscriptionListByEmail = async (email: string): Promise<SubscriptionPurchase[]> => {
|
|
||||||
const subscriptions = await prisma.subscription.findMany({
|
|
||||||
where: { email: email },
|
|
||||||
orderBy: { expireAt: "desc" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the latest ACTIVE, not-expired subscription if exists.
|
|
||||||
const result: SubscriptionPurchase[] = [];
|
|
||||||
for (const subscription of subscriptions) {
|
|
||||||
result.push({
|
|
||||||
id: subscription.id,
|
|
||||||
email: subscription.email,
|
|
||||||
amount: subscription.amount,
|
|
||||||
currency: subscription.currency,
|
|
||||||
receipt: subscription.receipt,
|
|
||||||
plan: subscription.plan as PlanType,
|
|
||||||
description: subscription.description,
|
|
||||||
createdAt: subscription.createdAt.getTime(),
|
|
||||||
startAt: subscription.startAt.getTime(),
|
|
||||||
expireAt: subscription.expireAt.getTime(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
@ -3,6 +3,7 @@ import { buffer } from "micro";
|
|||||||
import Cors from "micro-cors";
|
import Cors from "micro-cors";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
import { getSubscriptionByEmail } from "../utils/subscription";
|
||||||
|
|
||||||
const stripe = new Stripe(process.env.STRIPE_API_KEY, {
|
const stripe = new Stripe(process.env.STRIPE_API_KEY, {
|
||||||
// https://github.com/stripe/stripe-node#configuration
|
// https://github.com/stripe/stripe-node#configuration
|
||||||
@ -63,25 +64,45 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
where: { email: paymentIntent.metadata.email },
|
where: { email: paymentIntent.metadata.email },
|
||||||
});
|
});
|
||||||
|
|
||||||
const today = new Date(new Date().setHours(0, 0, 0, 0));
|
const payment: Prisma.PaymentUncheckedCreateInput = {
|
||||||
// Subtract 1 second from the year from now to make it 23:59:59
|
|
||||||
const yearFromNow = new Date(new Date(new Date().setHours(0, 0, 0, 0)).setFullYear(today.getFullYear() + 1) - 1000);
|
|
||||||
const subscription: Prisma.SubscriptionUncheckedCreateInput = {
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
email: paymentIntent.metadata.email,
|
email: paymentIntent.metadata.email,
|
||||||
createdAt: new Date(paymentIntent.created * 1000),
|
createdAt: new Date(paymentIntent.created * 1000),
|
||||||
status: "ACTIVE",
|
|
||||||
startAt: today,
|
|
||||||
expireAt: yearFromNow,
|
|
||||||
paymentId: paymentIntent.id,
|
paymentId: paymentIntent.id,
|
||||||
customerId: customerId || "",
|
customerId: customerId || "",
|
||||||
plan: paymentIntent.metadata.plan as SubscriptionPlan,
|
|
||||||
description: paymentIntent.metadata.description,
|
description: paymentIntent.metadata.description,
|
||||||
amount: paymentIntent.amount,
|
amount: paymentIntent.amount,
|
||||||
currency: paymentIntent.currency,
|
currency: paymentIntent.currency,
|
||||||
receipt: charge.receipt_url as string,
|
receipt: charge.receipt_url as string,
|
||||||
};
|
};
|
||||||
await prisma.subscription.create({ data: subscription });
|
await prisma.payment.create({ data: payment });
|
||||||
|
|
||||||
|
const currentSubscription = await getSubscriptionByEmail(paymentIntent.metadata.email);
|
||||||
|
// Create a new subscription if there is no active paid subscription.
|
||||||
|
if (currentSubscription.plan === "FREE" || currentSubscription.canceledAt || currentSubscription.expireAt < new Date().getTime()) {
|
||||||
|
const today = new Date(new Date().setHours(0, 0, 0, 0));
|
||||||
|
// Subtract 1 second from the year from now to make it 23:59:59
|
||||||
|
const yearFromNow = new Date(new Date(new Date().setHours(0, 0, 0, 0)).setFullYear(today.getFullYear() + 1) - 1000);
|
||||||
|
const subscription: Prisma.SubscriptionUncheckedCreateInput = {
|
||||||
|
userId: user.id,
|
||||||
|
email: paymentIntent.metadata.email,
|
||||||
|
createdAt: new Date(paymentIntent.created * 1000),
|
||||||
|
startAt: today,
|
||||||
|
expireAt: yearFromNow,
|
||||||
|
plan: paymentIntent.metadata.plan as SubscriptionPlan,
|
||||||
|
};
|
||||||
|
await prisma.subscription.create({ data: subscription });
|
||||||
|
} else {
|
||||||
|
// Extend the current subscription if there is an active paid subscription.
|
||||||
|
const expireAt = new Date(Math.max(currentSubscription.expireAt, new Date().getTime()));
|
||||||
|
expireAt.setFullYear(expireAt.getFullYear() + 1);
|
||||||
|
await prisma.subscription.update({
|
||||||
|
where: { id: currentSubscription.id },
|
||||||
|
data: {
|
||||||
|
expireAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (event.type === "payment_intent.payment_failed") {
|
} else if (event.type === "payment_intent.payment_failed") {
|
||||||
const paymentIntent = event.data.object as Stripe.PaymentIntent;
|
const paymentIntent = event.data.object as Stripe.PaymentIntent;
|
||||||
console.log(`❌ Payment failed: ${paymentIntent.last_payment_error?.message}`);
|
console.log(`❌ Payment failed: ${paymentIntent.last_payment_error?.message}`);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SubscriptionPlan, SubscriptionStatus } from "@prisma/client";
|
import { SubscriptionPlan } from "@prisma/client";
|
||||||
|
|
||||||
export type PlanType = SubscriptionPlan | "GUEST" | "FREE";
|
export type PlanType = SubscriptionPlan | "GUEST" | "FREE";
|
||||||
|
|
||||||
@ -20,22 +20,20 @@ export const PlanConfig: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface Subscription {
|
export interface Subscription {
|
||||||
|
id: string;
|
||||||
plan: PlanType;
|
plan: PlanType;
|
||||||
quota: number;
|
quota: number;
|
||||||
status: SubscriptionStatus;
|
|
||||||
startAt: number;
|
startAt: number;
|
||||||
expireAt: number;
|
expireAt: number;
|
||||||
|
canceledAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubscriptionPurchase {
|
export interface Payment {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
createdAt: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
receipt: string;
|
receipt: string;
|
||||||
plan: PlanType;
|
|
||||||
description: string;
|
description: string;
|
||||||
createdAt: number;
|
|
||||||
startAt: number;
|
|
||||||
expireAt: number;
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user