Skip to content

Commit

Permalink
Stripe Single Payment Checkout Session (#46)
Browse files Browse the repository at this point in the history
* creating checkout session endpoint

* update checkout endpoint

* remove subscription stuff from single payment enpoint

* donation message and product data

* removed comments

* removing migration.sql

* removed update to types.ts

* removed comments, migration.sql and the update to types.ts

* Add swagger docs

* Update endpoint description

* yarn linter fixes

* remove unused imports

* added stripeValidator to handle 400 errors (ex. invalid inputs)

* move url to environment variable

* remove comments, logs, fix typo

---------

Co-authored-by: Jason Le <[email protected]>
  • Loading branch information
andy-liuu and Jason Le authored Jun 26, 2024
1 parent aa4df60 commit 520c97a
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 7 deletions.
20 changes: 20 additions & 0 deletions backend/typescript/middlewares/validators/stripeValidators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Request, Response, NextFunction } from "express";
import { getApiValidationError, validatePrimitive } from "./util";

export const createCheckoutSessionValidator = async (
req: Request,
res: Response,
next: NextFunction,
) => {
if (!validatePrimitive(req.body.user_id, "string")) {
return res.status(400).send(getApiValidationError("user_id", "string"));
}
if (!validatePrimitive(req.body.amount, "integer")) {
return res.status(400).send(getApiValidationError("amount", "integer"));
}
if (!validatePrimitive(req.body.cause_id, "integer")) {
return res.status(400).send(getApiValidationError("cause_id", "integer"));
}

return next();
};
1 change: 1 addition & 0 deletions backend/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"reflect-metadata": "^0.1.13",
"sequelize": "^6.5.0",
"sequelize-typescript": "^2.1.0",
"stripe": "^15.9.0",
"swagger-ui-express": "^4.1.6",
"ts-node": "^10.0.0",
"umzug": "^3.0.0-beta.16",
Expand Down
32 changes: 32 additions & 0 deletions backend/typescript/rest/stripeRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Request, Response, Router } from "express";

import StripeService from "../services/implementations/stripeService";
import IStripeService from "../services/interfaces/stripeService";
import { createCheckoutSessionValidator } from "../middlewares/validators/stripeValidators";

const stripeService: IStripeService = new StripeService();

const stripeRouter = Router();

// Endpoint to create a Stripe Checkout session
stripeRouter.post(
"/create-checkout-session",
createCheckoutSessionValidator,
async (req: Request, res: Response) => {
try {
const { userId, amount, causeId } = req.body;
const sessionUrl = await stripeService.createCheckoutSession(
userId,
amount,
causeId,
);
res.json({ url: sessionUrl });
} catch (error) {
res
.status(500)
.json({ error: "Error creating payment checkout session." });
}
},
);

export default stripeRouter;
2 changes: 2 additions & 0 deletions backend/typescript/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import entityRouter from "./rest/entityRoutes";
import simpleEntityRouter from "./rest/simpleEntityRoutes";
import userRouter from "./rest/userRoutes";
import donationRouter from "./rest/donationsRoutes";
import stripeRouter from "./rest/stripeRoute";

