Skip to content

Commit

Permalink
feat: added configuration flag to enable Stripe integration (#669)
Browse files Browse the repository at this point in the history
Close #545
  • Loading branch information
flohoch committed Dec 11, 2023
1 parent 43c2aca commit f69e575
Show file tree
Hide file tree
Showing 17 changed files with 381 additions and 252 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ SMTP_FROM_EMAIL_ADDRESS="flo@kula.app"
SMTP_FROM_NAME="Flo Ho"

# add your stripe API key here
NEXT_PUBLIC_STRIPE_ENABLED=false
STRIPE_ENABLED=false
STRIPE_WEBHOOK_SECRET=
STRIPE_SECRET_KEY=
STRIPE_USE_AUTOMATIC_TAX=
Expand Down
1 change: 1 addition & 0 deletions config/interfaces/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { UsageReportConfig } from "./UsageReportConfig";
export interface Config {
client: {
sentryConfig: SentryConfig;
stripeConfig: StripeConfig;
};
server: {
database: DatabaseConfig;
Expand Down
9 changes: 5 additions & 4 deletions config/interfaces/StripeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export interface StripeConfig {
apiVersion: string;
webhookSecret: string;
secretKey: string;
useAutomaticTax: boolean;
isEnabled: boolean;
apiVersion?: string;
webhookSecret?: string;
secretKey?: string;
useAutomaticTax?: boolean;
dynamicTaxRates?: Array<string>;
taxRates?: Array<string>;
}
7 changes: 7 additions & 0 deletions config/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export function loadConfig(): Config {
env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE
) ?? 0.2,
},
stripeConfig: {
isEnabled:
parseBooleanEnvValue(env.STRIPE_ENABLED) ??
parseBooleanEnvValue(env.NEXT_PUBLIC_STRIPE_ENABLED) ??
false,
},
},
server: {
nextAuth: {
Expand Down Expand Up @@ -107,6 +113,7 @@ export function loadConfig(): Config {
sentinelPassword: env.REDIS_SENTINEL_PASSWORD,
},
stripeConfig: {
isEnabled: parseBooleanEnvValue(env.STRIPE_ENABLED) ?? false,
apiVersion: env.STRIPE_API_VERSION || "",
webhookSecret: env.STRIPE_WEBHOOK_SECRET || "",
secretKey: env.STRIPE_SECRET_KEY || "",
Expand Down
22 changes: 15 additions & 7 deletions pages/api/frontend/v0.1/stripe/checkoutSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@ export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const stripeConfig = loadConfig().server.stripeConfig;
const logger = new Logger(__filename);
const stripeConfig = loadConfig().server.stripeConfig;

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

const userInOrg = await getUserWithRoleFromRequest(req, res);

Expand All @@ -30,6 +37,9 @@ export default async function handler(

switch (req.method) {
case "POST":
if (!stripeConfig.secretKey) {
throw new Error("Stripe secret key is not configured");
}
const stripe = new Stripe(stripeConfig.secretKey, {
apiVersion: "2023-08-16",
});
Expand All @@ -44,11 +54,9 @@ export default async function handler(

if (!org) {
logger.error(`No organisation found with id ${userInOrg.orgId}`);
return res
.status(StatusCodes.NOT_FOUND)
.json({
message: `No organisation found with id ${userInOrg.orgId}`,
});
return res.status(StatusCodes.NOT_FOUND).json({
message: `No organisation found with id ${userInOrg.orgId}`,
});
}

if (!req.body.products || !Array.isArray(req.body.products)) {
Expand Down Expand Up @@ -96,7 +104,7 @@ export default async function handler(
let sessionOptions: Stripe.Checkout.SessionCreateParams = {
allow_promotion_codes: true,
automatic_tax: {
enabled: stripeConfig.useAutomaticTax,
enabled: !!stripeConfig.useAutomaticTax,
},
billing_address_collection: "required",
client_reference_id: req.body.orgId,
Expand Down
13 changes: 12 additions & 1 deletion pages/api/frontend/v0.1/stripe/customerPortal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export default async function handler(
const config = loadConfig();
const logger = new Logger(__filename);

if (!config.server.stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

const user = await getUserWithRoleFromRequest(req, res);

if (!user) {
Expand All @@ -31,7 +38,11 @@ export default async function handler(

switch (req.method) {
case "POST":
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
const stripeConfig = config.server.stripeConfig;
if (!stripeConfig.secretKey) {
throw new Error("Stripe secret key is not configured");
}
const stripe = new Stripe(stripeConfig.secretKey, {
apiVersion: "2023-08-16",
});

Expand Down
17 changes: 16 additions & 1 deletion pages/api/frontend/v0.1/stripe/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ const logger = new Logger(__filename);

export async function getProducts(): Promise<Product[]> {
const config = loadConfig();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {

const stripeConfig = loadConfig().server.stripeConfig;
if (!stripeConfig.secretKey) {
throw new Error("Stripe secret key is not configured");
}
const stripe = new Stripe(stripeConfig.secretKey, {
apiVersion: "2023-08-16",
});

const freeProduct: Product = {
id: "FREE",
description: "For checking it out",
Expand Down Expand Up @@ -150,6 +156,15 @@ export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const stripeConfig = loadConfig().server.stripeConfig;

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

switch (req.method) {
case "GET":
try {
Expand Down
9 changes: 9 additions & 0 deletions pages/api/frontend/v0.1/stripe/subscriptions/[orgId].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { StatusCodes } from "http-status-codes";
import type { NextApiRequest, NextApiResponse } from "next";
import { loadConfig } from "../../../../../../config/loadConfig";
import prisma from "../../../../../../lib/services/db";
import { getUserWithRoleFromRequest } from "../../../../../../util/auth";
import { Logger } from "../../../../../../util/logger";
Expand All @@ -9,6 +10,14 @@ export default async function handler(
res: NextApiResponse
) {
const logger = new Logger(__filename);
const stripeConfig = loadConfig().server.stripeConfig;

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

const userInOrg = await getUserWithRoleFromRequest(req, res);

Expand Down
10 changes: 9 additions & 1 deletion pages/api/usageReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const usageReportingConfig = loadConfig().server.usageReport;
const logger = new Logger(__filename);
const usageReportingConfig = loadConfig().server.usageReport;
const stripeConfig = loadConfig().server.stripeConfig;

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

if (
req.headers.authorization !== `Bearer ${usageReportingConfig.apiKey}` &&
Expand Down
148 changes: 75 additions & 73 deletions pages/api/v0.1/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,97 +128,99 @@ export default async function handler(
}

// Start of quota limitation
try {
const products = await getProducts();
if (config.server.stripeConfig.isEnabled) {
try {
const products = await getProducts();

// Check if there is a subItem with isMetered set to true
// Metered subItems do not have a limit
let hasMeteredSubItem = false;
// There should be 0 or 1 sub
let subFromDb = app?.organisation?.subs[0];
// Check if there is a subItem with isMetered set to true
// Metered subItems do not have a limit
let hasMeteredSubItem = false;
// There should be 0 or 1 sub
let subFromDb = app?.organisation?.subs[0];

if (app?.organisation?.subs) {
for (const sub of app.organisation.subs) {
if (sub.subItems?.some((subItem) => subItem.metered === true)) {
hasMeteredSubItem = true;
break;
if (app?.organisation?.subs) {
for (const sub of app.organisation.subs) {
if (sub.subItems?.some((subItem) => subItem.metered === true)) {
hasMeteredSubItem = true;
break;
}
}
}
}

// If not metered, check for the limit
if (!hasMeteredSubItem) {
let countingStartDate = new Date();
// If not metered, check for the limit
if (!hasMeteredSubItem) {
let countingStartDate = new Date();

// Free version counts back plainly one month
if (!subFromDb) {
countingStartDate.setMonth(countingStartDate.getMonth() - 1);
} else {
// use current period start of active subscription
countingStartDate = subFromDb.currentPeriodStart;
}
// Free version counts back plainly one month
if (!subFromDb) {
countingStartDate.setMonth(countingStartDate.getMonth() - 1);
} else {
// use current period start of active subscription
countingStartDate = subFromDb.currentPeriodStart;
}

// Prepare array of app ids of organisation
const appIds = app?.organisation?.apps?.map((app) => app.id) || [];
// Prepare array of app ids of organisation
const appIds = app?.organisation?.apps?.map((app) => app.id) || [];

// Count requests across all apps of the org
const requestCount = await prisma.loggedApiRequests.count({
where: {
appId: {
in: appIds,
},
createdAt: {
gte: countingStartDate,
// Count requests across all apps of the org
const requestCount = await prisma.loggedApiRequests.count({
where: {
appId: {
in: appIds,
},
createdAt: {
gte: countingStartDate,
},
},
},
});
logger.log(
`Request count for org with id '${app.orgId}' is ${requestCount}`
);

let isLimitReached = false;

// Check whether quota/limit for the request has been met (active subscription)
if (subFromDb) {
const targetProduct = products.find(
(product: { id: string | undefined }) =>
product.id === subFromDb?.subItems[0].productId
});
logger.log(
`Request count for org with id '${app.orgId}' is ${requestCount}`
);

if (!targetProduct) {
logger.error(
`No product found for org with id '${app.orgId}' and active sub with id '${subFromDb.subId}'`
let isLimitReached = false;

// Check whether quota/limit for the request has been met (active subscription)
if (subFromDb) {
const targetProduct = products.find(
(product: { id: string | undefined }) =>
product.id === subFromDb?.subItems[0].productId
);
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ message: "Please try again later" });
}

logger.log(
`Request limit for org with id '${app.orgId}' is ${targetProduct.requests}`
);
if (requestCount >= Number(targetProduct.requests)) {
if (!targetProduct) {
logger.error(
`No product found for org with id '${app.orgId}' and active sub with id '${subFromDb.subId}'`
);
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ message: "Please try again later" });
}

logger.log(
`Request limit for org with id '${app.orgId}' is ${targetProduct.requests}`
);
if (requestCount >= Number(targetProduct.requests)) {
isLimitReached = true;
}
} else if (!subFromDb && requestCount >= FREE_SUB_REQUEST_LIMIT) {
// Check quota/limit for free version
isLimitReached = true;
}
} else if (!subFromDb && requestCount >= FREE_SUB_REQUEST_LIMIT) {
// Check quota/limit for free version
isLimitReached = true;
}

// Return error if limit has been reached and the request cannot be served
if (isLimitReached) {
logger.log(
`The limit has been currently reached for org with id '${app?.orgId}'`
);
return res.status(StatusCodes.PAYMENT_REQUIRED).json({
message: "The limit for the current abo has been reached.",
});
// Return error if limit has been reached and the request cannot be served
if (isLimitReached) {
logger.log(
`The limit has been currently reached for org with id '${app?.orgId}'`
);
return res.status(StatusCodes.PAYMENT_REQUIRED).json({
message: "The limit for the current abo has been reached.",
});
}
}
} catch (error: any) {
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ message: error.message });
}
} catch (error: any) {
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ message: error.message });
}

logger.log(`Looking up all messages for app with id '${app.id}'`);
Expand Down
10 changes: 10 additions & 0 deletions pages/api/webhooks/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,20 @@ export default async function handler(
const logger = new Logger(__filename);

const stripeConfig = loadConfig().server.stripeConfig;
if (!stripeConfig.secretKey) {
throw new Error("Stripe secret key is not configured");
}
const stripe = new Stripe(stripeConfig.secretKey, {
apiVersion: "2023-08-16",
});

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

switch (req.method) {
case "POST":
const buf = await buffer(req);
Expand Down
Loading

0 comments on commit f69e575

Please sign in to comment.