UNPKG

@coursebuilder/core

Version:

Core package for Course Builder

334 lines (320 loc) 9.68 kB
import Stripe from 'stripe' import { PURCHASE_STATUS_UPDATED_EVENT } from '../../inngest/commerce/event-purchase-status-updated' import { STRIPE_CHECKOUT_SESSION_COMPLETED_EVENT } from '../../inngest/stripe/event-checkout-session-completed' import { checkoutSessionCompletedEvent } from '../../schemas/stripe/checkout-session-completed' import { InternalOptions, InternalProvider, PaymentsProviderConfig, } from '../../types' import { sendServerEmail } from '../send-server-email' const exampleEvent = { id: 'evt_1P6e0gAclagrtXef9ybQkT50', object: 'event', api_version: '2024-06-20', created: 1713381550, data: { object: { id: 'cs_test_a1HUHxQCrAdyfBpHFGeL6Rg5gmTWK7f4hN09zB9Bk8cJBtWDLTYZeMCyqP', object: 'checkout.session', after_expiration: null, allow_promotion_codes: null, amount_subtotal: 1000, amount_total: 1000, automatic_tax: { enabled: false, liability: null, status: null }, billing_address_collection: null, cancel_url: 'https://neatly-diverse-goldfish.ngrok-free.app/checkout-cancel', client_reference_id: null, client_secret: null, consent: null, consent_collection: null, created: 1713381530, currency: 'usd', currency_conversion: null, custom_fields: [], custom_text: { after_submit: null, shipping_address: null, submit: null, terms_of_service_acceptance: null, }, customer: 'cus_Pvsd48NJAKWjQ4', customer_creation: null, customer_details: { address: { city: null, country: 'US', line1: null, line2: null, postal_code: '42424', state: null, }, email: 'joelhooks@gmail.com', name: '42424', phone: null, tax_exempt: 'none', tax_ids: [], }, customer_email: null, expires_at: 1713424729, invoice: null, invoice_creation: { enabled: false, invoice_data: { account_tax_ids: null, custom_fields: null, description: null, footer: null, issuer: null, metadata: {}, rendering_options: null, }, }, livemode: false, locale: null, metadata: { siteName: 'inngest-gpt', userId: '57b0d091-9dfa-4f03-8774-e43f1ec5f3b8', country: 'US', productId: 'product_cluq5r0jl000008jp20fbfsgs', bulk: 'true', product: 'Test Event Product', ip_address: '', }, mode: 'payment', payment_intent: 'pi_3P6e0MAclagrtXef0vs1HBZt', payment_link: null, payment_method_collection: 'always', payment_method_configuration_details: null, payment_method_options: { card: { request_three_d_secure: 'automatic' } }, payment_method_types: ['card'], payment_status: 'paid', phone_number_collection: { enabled: false }, recovered_from: null, setup_intent: null, shipping: null, shipping_address_collection: null, shipping_options: [], shipping_rate: null, status: 'complete', submit_type: null, subscription: null, success_url: 'https://neatly-diverse-goldfish.ngrok-free.app/checkout-success/thanks/purchase?session_id={CHECKOUT_SESSION_ID}&provider=stripe', total_details: { amount_discount: 0, amount_shipping: 0, amount_tax: 0 }, ui_mode: 'hosted', url: null, }, }, livemode: false, pending_webhooks: 2, request: { id: null, idempotency_key: null }, type: 'checkout.session.completed', } const exampleRefundEvent = { id: 'evt_3PfrkvCsSasMEpCd0eCwd9JH', object: 'event', api_version: '2023-10-16', created: 1721927689, data: { object: { id: 'ch_3PfrkvCsSasMEpCd0DEIwaHw', object: 'charge', amount: 29900, amount_captured: 29900, amount_refunded: 29900, application: null, application_fee: null, application_fee_amount: null, balance_transaction: 'txn_3PfrkvCsSasMEpCd0VwSyQPW', billing_details: { address: [Object], email: 'zac+stripe2@egghead.io', name: 'zac jones', phone: null, }, calculated_statement_descriptor: 'SKILL RECORDINGS INC', captured: true, created: 1721775892, currency: 'usd', customer: 'cus_QWvcHlEQa9CjmY', description: null, destination: null, dispute: null, disputed: false, failure_balance_transaction: null, failure_code: null, failure_message: null, fraud_details: {}, invoice: null, livemode: false, metadata: { country: 'US', productId: 'product-saubh', product: 'vbd fundamentals', siteName: 'Value-Based Design', bulk: 'true', }, on_behalf_of: null, order: null, outcome: { network_status: 'approved_by_network', reason: null, risk_level: 'normal', risk_score: 29, seller_message: 'Payment complete.', type: 'authorized', }, paid: true, payment_intent: 'pi_3PfrkvCsSasMEpCd0buMpotO', payment_method: 'pm_1PfrlGCsSasMEpCdrHqXiEVO', payment_method_details: { card: [Object], type: 'card' }, radar_options: {}, receipt_email: null, receipt_number: null, receipt_url: 'https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xT2dhTGFDc1Nhc01FcENkKIqQirUGMgZ4E9zYBrY6LBZoJZBxV0YFWe5U0wev7bhGuBYwfxLBkQZaUtL_4sibYtrnmm2flohfONHw', refunded: true, review: null, shipping: null, source: null, source_transfer: null, statement_descriptor: null, statement_descriptor_suffix: null, status: 'succeeded', transfer_data: null, transfer_group: null, }, previous_attributes: { amount_refunded: 0, receipt_url: 'https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xT2dhTGFDc1Nhc01FcENkKImQirUGMgb8mlBO7cE6LBZs87Bl3MFsTMt5bJxyGQfjUoymQKrjL7F82uaMhXFXmH-kJWwRds68F046', refunded: false, }, }, livemode: false, pending_webhooks: 2, request: { id: 'req_QHie5TSZsMRlfA', idempotency_key: 'eb0244bf-48f2-4f9c-b481-a671a19df1d9', }, type: 'charge.refunded', } export async function updatePurchaseStatus({ status, stripeChargeId, options, }: { options: InternalOptions<'payment'> stripeChargeId: string status: 'Refunded' | 'Disputed' | 'Banned' }) { await options.inngest.send({ name: PURCHASE_STATUS_UPDATED_EVENT, data: { stripeChargeId, status, }, }) const purchase = await options.adapter?.getPurchaseForStripeCharge(stripeChargeId) if (!purchase) throw new Error('No purchase') if (!purchase.merchantChargeId) throw new Error('No merchant charge') const merchantCharge = await options.adapter?.getMerchantCharge( purchase.merchantChargeId, ) if (!merchantCharge) throw new Error('No merchant charge') const merchantChargeId = merchantCharge.id return options.adapter?.updatePurchaseStatusForCharge( merchantChargeId, status, ) } export async function processStripeWebhook( event: any, options: InternalOptions<'payment'>, ) { const stripeProvider: InternalProvider<'payment'> = options.provider const stripeAdapter = stripeProvider.options.paymentsAdapter const courseBuilderAdapter = options.adapter // charge.dispute.created // charge.dispute.funds_withdrawn // charge.refunded // charge.succeeded // checkout.session.async_payment_failed // checkout.session.async_payment_succeeded // checkout.session.completed // customer.subscription.created // customer.subscription.deleted // customer.subscription.updated // customer.updated switch (event.type) { case 'charge.dispute.funds_withdrawn': console.log('charge.dispute.funds_withdrawn', event) // record the transaction break case 'charge.succeeded': console.log('charge.succeeded', event) // record the transaction break case 'customer.updated': console.log('customer.updated', event) // update the organization accordingly break case 'customer.subscription.created': console.log('customer.subscription.created', event) // add the subscription to the organization // assign appropriate permissions // if it is for a single member org the default is to give it to the purchasing user's memberahip to the org // if it is multi user org, or for multiple people default is to not assign any permissions // the subscription is owned by the organization // the purchaing member is the billing contact // the purchasing member is a subscription admin // additional billing contacts and admins can be added // admins can add/remove billing contacts and admins // there must be at least 1 admin and billing contact break case 'customer.subscription.deleted': console.log('customer.subscription.deleted', event) // this is the final cancellation of a subscription // remove the subscription from the organization break case 'customer.subscription.updated': console.log('customer.subscription.updated', event) // update the subscription // some status update for an existing subscription // Occurs whenever a subscription changes (e.g., switching from one plan to another, or changing the status from trial to active). break case 'checkout.session.completed': // is this a subscription payment? (event.data.object.mode) await options.inngest.send({ name: STRIPE_CHECKOUT_SESSION_COMPLETED_EVENT, data: { stripeEvent: checkoutSessionCompletedEvent.parse(event), }, }) break case 'checkout.session.async_payment_failed': console.log('checkout.session.async_payment_failed', event) break case 'checkout.session.async_payment_succeeded': console.log('checkout.session.async_payment_succeeded', event) break case 'charge.refunded': await updatePurchaseStatus({ stripeChargeId: event.data.object.id, status: 'Refunded', options, }) // log the transaction break case 'charge.dispute.created': await updatePurchaseStatus({ stripeChargeId: event.data.object.id, status: 'Disputed', options, }) break } }