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
Alex Johansson 2021-09-28 14:00:19 +01:00 committed by GitHub
parent 0372289fe6
commit a4fbe7b2b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 129 additions and 25 deletions

View File

@ -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
] ]
} }

View File

@ -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);

View File

@ -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",

View File

@ -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;
} }

View File

@ -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 });
}

View File

@ -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"