diff --git a/backend/typescript/package.json b/backend/typescript/package.json index 95fbdfa..16dec87 100644 --- a/backend/typescript/package.json +++ b/backend/typescript/package.json @@ -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", diff --git a/backend/typescript/rest/stripeRoute.ts b/backend/typescript/rest/stripeRoute.ts new file mode 100644 index 0000000..d79ebc4 --- /dev/null +++ b/backend/typescript/rest/stripeRoute.ts @@ -0,0 +1,73 @@ +import express, { Request, Response } from "express"; +import { Router } from "express"; +import StripeService from "../services/implementations/stripeService"; +import IStripeService from "../services/interfaces/stripeService"; + +const stripeService: IStripeService = new StripeService(); + +const stripeRouter = Router(); + +//const app = express(); +//app.use(express.json()); + +// Endpoint to create a Stripe Checkout session +stripeRouter.post("/create-checkout-session-payment",async (req: Request, res: Response) => { + try { + const {user_id, amount, cause_id} = req.body; + const sessionUrl = await stripeService.createCheckoutSessionPayment(user_id, amount, cause_id); + console.log(`Created checkout session`); + res.json({ url: sessionUrl }); + } catch (error) { + res.status(500).json({ error: "Error creating payment checkout session." }); + } + }, +); + +/* +stripeRouter.post("/create-checkout-session-subscription", async (req, res) => { + try { + const prices = await stripe.prices.list({ + lookup_keys: [req.body.lookup_key], + expand: ["data.product"], + }); + const session = await stripe.checkout.sessions.create({ + billing_address_collection: "auto", + line_items: [ + { + price: prices.data[0].id, + // For metered billing, do not pass quantity + quantity: 1, + }, + ], + mode: "subscription", + success_url: `http://localhost:5173/checkout-success`, + cancel_url: `http://localhost:5173/checkout-cancel`, + }); + res.send({url: session.url}); + //res.redirect(303, session.url); + } catch (error) { + res + .status(500) + .json({ error: "Error creating subscription checkout session." }); + } +}); + +stripeRouter.post("/create-portal-session", async (req, res) => { + // For demonstration purposes, we're using the Checkout session to retrieve the customer ID. + // Typically this is stored alongside the authenticated user in your database. + const { session_id } = req.body; + const checkoutSession = await stripe.checkout.sessions.retrieve(session_id); + + // This is the url to which the customer will be redirected when they are done + // managing their billing with the portal. + const returnUrl = YOUR_DOMAIN; + + const portalSession = await stripe.billingPortal.sessions.create({ + customer: checkoutSession.customer, + return_url: returnUrl, + }); + + res.redirect(303, portalSession.url); +}); +*/ +export default stripeRouter; diff --git a/backend/typescript/server.ts b/backend/typescript/server.ts index ccb6a64..0db2c34 100644 --- a/backend/typescript/server.ts +++ b/backend/typescript/server.ts @@ -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", @@ -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) => { diff --git a/backend/typescript/services/implementations/stripeService.ts b/backend/typescript/services/implementations/stripeService.ts new file mode 100644 index 0000000..e2122e7 --- /dev/null +++ b/backend/typescript/services/implementations/stripeService.ts @@ -0,0 +1,47 @@ +import dotenv from "dotenv"; +dotenv.config(); + +import IStripeService from "../interfaces/stripeService"; +import logger from "../../utilities/logger"; +import Stripe from "stripe"; + +const stripe = new Stripe(process.env.STRIPE_PRIVATE_KEY as string); + +const Logger = logger(__filename); + +console.log(process.env.STRIPE_PRIVATE_KEY); // Log the value of STRIPE_PRIVATE_KEY +class StripeService implements IStripeService { + createCheckoutSessionPayment = async ( + user_id: string, + amount: number, + cause_id: number, + ): Promise => { + try { + const session = await stripe.checkout.sessions.create({ + //ui_mode: "embedded", + payment_method_types: ["card"], + mode: "payment", + line_items: [ + { + price: "$8.99", + quantity: 1, + }, + ], + success_url: `http://localhost:5000/checkout-success`, + cancel_url: `http://localhost:5000/checkout-cancel`, + }); + + 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; diff --git a/backend/typescript/services/interfaces/donationService.ts b/backend/typescript/services/interfaces/donationService.ts index 069cfca..61514be 100644 --- a/backend/typescript/services/interfaces/donationService.ts +++ b/backend/typescript/services/interfaces/donationService.ts @@ -30,7 +30,7 @@ interface IDonationService { user_id: string, amount: number, cause_id: number, - is_recurring: string, + is_recurring: string, // should this be Recurrence? confirmation_email_sent: boolean, ): Promise; } diff --git a/backend/typescript/services/interfaces/stripeService.ts b/backend/typescript/services/interfaces/stripeService.ts new file mode 100644 index 0000000..3fa34a0 --- /dev/null +++ b/backend/typescript/services/interfaces/stripeService.ts @@ -0,0 +1,19 @@ +import { DonationDTO, Recurrence } from "../../types"; + +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 + */ + createCheckoutSessionPayment( + user_id: string, + amount: number, + cause_id: number, + ): Promise; +} + +export default IStripeService; diff --git a/backend/typescript/yarn.lock b/backend/typescript/yarn.lock index 0f493c6..478e64b 100644 --- a/backend/typescript/yarn.lock +++ b/backend/typescript/yarn.lock @@ -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/debug@5.11.0": version "5.11.0" @@ -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" @@ -7600,6 +7607,13 @@ qs@6.11.0: 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" @@ -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== @@ -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"