a1d5d161dd
* Move concurrently dep upwards * Add express as explicit dependency * Accept just one HTTP method per endpoint * Fix endpoint option coalescing * Expressification of cloud functions * Nicer logging of API requests * Refactor web package.json * Add ts-node and nodemon to dev dependencies, bring back cors * Add scaffolding to point dev server at local functions * Enable emulator in dev server scaffolding * Fix up a little stuff I broke
171 lines
4.4 KiB
TypeScript
171 lines
4.4 KiB
TypeScript
import * as admin from 'firebase-admin'
|
|
import Stripe from 'stripe'
|
|
|
|
import { EndpointDefinition } from './api'
|
|
import { getPrivateUser, getUser, isProd, payUser } from './utils'
|
|
import { sendThankYouEmail } from './emails'
|
|
import { track } from './analytics'
|
|
|
|
export type StripeSession = Stripe.Event.Data.Object & {
|
|
id: string
|
|
metadata: {
|
|
userId: string
|
|
manticDollarQuantity: string
|
|
}
|
|
}
|
|
|
|
export type StripeTransaction = {
|
|
userId: string
|
|
manticDollarQuantity: number
|
|
sessionId: string
|
|
session: StripeSession
|
|
timestamp: number
|
|
}
|
|
|
|
const initStripe = () => {
|
|
const apiKey = process.env.STRIPE_APIKEY as string
|
|
return new Stripe(apiKey, { apiVersion: '2020-08-27', typescript: true })
|
|
}
|
|
|
|
// manage at https://dashboard.stripe.com/test/products?active=true
|
|
const manticDollarStripePrice = isProd()
|
|
? {
|
|
500: 'price_1KFQXcGdoFKoCJW770gTNBrm',
|
|
1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65',
|
|
2500: 'price_1KFQqNGdoFKoCJW7SDvrSaEB',
|
|
10000: 'price_1KFQraGdoFKoCJW77I4XCwM3',
|
|
}
|
|
: {
|
|
500: 'price_1K8W10GdoFKoCJW7KWORLec1',
|
|
1000: 'price_1K8bC1GdoFKoCJW76k3g5MJk',
|
|
2500: 'price_1K8bDSGdoFKoCJW7avAwpV0e',
|
|
10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE',
|
|
}
|
|
|
|
export const createcheckoutsession: EndpointDefinition = {
|
|
opts: { method: 'POST', minInstances: 1, secrets: ['STRIPE_APIKEY'] },
|
|
handler: async (req, res) => {
|
|
const userId = req.query.userId?.toString()
|
|
|
|
const manticDollarQuantity = req.query.manticDollarQuantity?.toString()
|
|
|
|
if (!userId) {
|
|
res.status(400).send('Invalid user ID')
|
|
return
|
|
}
|
|
|
|
if (
|
|
!manticDollarQuantity ||
|
|
!Object.keys(manticDollarStripePrice).includes(manticDollarQuantity)
|
|
) {
|
|
res.status(400).send('Invalid Mantic Dollar quantity')
|
|
return
|
|
}
|
|
|
|
const referrer =
|
|
req.query.referer || req.headers.referer || 'https://manifold.markets'
|
|
|
|
const stripe = initStripe()
|
|
const session = await stripe.checkout.sessions.create({
|
|
metadata: {
|
|
userId,
|
|
manticDollarQuantity,
|
|
},
|
|
line_items: [
|
|
{
|
|
price:
|
|
manticDollarStripePrice[
|
|
manticDollarQuantity as unknown as keyof typeof manticDollarStripePrice
|
|
],
|
|
quantity: 1,
|
|
},
|
|
],
|
|
mode: 'payment',
|
|
success_url: `${referrer}?funding-success`,
|
|
cancel_url: `${referrer}?funding-failiure`,
|
|
})
|
|
|
|
res.redirect(303, session.url || '')
|
|
},
|
|
}
|
|
|
|
export const stripewebhook: EndpointDefinition = {
|
|
opts: {
|
|
method: 'POST',
|
|
minInstances: 1,
|
|
secrets: ['MAILGUN_KEY', 'STRIPE_APIKEY', 'STRIPE_WEBHOOKSECRET'],
|
|
},
|
|
handler: async (req, res) => {
|
|
const stripe = initStripe()
|
|
let event
|
|
|
|
try {
|
|
// Cloud Functions jam the raw body into a special `rawBody` property
|
|
const rawBody = (req as any).rawBody ?? req.body
|
|
event = stripe.webhooks.constructEvent(
|
|
rawBody,
|
|
req.headers['stripe-signature'] as string,
|
|
process.env.STRIPE_WEBHOOKSECRET as string
|
|
)
|
|
} catch (e: any) {
|
|
console.log(`Webhook Error: ${e.message}`)
|
|
res.status(400).send(`Webhook Error: ${e.message}`)
|
|
return
|
|
}
|
|
|
|
if (event.type === 'checkout.session.completed') {
|
|
const session = event.data.object as StripeSession
|
|
await issueMoneys(session)
|
|
}
|
|
|
|
res.status(200).send('success')
|
|
},
|
|
}
|
|
|
|
const issueMoneys = async (session: StripeSession) => {
|
|
const { id: sessionId } = session
|
|
|
|
const query = await firestore
|
|
.collection('stripe-transactions')
|
|
.where('sessionId', '==', sessionId)
|
|
.get()
|
|
|
|
if (!query.empty) {
|
|
console.log('session', sessionId, 'already processed')
|
|
return
|
|
}
|
|
|
|
const { userId, manticDollarQuantity } = session.metadata
|
|
const payout = Number.parseInt(manticDollarQuantity)
|
|
|
|
const transaction: StripeTransaction = {
|
|
userId,
|
|
manticDollarQuantity: payout, // save as number
|
|
sessionId,
|
|
session,
|
|
timestamp: Date.now(),
|
|
}
|
|
|
|
await firestore.collection('stripe-transactions').add(transaction)
|
|
|
|
await payUser(userId, payout, true)
|
|
console.log('user', userId, 'paid M$', payout)
|
|
|
|
const user = await getUser(userId)
|
|
if (!user) return
|
|
|
|
const privateUser = await getPrivateUser(userId)
|
|
if (!privateUser) return
|
|
|
|
await sendThankYouEmail(user, privateUser)
|
|
|
|
await track(
|
|
userId,
|
|
'M$ purchase',
|
|
{ amount: payout, sessionId },
|
|
{ revenue: payout / 100 }
|
|
)
|
|
}
|
|
|
|
const firestore = admin.firestore()
|