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 | 3x 3x 3x 3x 3x 3x 58x 58x 58x 58x 7x 7x 1x 6x 1x 5x 1x 4x 2x 4x 2x 4x 4x 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) {
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)
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]) {
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) {
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>) {
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,
},
)
}
}
|