downgrade users when trial ends (#767)
* wip * wip * wip * wtf * should be all the logic * comment * fix receiver name * safeguard a bit more * downgrade users cron job Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>pull/568/head
parent
0372289fe6
commit
a4fbe7b2b4
|
@ -4,6 +4,7 @@
|
||||||
"esbenp.prettier-vscode", // prettier plugin
|
"esbenp.prettier-vscode", // prettier plugin
|
||||||
"dbaeumer.vscode-eslint", // eslint plugin
|
"dbaeumer.vscode-eslint", // eslint plugin
|
||||||
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
|
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
|
||||||
"heybourn.headwind" // automatically sort tailwind classes in predictable order, kinda like "prettier for tailwind"
|
"heybourn.headwind", // automatically sort tailwind classes in predictable order, kinda like "prettier for tailwind",
|
||||||
|
"stripe.vscode-stripe" // stripe VSCode extension
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,10 @@ import Stripe from "stripe";
|
||||||
import stripe from "@ee/lib/stripe/server";
|
import stripe from "@ee/lib/stripe/server";
|
||||||
|
|
||||||
import { CalendarEvent } from "@lib/calendarClient";
|
import { CalendarEvent } from "@lib/calendarClient";
|
||||||
|
import { HttpError } from "@lib/core/http/error";
|
||||||
import EventManager from "@lib/events/EventManager";
|
import EventManager from "@lib/events/EventManager";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
bodyParser: false,
|
bodyParser: false,
|
||||||
|
@ -98,29 +97,55 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebhookHandler = (event: Stripe.Event) => Promise<void>;
|
||||||
|
|
||||||
|
const webhookHandlers: Record<string, WebhookHandler | undefined> = {
|
||||||
|
"payment_intent.succeeded": handlePaymentSuccess,
|
||||||
|
"customer.subscription.deleted": async (event) => {
|
||||||
|
const data = event.data as Stripe.Subscription;
|
||||||
|
|
||||||
|
const customerId = typeof data.customer === "string" ? data.customer : data.customer.id;
|
||||||
|
|
||||||
|
const customer = (await stripe.customers.retrieve(customerId)) as Stripe.Customer;
|
||||||
|
if (typeof customer.email !== "string") {
|
||||||
|
throw new Error(`Couldn't find customer email for ${data.customer}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
email: customer.email,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
plan: "FREE",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const requestBuffer = await buffer(req);
|
|
||||||
const sig = req.headers["stripe-signature"];
|
|
||||||
let event;
|
|
||||||
|
|
||||||
if (!sig) {
|
|
||||||
res.status(400).send(`Webhook Error: missing Stripe signature`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!webhookSecret) {
|
|
||||||
res.status(400).send(`Webhook Error: missing Stripe webhookSecret`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
event = stripe.webhooks.constructEvent(requestBuffer.toString(), sig, webhookSecret);
|
if (req.method !== "POST") {
|
||||||
|
throw new HttpError({ statusCode: 405, message: "Method Not Allowed" });
|
||||||
|
}
|
||||||
|
const sig = req.headers["stripe-signature"];
|
||||||
|
if (!sig) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: "Missing stripe-signature" });
|
||||||
|
}
|
||||||
|
|
||||||
// Handle the event
|
if (!process.env.STRIPE_WEBHOOK_SECRET) {
|
||||||
if (event.type === "payment_intent.succeeded") {
|
throw new HttpError({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" });
|
||||||
await handlePaymentSuccess(event);
|
}
|
||||||
|
const requestBuffer = await buffer(req);
|
||||||
|
const payload = requestBuffer.toString();
|
||||||
|
// console.log("payload", payload);
|
||||||
|
|
||||||
|
const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET);
|
||||||
|
|
||||||
|
const handler = webhookHandlers[event.type];
|
||||||
|
if (handler) {
|
||||||
|
await handler(event);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Unhandled event type ${event.type}`);
|
console.warn(`Unhandled Stripe Webhook event type ${event.type}`);
|
||||||
}
|
}
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
const err = getErrorFromUnknown(_err);
|
const err = getErrorFromUnknown(_err);
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/lodash.merge": "^4.6.6",
|
"@types/lodash.merge": "^4.6.6",
|
||||||
|
"@types/micro": "^7.3.6",
|
||||||
"@types/node": "^16.6.1",
|
"@types/node": "^16.6.1",
|
||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/qrcode": "^1.4.1",
|
"@types/qrcode": "^1.4.1",
|
||||||
|
|
|
@ -25,7 +25,7 @@ type AugmentedNextPageContext = Omit<NextPageContext, "err"> & {
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["[error]"] });
|
const log = logger.getChildLogger({ prefix: ["[error]"] });
|
||||||
|
|
||||||
export function getErrorFromUnknown(cause: unknown): Error {
|
export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: number } {
|
||||||
if (cause instanceof Error) {
|
if (cause instanceof Error) {
|
||||||
return cause;
|
return cause;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
const TRIAL_LIMIT_DAYS = 14;
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const apiKey = req.query.apiKey;
|
||||||
|
if (process.env.CRON_API_KEY !== apiKey) {
|
||||||
|
return res.status(401).json({ message: "Not authenticated" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.updateMany({
|
||||||
|
data: {
|
||||||
|
plan: "FREE",
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
plan: "TRIAL",
|
||||||
|
createdDate: {
|
||||||
|
lt: dayjs().subtract(TRIAL_LIMIT_DAYS, "day").toDate(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.json({ ok: true });
|
||||||
|
}
|
55
yarn.lock
55
yarn.lock
|
@ -1371,6 +1371,18 @@
|
||||||
version "2.4.2"
|
version "2.4.2"
|
||||||
resolved "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz"
|
resolved "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz"
|
||||||
|
|
||||||
|
"@types/component-emitter@^1.2.10":
|
||||||
|
version "1.2.10"
|
||||||
|
resolved "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea"
|
||||||
|
integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==
|
||||||
|
|
||||||
|
"@types/engine.io@*":
|
||||||
|
version "3.1.7"
|
||||||
|
resolved "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.7.tgz#86e541a5dc52fb7e97735383564a6ae4cfe2e8f5"
|
||||||
|
integrity sha512-qNjVXcrp+1sS8YpRUa714r0pgzOwESdW5UjHL7D/2ZFdBX0BXUXtg1LUrp+ylvqbvMcMWUy73YpRoxPN2VoKAQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/graceful-fs@^4.1.2":
|
"@types/graceful-fs@^4.1.2":
|
||||||
version "4.1.5"
|
version "4.1.5"
|
||||||
resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz"
|
resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz"
|
||||||
|
@ -1437,6 +1449,14 @@
|
||||||
version "4.14.168"
|
version "4.14.168"
|
||||||
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz"
|
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz"
|
||||||
|
|
||||||
|
"@types/micro@^7.3.6":
|
||||||
|
version "7.3.6"
|
||||||
|
resolved "https://registry.npmjs.org/@types/micro/-/micro-7.3.6.tgz#7d68eb5a780ac4761e3b80687b4ee7328ebc3f2e"
|
||||||
|
integrity sha512-rZHvZ3+Ev3cGJJSy/wtSiXZmafU8guI07PHXf4ku9sQLfDuFALHMCiV+LuH4VOaeMMMnRs8nqxU392gxfn661g==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
"@types/socket.io" "2.1.13"
|
||||||
|
|
||||||
"@types/node@*", "@types/node@>=8.1.0", "@types/node@^16.6.1":
|
"@types/node@*", "@types/node@>=8.1.0", "@types/node@^16.6.1":
|
||||||
version "16.9.6"
|
version "16.9.6"
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz"
|
||||||
|
@ -1517,6 +1537,22 @@
|
||||||
version "2.3.3"
|
version "2.3.3"
|
||||||
resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz"
|
resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz"
|
||||||
|
|
||||||
|
"@types/socket.io-parser@*":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/@types/socket.io-parser/-/socket.io-parser-3.0.0.tgz#9726d3ab9235757a0a30dd5ccf8975dce54e5e2c"
|
||||||
|
integrity sha512-Ry/rbTE6HQNL9eu3LpL1Ocup5VexXu1bSSGlSho/IR5LuRc8YvxwSNJ3JxqTltVJEATLbZkMQETSbxfKNgp4Ew==
|
||||||
|
dependencies:
|
||||||
|
socket.io-parser "*"
|
||||||
|
|
||||||
|
"@types/socket.io@2.1.13":
|
||||||
|
version "2.1.13"
|
||||||
|
resolved "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.13.tgz#b6d694234e99956c96ff99e197eda824b6f9dc48"
|
||||||
|
integrity sha512-JRgH3nCgsWel4OPANkhH8TelpXvacAJ9VeryjuqCDiaVDMpLysd6sbt0dr6Z15pqH3p2YpOT3T1C5vQ+O/7uyg==
|
||||||
|
dependencies:
|
||||||
|
"@types/engine.io" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
"@types/socket.io-parser" "*"
|
||||||
|
|
||||||
"@types/stack-utils@^2.0.0":
|
"@types/stack-utils@^2.0.0":
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz"
|
||||||
|
@ -2467,6 +2503,11 @@ commondir@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz"
|
||||||
|
|
||||||
|
component-emitter@~1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
|
||||||
|
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
||||||
|
@ -2707,7 +2748,7 @@ debug@2:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2:
|
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@~4.3.1:
|
||||||
version "4.3.2"
|
version "4.3.2"
|
||||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz"
|
resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5014,7 +5055,8 @@ merge2@^1.3.0:
|
||||||
|
|
||||||
micro@^9.3.4:
|
micro@^9.3.4:
|
||||||
version "9.3.4"
|
version "9.3.4"
|
||||||
resolved "https://registry.npmjs.org/micro/-/micro-9.3.4.tgz"
|
resolved "https://registry.npmjs.org/micro/-/micro-9.3.4.tgz#745a494e53c8916f64fb6a729f8cbf2a506b35ad"
|
||||||
|
integrity sha512-smz9naZwTG7qaFnEZ2vn248YZq9XR+XoOH3auieZbkhDL4xLOxiE+KqG8qqnBeKfXA9c1uEFGCxPN1D+nT6N7w==
|
||||||
dependencies:
|
dependencies:
|
||||||
arg "4.1.0"
|
arg "4.1.0"
|
||||||
content-type "1.0.4"
|
content-type "1.0.4"
|
||||||
|
@ -6392,6 +6434,15 @@ slice-ansi@^4.0.0:
|
||||||
astral-regex "^2.0.0"
|
astral-regex "^2.0.0"
|
||||||
is-fullwidth-code-point "^3.0.0"
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
|
||||||
|
socket.io-parser@*:
|
||||||
|
version "4.0.4"
|
||||||
|
resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
|
||||||
|
integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
|
||||||
|
dependencies:
|
||||||
|
"@types/component-emitter" "^1.2.10"
|
||||||
|
component-emitter "~1.3.0"
|
||||||
|
debug "~4.3.1"
|
||||||
|
|
||||||
source-map-js@^0.6.2:
|
source-map-js@^0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz"
|
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz"
|
||||||
|
|
Loading…
Reference in New Issue