All files / payments/services payments.service.ts

100% Statements 48/48
100% Branches 14/14
100% Functions 3/3
100% Lines 45/45

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