const CORS_ALLOW_LIST = [
"http://localhost:3000",
Expand Down Expand Up @@ -38,6 +39,7 @@ app.use("/simple-entities", simpleEntityRouter);
app.use("/users", userRouter);
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.use("/donations", donationRouter);
app.use("/stripe", stripeRouter);

// Health check
app.get("/test", async (req: any, res: any) => {
Expand Down
59 changes: 59 additions & 0 deletions backend/typescript/services/implementations/stripeService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import dotenv from "dotenv";

import Stripe from "stripe";
import IStripeService from "../interfaces/stripeService";
import logger from "../../utilities/logger";

dotenv.config();

const stripe = new Stripe(process.env.STRIPE_PRIVATE_KEY as string);
const Logger = logger(__filename);

// Stripe checkout will redirect to these frontend urls upon successful payment or cancel.
const SUCCESS_URL = `${process.env.FRONTEND_URL}/checkout-success`;
const CANCEL_URL = `${process.env.FRONTEND_URL}/checkout-cancel`;

const checkoutSessionDefaultOptions: Stripe.Checkout.SessionCreateParams = {
payment_method_types: ["card"],
mode: "payment",
success_url: SUCCESS_URL,
cancel_url: CANCEL_URL,
};

class StripeService implements IStripeService {
createCheckoutSession = async (
user_id: string,
amount: number, // in cents (euro)
cause_id: number,
): Promise<string> => {
try {
const session = await stripe.checkout.sessions.create({
...checkoutSessionDefaultOptions,
line_items: [
{
price_data: {
currency: "EUR",
unit_amount: amount,
product_data: {
name: `Donation to cause ${cause_id}`,
},
},
quantity: 1,
},
],
});

if (!session.url) {
throw new Error("Session URL is null");
}

return session.url;
} catch (error) {
Logger.error(
`Error creating a checkout session for a payment for user ${user_id} = ${error}`,
);
throw error;
}
};
}
export default StripeService;
2 changes: 1 addition & 1 deletion backend/typescript/services/interfaces/donationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface IDonationService {
user_id: string,
amount: number,
cause_id: number,
is_recurring: string,
is_recurring: Recurrence,
confirmation_email_sent: boolean,
): Promise<DonationDTO>;
}
Expand Down
17 changes: 17 additions & 0 deletions backend/typescript/services/interfaces/stripeService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface IStripeService {
/**
* Create a checkout session for a one time payemnt
* @param user_id user's id
* @param amount the amount the user is donating toward this cause in the donation
* @param cause_id the id of the cause the donation is associated with
* @returns the newly created session url
* @throws Error if
*/
createCheckoutSession(
user_id: string,
amount: number,
cause_id: number,
): Promise<string>;
}

export default IStripeService;
40 changes: 39 additions & 1 deletion backend/typescript/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -951,4 +951,42 @@ paths:
'401':
description: Unauthorized
'500':
description: Invalid request
description: Invalid request
/stripe/create-checkout-session:
post:
security:
- bearerAuth: []
tags:
- Stripe
description: Create a new single payment Stripe checkout session
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- user_id, amount, cause_id
properties:
user_id:
type: string
amount:
type: integer
cause_id:
type: number
responses:
'200':
description: Successfully created a checkout session
content:
application/json:
schema:
type: object
required:
- url
properties:
url:
type: string
'500':
description: Error creating payment checkout session.


32 changes: 27 additions & 5 deletions backend/typescript/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1375,10 +1375,10 @@
resolved "https://registry.yarnpkg.com/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6"
integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==

"@prisma/client@^5.10.2":
version "5.11.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.11.0.tgz#d8e55fab85163415b2245fb408b9106f83c8106d"
integrity sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==
"@prisma/client@^5.11.0":
version "5.14.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.14.0.tgz#dadca5bb1137ddcebb454bbdaf89423823d3363f"
integrity sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg==

"@prisma/[email protected]":
version "5.11.0"
Expand Down Expand Up @@ -2256,6 +2256,13 @@
dependencies:
undici-types "~5.26.4"

"@types/node@>=8.1.0":
version "20.14.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.0.tgz#49ceec7b34f8621470cff44677fa9d461a477f17"
integrity sha512-5cHBxFGJx6L4s56Bubp4fglrEpmyJypsqI6RgzMfBHWUJQGWAAi8cWcgetEbZXHYXo9C2Fa4EEds/uSyS4cxmA==
dependencies:
undici-types "~5.26.4"

"@types/node@^10.1.0":
version "10.17.60"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
Expand Down Expand Up @@ -7600,6 +7607,13 @@ [email protected]:
dependencies:
side-channel "^1.0.4"

qs@^6.11.0:
version "6.12.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a"
integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==
dependencies:
side-channel "^1.0.6"

qs@~6.5.2:
version "6.5.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
Expand Down Expand Up @@ -8020,7 +8034,7 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==

side-channel@^1.0.4:
side-channel@^1.0.4, side-channel@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
Expand Down Expand Up @@ -8306,6 +8320,14 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==

stripe@^15.9.0:
version "15.9.0"
resolved "https://registry.yarnpkg.com/stripe/-/stripe-15.9.0.tgz#fbe6434496afa27cb08be7dc730064a719e7c925"
integrity sha512-C7NAK17wGr6DOybxThO0n1zVcqSShWno7kx/UcJorQ/nWZ5KnfHQ38DUTb9NgizC8TCII3BPIhqq6Zc/1aUkrg==
dependencies:
"@types/node" ">=8.1.0"
qs "^6.11.0"

strnum@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
Expand Down

0 comments on commit 520c97a

Please sign in to comment.