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,
},
)
}
}
|