generated from UoaWDCC/ssr-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #136 from UoaWDCC/tarun/feature/stripe-express
Add Stripe Payments, Clerk Login, Database Changes
- Loading branch information
Showing
53 changed files
with
2,015 additions
and
796 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,3 +67,60 @@ There is a yarn script called yarn watch-types that would automatically create c | |
```bash | ||
yarn watch-types | ||
``` | ||
|
||
# Instructions for setting up Stripe: | ||
|
||
1. Install Stripe CLI and login using the AUIS acount. Refer to instructions here: https://docs.stripe.com/stripe-cli | ||
2. Setup the appropriate .env files for both frontend and backend: see the #resources chat in discord. | ||
3. Setup webhook by following the Stripe docs: https://docs.stripe.com/payments/checkout/fulfill-orders?lang=node . Ensure that you use the following code to setup the webhook listen endpoint: `stripe listen --forward-to localhost:3000/api/stripe/webhook` | ||
|
||
Additional info: Stripe restricts all production web apps to run in HTTPS. HTTP for development is fine. | ||
|
||
# Notes: | ||
Schema: users = admin users for strapi | ||
Schema: people = users that sign up | ||
Schema: user_tickets/people_tickets = tickets that the users purchase | ||
|
||
Additional info: Stripe restricts all production web apps to run in HTTPS. HTTP for development is fine. | ||
|
||
Stripe will pay AUIS's bank account every 7-14 days' based on Stripe's risk assessment of AUIS. Read more here: https://docs.stripe.com/payouts | ||
|
||
Stripe API Documentation: https://docs.stripe.com/api/ | ||
|
||
Accept a payment (embedded form): https://docs.stripe.com/payments/accept-a-payment?platform=web&ui=embedded-form | ||
|
||
Handle Payments (webhook included): https://docs.stripe.com/payments/handling-payment-events | ||
|
||
Fulfill Orders (Webhook stuff): https://docs.stripe.com/payments/checkout/fulfill-orders | ||
|
||
Register Webhook (FOR PRODUCTION): https://docs.stripe.com/webhooks#register-webhook | ||
|
||
Product and Pricing: https://docs.stripe.com/products-prices/how-products-and-prices-work | ||
|
||
# Instructions for setting up Clerk | ||
Follow Step 1: Connect except the "DEPLOY YOUR APP ONLINE" part: https://dashboard.ngrok.com/get-started/setup/windows | ||
|
||
Step 2: Run `ngrok http --url=https://gelding-trusty-exactly.ngrok-free.app 3000`. | ||
|
||
Step 3: Now try to create a new user and login. You should now see an event of type "user.created" in the console. | ||
|
||
# Instructions for testing with Clerk: | ||
|
||
Email addresses | ||
Any email with the +clerk_test subaddress is a test email address. No emails will be sent, and they can be verified with the code 424242. | ||
|
||
For example: | ||
|
||
[email protected] | ||
[email protected] | ||
|
||
# Additional articles for Clerk | ||
https://clerk.com/docs/integrations/webhooks/overview | ||
|
||
https://clerk.com/docs/integrations/webhooks/sync-data | ||
|
||
https://ngrok.com/docs/integrations/clerk/webhooks/ | ||
|
||
https://docs.svix.com/receiving/verifying-payloads/how#nodejs-express | ||
|
||
https://clerk.com/docs/deployments/overview |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import express, { Request, Response, Router, json } from "express"; | ||
import asyncHandler from "../middleware/asyncHandler"; | ||
import { | ||
isTicketAvailableByEventId, | ||
reserveTicket, | ||
releaseReservedTicket, | ||
completeTicketPurchase, | ||
} from "../gateway/eventsGateway"; | ||
import Stripe from "stripe"; //Types and Interfaces | ||
import { stripe } from "../stripe/stripe"; | ||
|
||
const endpointSecret: string = process.env.STRIPE_WEBHOOK_ENDPOINT as string; | ||
|
||
// Create a checkout session based on priceId. Send a client secret back (cs_ABCD123) | ||
export const createEventCheckoutSession = asyncHandler( | ||
async (req: Request, res: Response) => { | ||
const { priceId, eventId } = req.body; | ||
|
||
// if priceId is undefined, send a 404 back. | ||
if (priceId == undefined || priceId == "") { | ||
return res | ||
.send({ | ||
error: | ||
"This event does not exist, or is not available for sale at the moment.", | ||
}) | ||
.status(404); | ||
} | ||
|
||
//check eventId validity | ||
if (!eventId || eventId == "") { | ||
return res | ||
.send({ | ||
error: | ||
"This event does not exist, or is not available for sale at the moment.", | ||
}) | ||
.status(404); | ||
} | ||
|
||
let ticketAvailable = await isTicketAvailableByEventId(eventId); | ||
|
||
if (ticketAvailable == false) { | ||
return res.send({ | ||
error: | ||
"There are no tickets available for this event. Please come back later to see if more tickets become available.", | ||
}); | ||
} else { | ||
reserveTicket(0); | ||
} | ||
|
||
// epoch time in seconds, 30mins timeout | ||
let session_expiry = Math.floor(new Date().getTime() / 1000 + 30 * 60); | ||
|
||
try { | ||
const session = await stripe.checkout.sessions.create({ | ||
//do not change anything below | ||
ui_mode: "embedded", | ||
expires_at: session_expiry, | ||
line_items: [ | ||
{ | ||
// Provide the exact Price ID (pr_1234) of the product on sale | ||
price: priceId, | ||
quantity: 1, | ||
}, | ||
], | ||
mode: "payment", | ||
payment_method_types: ["card"], | ||
currency: "NZD", | ||
return_url: `${process.env.DOMAIN_FRONTEND}return?session_id={CHECKOUT_SESSION_ID}`, | ||
|
||
//changeable below: | ||
// use metadata property | ||
metadata: { eventId: `${eventId}` }, | ||
}); | ||
|
||
res.send({ clientSecret: session.client_secret }); | ||
} catch (error) { | ||
res.send({ error }).status(404); | ||
} | ||
} | ||
); | ||
|
||
export const getSessionStatus = asyncHandler( | ||
async (req: Request, res: Response) => { | ||
const session = await stripe.checkout.sessions.retrieve( | ||
req.query.session_id as string | ||
); | ||
|
||
res.send({ | ||
status: session.status, | ||
customer_email: session.customer_details?.email, | ||
}); | ||
} | ||
); | ||
|
||
export const handleWebhook = asyncHandler( | ||
async (req: express.Request, res: express.Response): Promise<void> => { | ||
const sig = req.headers["stripe-signature"] as string | string[] | Buffer; | ||
|
||
let event: Stripe.Event; | ||
|
||
try { | ||
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret); | ||
|
||
if (event.type === "checkout.session.completed") { | ||
const session: Stripe.Checkout.Session = event.data.object; | ||
completeTicketPurchase(session.id); | ||
} else if (event.type === "checkout.session.expired") { | ||
const session = event.data.object; | ||
|
||
if ( | ||
session.metadata != null && | ||
session.metadata["eventId"] != undefined | ||
) { | ||
releaseReservedTicket(parseInt(session.metadata["eventId"])); | ||
} | ||
} | ||
} catch (err) { | ||
res.status(400).send(`Webhook Error: ${err}`); | ||
return; | ||
} | ||
|
||
res.json({ received: true }); | ||
} | ||
); | ||
|
||
export const templateFunction = asyncHandler( | ||
async (req: Request, res: Response) => { | ||
throw new Error("Not implemented yet"); | ||
} | ||
); |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { and, eq, sql, gt } from "drizzle-orm"; | ||
import { db } from "../db/config/db"; | ||
import { events, user_tickets, peoples, tickets } from "../schemas/schema"; | ||
import Stripe from "stripe"; | ||
import { stripe } from "../stripe/stripe"; | ||
|
||
export async function isTicketAvailableByEventId( | ||
eventId: any | ||
): Promise<boolean> { | ||
let isTicketAvailable = false; | ||
|
||
const remainingTickets = await db.query.events.findFirst({ | ||
columns: { event_capacity_remaining: true }, | ||
// Event must be LIVE (true) for reserve and sales to go through. | ||
where: and( | ||
and(eq(events.id, eventId), eq(events.is_live, true)), | ||
gt(events.event_capacity_remaining, eventId) | ||
), | ||
}); | ||
|
||
// Handle the case where remainingTickets or event_capacity_remaining is undefined or null | ||
if (remainingTickets && remainingTickets.event_capacity_remaining != null) { | ||
isTicketAvailable = remainingTickets.event_capacity_remaining > 0; | ||
} | ||
|
||
return isTicketAvailable; | ||
} | ||
|
||
// @Ratchet7x5: Reserve one ticket | ||
export async function reserveTicket(eventId: number) { | ||
let canReserveTicket = await isTicketAvailableByEventId(eventId); | ||
let reservedTicket; | ||
|
||
//if ticket available, reduce by 1 | ||
if (canReserveTicket === true) { | ||
// sql for reserving statement | ||
reservedTicket = await db | ||
.update(events) | ||
.set({ | ||
event_capacity_remaining: sql`${events.event_capacity_remaining} - 1`, | ||
}) | ||
.where(eq(events.id, eventId)) | ||
.returning(); | ||
} | ||
|
||
return reservedTicket; | ||
} | ||
|
||
// @Ratchet7x5: Release one reserved ticket | ||
export async function releaseReservedTicket(eventId: number) { | ||
let releasedTicket; | ||
|
||
// increment event_remaining_ticket by 1 | ||
releasedTicket = await db | ||
.update(events) | ||
.set({ | ||
event_capacity_remaining: sql`${events.event_capacity_remaining} + 1`, | ||
}) | ||
.where(eq(events.id, eventId)) | ||
.returning(); | ||
|
||
return releasedTicket; | ||
} | ||
|
||
export async function completeTicketPurchase(sessionId: string) { | ||
// TODO: Make this function safe to run multiple times, | ||
// even concurrently, with the same session ID | ||
|
||
// TODO: Make sure fulfillment hasn't already been | ||
// peformed for this Checkout Session | ||
|
||
//retrieve session from API with line_items expanded | ||
const checkoutSession = await stripe.checkout.sessions.retrieve(sessionId, { | ||
expand: ["line_items"], | ||
}); | ||
|
||
// Check the Checkout Session's payment_status property | ||
// to determine if fulfillment should be peformed | ||
if (checkoutSession.payment_status !== "unpaid") { | ||
db.insert(tickets).values({}); | ||
} | ||
} |
Oops, something went wrong.