Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jbranchaud/sks 91 set up payment options for epic web #1473

Merged
merged 5 commits into from Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,12 +1,12 @@
import {inngest} from 'inngest/inngest.server'
import {v4} from 'uuid'
import {prisma} from '@skillrecordings/database'
import {defaultContext as defaultStripeContext} from '@skillrecordings/stripe-sdk'
import {loadSanityProduct} from './index'
import {sanityWriteClient} from 'utils/sanity-server'
import {SANITY_WEBHOOK_EVENT} from '../sanity-inngest-events'
import {paymentOptions} from 'pages/api/skill/[...skillRecordings]'

const {stripe} = defaultStripeContext
const stripe = paymentOptions.providers.stripe?.paymentClient

export const sanityProductCreated = inngest.createFunction(
{id: `product-create`, name: 'Create Product in Database'},
Expand All @@ -15,6 +15,10 @@ export const sanityProductCreated = inngest.createFunction(
if: 'event.data.event == "product.create"',
},
async ({event, step}) => {
if (!stripe) {
throw new Error('Payment provider (Stripe) is missing')
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joelhooks does this make sense as a way to handle the "Stripe provider not defined" scenario? This is mostly to let TS know that it definitely exists below since it has to get defined in the API setup for the app to work.

Or is there a more idiomatic inngest way of bailing if the payment provider isn't configured?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will keep retrying. there's a specific error if you want it to stop trying

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will keep retrying. there's a specific error if you want it to stop trying

nice, I updated it to use NonRetriableError 🙌 https://www.inngest.com/docs/guides/error-handling#preventing-retries-with-non-retriable-errors


const sanityProduct = await step.run('get sanity product', async () => {
return loadSanityProduct(event.data._id)
})
Expand Down
@@ -1,9 +1,9 @@
import {inngest} from 'inngest/inngest.server'
import {prisma} from '@skillrecordings/database'
import {SANITY_WEBHOOK_EVENT} from '../sanity-inngest-events'
import {defaultContext as defaultStripeContext} from '@skillrecordings/stripe-sdk'
import {paymentOptions} from 'pages/api/skill/[...skillRecordings]'

const {stripe} = defaultStripeContext
const stripe = paymentOptions.providers.stripe?.paymentClient

export const sanityProductDeleted = inngest.createFunction(
{id: `product-delete`, name: 'Deactivate Product in Database'},
Expand All @@ -12,6 +12,10 @@ export const sanityProductDeleted = inngest.createFunction(
if: 'event.data.event == "product.delete"',
},
async ({event, step}) => {
if (!stripe) {
throw new Error('Payment provider (Stripe) is missing')
}

const {productId} = event.data

if (!productId) {
Expand Down
Expand Up @@ -3,9 +3,9 @@ import {prisma} from '@skillrecordings/database'
import {v4} from 'uuid'
import {loadSanityProduct} from './index'
import {SANITY_WEBHOOK_EVENT} from '../sanity-inngest-events'
import {defaultContext as defaultStripeContext} from '@skillrecordings/stripe-sdk'
import {paymentOptions} from 'pages/api/skill/[...skillRecordings]'

const {stripe} = defaultStripeContext
const stripe = paymentOptions.providers.stripe?.paymentClient

export const sanityProductUpdated = inngest.createFunction(
{
Expand All @@ -22,6 +22,10 @@ export const sanityProductUpdated = inngest.createFunction(
if: 'event.data.event == "product.update"',
},
async ({event, step}) => {
if (!stripe) {
throw new Error('Payment provider (Stripe) is missing')
}

const sanityProduct = await step.run('get sanity product', async () => {
return loadSanityProduct(event.data._id)
})
Expand Down
23 changes: 11 additions & 12 deletions apps/epic-web/src/inngest/functions/stripe/webhook-received.ts
Expand Up @@ -5,22 +5,21 @@ import {prisma} from '@skillrecordings/database'
import {NonRetriableError} from 'inngest'
import {postToSlack} from '@skillrecordings/skill-api'
import {WebClient} from '@slack/web-api'
import {
defaultContext as defaultStripeContext,
Stripe,
} from '@skillrecordings/stripe-sdk'
import {paymentOptions} from 'pages/api/skill/[...skillRecordings]'
import {z} from 'zod'

const {stripe} = defaultStripeContext
const stripe = paymentOptions.providers.stripe?.paymentClient

const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
const CustomerSchema = z.object({id: z.string(), email: z.string()})

export const stripeWebhookReceived = inngest.createFunction(
{id: `stripe-webhook-received`, name: 'Stripe Webhook Received'},
{event: STRIPE_WEBHOOK_RECEIVED_EVENT},
async ({event, step}) => {
if (!stripe) {
throw new Error('Payment provider (Stripe) is missing')
}

const stripeAccountId = await step.run(
'get stripe account id',
async () => {
Expand Down Expand Up @@ -54,9 +53,9 @@ export const stripeWebhookReceived = inngest.createFunction(
}

const customer = await step.run('get customer', async () => {
return (await stripe.customers.retrieve(
invoice.customer as string,
)) as Stripe.Customer
return CustomerSchema.parse(
await stripe.customers.retrieve(invoice.customer as string),
)
})

if (!customer) {
Expand Down
12 changes: 12 additions & 0 deletions apps/epic-web/src/pages/api/skill/[...skillRecordings].ts
Expand Up @@ -6,13 +6,25 @@ import {nextAuthOptions} from '../auth/[...nextauth]'
import {getToken} from 'next-auth/jwt'
import {NextApiRequest} from 'next'
import {getCurrentAbility, UserSchema} from '@skillrecordings/skill-lesson'
import {
defaultPaymentOptions,
StripeProvider,
} from '@skillrecordings/commerce-server'

export const paymentOptions = defaultPaymentOptions({
stripeProvider: StripeProvider({
stripeSecretKey: process.env.STRIPE_SECRET_TOKEN,
apiVersion: '2020-08-27',
}),
})

export const skillOptions: SkillRecordingsOptions = {
site: {
title: process.env.NEXT_PUBLIC_SITE_TITLE,
supportEmail: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
},
nextAuthOptions,
paymentOptions,
getAbility: async (req: IncomingRequest) => {
const token = await getToken({req: req as unknown as NextApiRequest})
return getCurrentAbility({user: UserSchema.parse(token)})
Expand Down
7 changes: 3 additions & 4 deletions apps/epic-web/src/pages/invoices/[merchantChargeId].tsx
@@ -1,10 +1,7 @@
import * as React from 'react'
import {DownloadIcon} from '@heroicons/react/outline'
import {convertToSerializeForNextResponse} from '@skillrecordings/commerce-server'
import {useLocalStorage} from 'react-use'
import {GetServerSideProps} from 'next'
import {Coupon, MerchantProduct} from '@skillrecordings/database'
import {Stripe} from 'stripe'
import fromUnixTime from 'date-fns/fromUnixTime'
import Layout from 'components/app/layout'
import format from 'date-fns/format'
Expand Down Expand Up @@ -66,7 +63,9 @@ const Invoice: React.FC<

const {charge, product, bulkCoupon, quantity} = chargeDetails.result

const customer = charge.customer as Stripe.Customer
const customer = z
.object({name: z.string().nullish(), email: z.string().nullish()})
.parse(charge.customer)
const formatUsd = (amount: number) => {
return Intl.NumberFormat('en-US', {
style: 'currency',
Expand Down
24 changes: 12 additions & 12 deletions apps/epic-web/src/pages/thanks/purchase.tsx
Expand Up @@ -5,7 +5,6 @@ import {
convertToSerializeForNextResponse,
determinePurchaseType,
PurchaseType,
stripeData,
} from '@skillrecordings/commerce-server'
import {getSdk, Purchase} from '@skillrecordings/database'
import CopyInviteLink from '@skillrecordings/skill-lesson/team/copy-invite-link'
Expand All @@ -17,32 +16,34 @@ import {isEmpty} from 'lodash'
import {Transfer} from 'purchase-transfer/purchase-transfer'
import {trpc} from 'trpc/trpc.client'
import {getPostPurchaseThanksText} from 'utils/get-post-purchase-thanks-text'
import {paymentOptions} from 'pages/api/skill/[...skillRecordings]'

export const getServerSideProps: GetServerSideProps = async (context) => {
const {query} = context

const {session_id} = query
const session_id =
query.session_id instanceof Array ? query.session_id[0] : query.session_id

if (!session_id) {
const paymentProvider = paymentOptions.providers.stripe

if (!session_id || !paymentProvider) {
return {
notFound: true,
}
}

const purchaseInfo = await stripeData({
checkoutSessionId: session_id as string,
})
const purchaseInfo = await paymentProvider.getPurchaseInfo(session_id)

const {
email,
stripeChargeId,
chargeIdentifier,
quantity: seatsPurchased,
stripeProduct,
product: merchantProduct,
} = purchaseInfo

const stripeProductName = stripeProduct.name
const stripeProductName = merchantProduct.name

const purchase = await getSdk().getPurchaseForStripeCharge(stripeChargeId)
const purchase = await getSdk().getPurchaseForStripeCharge(chargeIdentifier)

if (!purchase || !email) {
return {
Expand All @@ -51,7 +52,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
}

const purchaseType = await determinePurchaseType({
checkoutSessionId: session_id as string,
checkoutSessionId: session_id,
})

const product = await getProduct(purchase.productId)
Expand Down Expand Up @@ -81,7 +82,6 @@ const InlineTeamInvite = ({
}) => {
if (!bulkCouponId) return null


return (
<div className="mx-auto w-full px-5">
<h2 className="pb-2 text-sm font-medium">Invite your team</h2>
Expand Down
24 changes: 11 additions & 13 deletions apps/epic-web/src/pages/welcome/index.tsx
@@ -1,40 +1,38 @@
import * as React from 'react'
import {DocumentTextIcon, UserGroupIcon} from '@heroicons/react/outline'
import {
convertToSerializeForNextResponse,
stripeData,
} from '@skillrecordings/commerce-server'
import {useSession} from 'next-auth/react'
import {GetServerSideProps} from 'next'
import {getToken} from 'next-auth/jwt'
import Layout from 'components/app/layout'
import {getSdk, prisma} from '@skillrecordings/database'
import Link from 'next/link'
import {first, isString} from 'lodash'
import {isString} from 'lodash'
import InviteTeam from '@skillrecordings/skill-lesson/team'
import {InvoiceCard} from 'pages/invoices'
import MuxPlayer from '@mux/mux-player-react'
import {SanityDocument} from '@sanity/client'
import Image from 'next/legacy/image'
import {trpc} from '../../trpc/trpc.client'
import {Transfer} from 'purchase-transfer/purchase-transfer'
import {getProduct} from 'lib/products'
import {paymentOptions} from 'pages/api/skill/[...skillRecordings]'

export const getServerSideProps: GetServerSideProps = async ({req, query}) => {
const {purchaseId: purchaseQueryParam, session_id, upgrade} = query
const {purchaseId: purchaseQueryParam, upgrade} = query
const session_id =
query.session_id instanceof Array ? query.session_id[0] : query.session_id
const token = await getToken({req})
const {getPurchaseDetails} = getSdk()

const paymentProvider = paymentOptions.providers.stripe

let purchaseId = purchaseQueryParam

if (session_id) {
const {stripeChargeId} = await stripeData({
checkoutSessionId: session_id as string,
})
if (session_id && paymentProvider) {
const {chargeIdentifier} = await paymentProvider.getPurchaseInfo(session_id)

const purchase = await prisma.purchase.findFirst({
where: {
merchantCharge: {
identifier: stripeChargeId,
identifier: chargeIdentifier,
},
},
})
Expand Down