Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 7x 7x 1x 6x 1x 1x 5x 1x 1x 4x 2x 4x 2x 4x 4x 1x 1x 1x 4x 4x 4x 4x 4x 6x 6x 5x 5x 1x 1x 1x 4x 3x 3x 2x 2x | import { ForbiddenException, Injectable, Logger, RawBodyRequest, UnauthorizedException, } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { Request } from 'express' import { Schema } from 'mongoose' import Stripe from 'stripe' import { UserDBService } from '../../db/services/user.service' import { PaymentOptions, paymentPlans } from '../interfaces/payments' import { StripeService } from './stripe.service' @Injectable() export class PaymentsService { private logger = new Logger(PaymentsService.name) private stripe: Stripe constructor( private stripeService: StripeService, private configService: ConfigService, private readonly userService: UserDBService, ) {} async createCheckoutSession( plan: string, userId: Schema.Types.ObjectId, ): Promise<Stripe.Response<Stripe.Checkout.Session>> { const user = await this.userService.findOneById(userId) if (!user) throw new UnauthorizedException( 'This user does not exist and cannot make a purchase', // How unfortunate, we always want money ) if (plan === user.paymentPlan) { this.logger.log(`Attempt for rebuy of current plan`) throw new ForbiddenException('You cannot rebuy a plan') // Actually I would love to allow it if it means more money } if (paymentPlans[plan] < paymentPlans[user.paymentPlan]) { this.logger.log(`Attempt for downgrad plan seen! Thats illegal!!!`) throw new ForbiddenException('You cannot downgrade your plan') // Actually I would love to allow it if it means more money } let price_id: string if (plan === 'single') price_id = this.configService.get('STRIPE_ITEM_SINGLE') if (plan === 'family') price_id = user.paymentPlan === 'single' ? this.configService.get('STRIPE_ITEM_SINGLE_TO_FAMILY') : this.configService.get('STRIPE_ITEM_FAMILY') let customer = user.stripeCustomerId if (!customer) { this.logger.log(`Creating stripe customer for user`) customer = (await this.stripeService.customer_create(user.email)).id await this.userService.updateUserStripeCustomerId( user._id.toString(), customer, ) } const session = await this.stripeService.checkout_session_create( plan, price_id, customer, ) this.logger.debug('customer:', customer) // This prevents the user from receiving a session if we can't match the customerId in the db or set his status to pending // Guarantees consistency in information since we would later not be able to upgrade the users plan await this.userService.updateUserCheckoutInformation(customer, { status: 'pending', lastInformationTime: session.created, }) this.logger.debug('session: ', session) return session } // Make sure Stripe is configured to only send the relevant events, in our case checkout.session.completed async handleWebhook(req: RawBodyRequest<Request>): Promise<void> { const signature = req.headers['stripe-signature'] const event = await this.stripeService.webhook_constructEvent( req.body, signature as string, ) // We only really care for the completion of the checkout, everything else is relevant on the frontend site // We can disable/enable the webhook allowed types in the Stripe dashboard but save is save this.logger.debug(event) if ( event.type.includes('failed') || event.type.includes('canceled') || event.type.includes('expired') ) { const object = event.data.object as Stripe.Checkout.Session await this.userService.updateUserCheckoutInformation( object.customer as string, { status: 'failed', lastInformationTime: event.created, }, ) return } // Just making sure we ignore unimportant events if (!(event.type === 'checkout.session.completed')) return const checkoutSession = event.data.object as Stripe.Checkout.Session // TODO: Check: We don't know if payment methods may return something like will be paid in case of SEPA or others if (checkoutSession.payment_status !== 'paid') return // This is idempotent, there is no problem that if the request comes again, that the user is already on the plan await this.userService.updateUserPaymentPlan( checkoutSession.customer as string, checkoutSession.metadata.plan as PaymentOptions, ) await this.userService.updateUserCheckoutInformation( checkoutSession.customer as string, { status: 'paid', lastInformationTime: event.created, }, ) } } |