From f6e4f6701abf28c621d067fa448b41942fb4eea2 Mon Sep 17 00:00:00 2001 From: quantum-grit <91589884+quantum-grit@users.noreply.github.com> Date: Thu, 11 Aug 2022 19:34:20 +0300 Subject: [PATCH] Handle Stripe Payment_intent.canceled event (#321) * added @StripeWebhookHandler('payment_intent.canceled') and consolidated all handlers into a single StripePaymentService class * added tests for stripe hooks and moved test data in separate file Co-authored-by: quantum-grit --- TESTING.md | 4 +- apps/api/src/campaign/campaign.service.ts | 50 +- apps/api/src/donations/donations.module.ts | 6 +- .../events/payment-created.service.ts | 53 - .../payment-intent-succeeded.service.ts | 44 - .../donations/events/payment.service.spec.ts | 736 ------------ .../events/stripe-payment.service.spec.ts | 229 ++++ .../events/stripe-payment.service.ts | 115 ++ .../events/stripe-payment.testdata.ts | 1018 +++++++++++++++++ package.json | 1 + yarn.lock | 78 +- 11 files changed, 1470 insertions(+), 864 deletions(-) delete mode 100644 apps/api/src/donations/events/payment-created.service.ts delete mode 100644 apps/api/src/donations/events/payment-intent-succeeded.service.ts delete mode 100644 apps/api/src/donations/events/payment.service.spec.ts create mode 100644 apps/api/src/donations/events/stripe-payment.service.spec.ts create mode 100644 apps/api/src/donations/events/stripe-payment.service.ts create mode 100644 apps/api/src/donations/events/stripe-payment.testdata.ts diff --git a/TESTING.md b/TESTING.md index 92e809b0e..e7900de1d 100644 --- a/TESTING.md +++ b/TESTING.md @@ -26,5 +26,7 @@ yarn dev In a third shell trigger individual stripe events on demand ```shell -stripe trigger payment_intent.succeeded +stripe trigger payment_intent.succeeded --override payment_intent:metadata.campaignId=e8bf74dd-6212-4a0e-b192-56e4eb19e1f2 --override payment_intent:currency=BGN ``` + +Important - From the the Stripe CLI docs: Triggering some events like payment_intent.succeeded or payment_intent.canceled will also send you a payment_intent.created event for completeness. diff --git a/apps/api/src/campaign/campaign.service.ts b/apps/api/src/campaign/campaign.service.ts index d9bf46833..999fc4fb8 100644 --- a/apps/api/src/campaign/campaign.service.ts +++ b/apps/api/src/campaign/campaign.service.ts @@ -283,7 +283,7 @@ export class CampaignService { newDonationStatus: DonationStatus, ) { const campaignId = campaign.id - Logger.debug('[Stripe webhook] Update donation from state initial to waiting', { + Logger.debug('[Stripe webhook] Update donation to status: ' + newDonationStatus, { campaignId, paymentIntentId: paymentData.paymentIntentId, }) @@ -298,7 +298,7 @@ export class CampaignService { : // Create new vault for the campaign { create: { campaignId, currency: campaign.currency, name: campaign.title } } - // Find donation by extPaymentIntentId an update if status allows + // Find donation by extPaymentIntentId and update if status allows const donation = await this.prisma.donation.findUnique({ where: { extPaymentIntentId: paymentData.paymentIntentId }, @@ -307,28 +307,36 @@ export class CampaignService { //if missing create the donation with the incoming status if (!donation) { - Logger.error( + Logger.debug( 'No donation exists with extPaymentIntentId: ' + paymentData.paymentIntentId + ' Creating new donation with status: ' + newDonationStatus, ) - this.prisma.donation.create({ - data: { - amount: paymentData.netAmount, - chargedAmount: paymentData.chargedAmount, - currency: campaign.currency, - targetVault: targetVaultData, - provider: PaymentProvider.stripe, - type: DonationType.donation, - status: newDonationStatus, - extCustomerId: paymentData.stripeCustomerId ?? '', - extPaymentIntentId: paymentData.paymentIntentId, - extPaymentMethodId: paymentData.paymentMethodId ?? '', - billingName: paymentData.billingName, - billingEmail: paymentData.billingEmail, - }, - }) + + try { + await this.prisma.donation.create({ + data: { + amount: paymentData.netAmount, + chargedAmount: paymentData.chargedAmount, + currency: campaign.currency, + targetVault: targetVaultData, + provider: PaymentProvider.stripe, + type: DonationType.donation, + status: newDonationStatus, + extCustomerId: paymentData.stripeCustomerId ?? '', + extPaymentIntentId: paymentData.paymentIntentId, + extPaymentMethodId: paymentData.paymentMethodId ?? '', + billingName: paymentData.billingName, + billingEmail: paymentData.billingEmail, + }, + }) + } catch (error) { + Logger.error( + `[Stripe webhook] Error while creating donation with paymentIntentId: ${paymentData.paymentIntentId} and status: ${newDonationStatus} . Error is: ${error}`, + ) + throw new InternalServerErrorException(error) + } return } @@ -360,7 +368,7 @@ export class CampaignService { } //donation exists but we need to skip because previous status is from later event than the incoming else { - Logger.error( + Logger.warn( `[Stripe webhook] Skipping update of donation with paymentIntentId: ${paymentData.paymentIntentId} and status: ${newDonationStatus} because the event comes after existing donation with status: ${donation.status}`, ) @@ -375,7 +383,7 @@ export class CampaignService { chargedAmount: paymentData.chargedAmount, }) - this.updateDonationPayment(campaign, paymentData, DonationStatus.succeeded) + await this.updateDonationPayment(campaign, paymentData, DonationStatus.succeeded) const vault = await this.getCampaignVault(campaign.id) if (vault) { diff --git a/apps/api/src/donations/donations.module.ts b/apps/api/src/donations/donations.module.ts index 5f9393db7..5da2ce899 100644 --- a/apps/api/src/donations/donations.module.ts +++ b/apps/api/src/donations/donations.module.ts @@ -11,8 +11,7 @@ import { VaultModule } from '../vault/vault.module' import { VaultService } from '../vault/vault.service' import { DonationsController } from './donations.controller' import { DonationsService } from './donations.service' -import { PaymentCreatedService } from './events/payment-created.service' -import { PaymentSucceededService } from './events/payment-intent-succeeded.service' +import { StripePaymentService } from './events/stripe-payment.service' @Module({ imports: [ @@ -27,8 +26,7 @@ import { PaymentSucceededService } from './events/payment-intent-succeeded.servi controllers: [DonationsController], providers: [ DonationsService, - PaymentCreatedService, - PaymentSucceededService, + StripePaymentService, CampaignService, PrismaService, VaultService, diff --git a/apps/api/src/donations/events/payment-created.service.ts b/apps/api/src/donations/events/payment-created.service.ts deleted file mode 100644 index a0bf4d641..000000000 --- a/apps/api/src/donations/events/payment-created.service.ts +++ /dev/null @@ -1,53 +0,0 @@ -import Stripe from 'stripe' -import { BadRequestException, Injectable, Logger } from '@nestjs/common' -import { StripeWebhookHandler } from '@golevelup/nestjs-stripe' - -import { DonationMetadata } from '../dontation-metadata.interface' -import { CampaignService } from '../../campaign/campaign.service' -import { getPaymentData } from '../helpers/payment-intent-helpers' -import { DonationStatus } from '@prisma/client' - -@Injectable() -export class PaymentCreatedService { - constructor(private campaignService: CampaignService) {} - - @StripeWebhookHandler('payment_intent.created') - async handlePaymentIntentCreated(event: Stripe.Event) { - const paymentIntent: Stripe.PaymentIntent = event.data.object as Stripe.PaymentIntent - - Logger.debug( - '[ handlePaymentIntentCreated ]', - paymentIntent, - paymentIntent.metadata as DonationMetadata, - ) - - const metadata: DonationMetadata = paymentIntent.metadata as DonationMetadata - if (!metadata.campaignId) { - throw new BadRequestException( - 'Payment intent metadata does not contain target campaignId. Probably wrong session initiation. Payment intent is: ' + - paymentIntent.id, - ) - } - - const campaign = await this.campaignService.getCampaignById(metadata.campaignId) - - if (campaign.currency !== paymentIntent.currency.toUpperCase()) { - throw new BadRequestException( - `Donation in different currency is not allowed. Campaign currency ${ - campaign.currency - } <> donation currency ${paymentIntent.currency.toUpperCase()}`, - ) - } - - const billingDetails = getPaymentData(paymentIntent) - - /* - * Handle the create event - */ - await this.campaignService.updateDonationPayment( - campaign, - billingDetails, - DonationStatus.waiting, - ) - } -} diff --git a/apps/api/src/donations/events/payment-intent-succeeded.service.ts b/apps/api/src/donations/events/payment-intent-succeeded.service.ts deleted file mode 100644 index 44dafc1b5..000000000 --- a/apps/api/src/donations/events/payment-intent-succeeded.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Stripe from 'stripe' -import { StripeWebhookHandler } from '@golevelup/nestjs-stripe' -import { BadRequestException, Injectable, Logger } from '@nestjs/common' - -import { CampaignService } from '../../campaign/campaign.service' -import { DonationMetadata } from '../dontation-metadata.interface' -import { getPaymentData } from '../helpers/payment-intent-helpers' - -@Injectable() -export class PaymentSucceededService { - constructor(private campaignService: CampaignService) {} - - @StripeWebhookHandler('payment_intent.succeeded') - async handlePaymentIntentSucceeded(event: Stripe.Event) { - const paymentIntent: Stripe.PaymentIntent = event.data.object as Stripe.PaymentIntent - Logger.log( - '[ handlePaymentIntentSucceeded ]', - paymentIntent, - paymentIntent.metadata as DonationMetadata, - ) - - const metadata: DonationMetadata = paymentIntent.metadata as DonationMetadata - if (!metadata.campaignId) { - throw new BadRequestException( - 'Payment intent metadata does not contain target campaignId. Probably wrong session initiation. Payment intent is: ' + - paymentIntent.id, - ) - } - - const campaign = await this.campaignService.getCampaignById(metadata.campaignId) - - if (campaign.currency !== paymentIntent.currency.toUpperCase()) { - throw new BadRequestException( - `Donation in different currency is not allowed. Campaign currency ${ - campaign.currency - } <> donation currency ${paymentIntent.currency.toUpperCase()}`, - ) - } - - const billingData = getPaymentData(paymentIntent) - - await this.campaignService.donateToCampaign(campaign, billingData) - } -} diff --git a/apps/api/src/donations/events/payment.service.spec.ts b/apps/api/src/donations/events/payment.service.spec.ts deleted file mode 100644 index d1794e88b..000000000 --- a/apps/api/src/donations/events/payment.service.spec.ts +++ /dev/null @@ -1,736 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { ConfigService } from '@nestjs/config' -import { CampaignService } from '../../campaign/campaign.service' -import { PaymentSucceededService } from './payment-intent-succeeded.service' -import { getPaymentData } from '../helpers/payment-intent-helpers' -import Stripe from 'stripe' -import { VaultService } from '../../vault/vault.service' -import { PersonService } from '../../person/person.service' -import { MockPrismaService } from '../../prisma/prisma-client.mock' - -describe('PaymentService', () => { - let service: PaymentSucceededService - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ConfigService, - PaymentSucceededService, - CampaignService, - MockPrismaService, - VaultService, - PersonService, - ], - }).compile() - - service = module.get(PaymentSucceededService) - }) - - it('should be defined', () => { - expect(service).toBeDefined() - }) - - it('accept payment-intent.created', async () => { - const mockPaymentIntentCreated: Stripe.PaymentIntent = { - id: 'pi_3LNwijKApGjVGa9t1F9QYd5s', - object: 'payment_intent', - amount: 1065, - amount_capturable: 0, - amount_details: { - tip: {}, - }, - amount_received: 0, - application: null, - application_fee_amount: null, - automatic_payment_methods: null, - canceled_at: null, - cancellation_reason: null, - capture_method: 'automatic', - charges: { - object: 'list', - data: [], - has_more: false, - url: '/v1/charges?payment_intent=pi_3LNwijKApGjVGa9t1F9QYd5s', - }, - client_secret: null, - confirmation_method: 'automatic', - created: 1658399705, - currency: 'bgn', - customer: null, - description: null, - invoice: null, - last_payment_error: null, - livemode: false, - metadata: { - campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', - }, - next_action: null, - on_behalf_of: null, - payment_method: null, - payment_method_options: { - card: { - installments: null, - mandate_options: null, - network: null, - request_three_d_secure: 'automatic', - }, - }, - payment_method_types: ['card'], - processing: null, - receipt_email: null, - review: null, - setup_future_usage: null, - shipping: null, - source: null, - statement_descriptor: null, - statement_descriptor_suffix: null, - status: 'requires_payment_method', - transfer_data: null, - transfer_group: null, - } - - const billingDetails = getPaymentData(mockPaymentIntentCreated) - expect(billingDetails.netAmount).toEqual(0) - expect(billingDetails.chargedAmount).toEqual(1065) - }) - - it('accept payment-intent.succeeded with BG tax included in charge', async () => { - const mockPaymentIntentCreated: Stripe.PaymentIntent = { - id: 'pi_3LNwijKApGjVGa9t1F9QYd5s', - object: 'payment_intent', - amount: 1065, - amount_capturable: 0, - amount_details: { - tip: {}, - }, - amount_received: 1065, - application: null, - application_fee_amount: null, - automatic_payment_methods: null, - canceled_at: null, - cancellation_reason: null, - capture_method: 'automatic', - charges: { - object: 'list', - data: [ - { - id: 'ch_3LNwijKApGjVGa9t1tuRzvbL', - object: 'charge', - amount: 1065, - amount_captured: 1065, - amount_refunded: 0, - application: null, - application_fee: null, - application_fee_amount: null, - balance_transaction: 'txn_3LNwijKApGjVGa9t100xnggj', - billing_details: { - address: { - city: null, - country: 'BG', - line1: null, - line2: null, - postal_code: null, - state: null, - }, - email: 'test@gmail.com', - name: 'First Last', - phone: null, - }, - calculated_statement_descriptor: 'PODKREPI.BG', - captured: true, - created: 1658399779, - currency: 'bgn', - customer: 'cus_M691kVNYuUp4po', - 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: { - campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', - }, - on_behalf_of: null, - outcome: { - network_status: 'approved_by_network', - reason: null, - risk_level: 'normal', - risk_score: 33, - seller_message: 'Payment complete.', - type: 'authorized', - }, - paid: true, - payment_intent: 'pi_3LNwijKApGjVGa9t1F9QYd5s', - payment_method: 'pm_1LNwjtKApGjVGa9thtth9iu7', - payment_method_details: { - card: { - brand: 'visa', - checks: { - address_line1_check: null, - address_postal_code_check: null, - cvc_check: 'pass', - }, - country: 'BG', - exp_month: 4, - exp_year: 2024, - fingerprint: 'iCySKWAAAZGp2hwr', - funding: 'credit', - installments: null, - last4: '0000', - mandate: null, - network: 'visa', - three_d_secure: null, - wallet: null, - }, - type: 'card', - }, - receipt_email: 'test@gmail.com', - receipt_number: null, - receipt_url: 'https://pay.stripe.com/receipts/', - refunded: false, - refunds: { - object: 'list', - data: [], - has_more: false, - url: '/v1/charges/ch_3LNwijKApGjVGa9t1tuRzvbL/refunds', - }, - review: null, - shipping: null, - source: null, - source_transfer: null, - statement_descriptor: null, - statement_descriptor_suffix: null, - status: 'succeeded', - transfer_data: null, - transfer_group: null, - }, - ], - has_more: false, - url: '/v1/charges?payment_intent=pi_3LNwijKApGjVGa9t1F9QYd5s', - }, - client_secret: 'xxx', - confirmation_method: 'automatic', - created: 1658399705, - currency: 'bgn', - customer: 'cus_M691kVNYuUp4po', - description: null, - invoice: null, - last_payment_error: null, - livemode: false, - metadata: { - campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', - }, - next_action: null, - on_behalf_of: null, - payment_method: 'pm_1LNwjtKApGjVGa9thtth9iu7', - payment_method_options: { - card: { - installments: null, - mandate_options: null, - network: null, - request_three_d_secure: 'automatic', - }, - }, - payment_method_types: ['card'], - processing: null, - receipt_email: 'test@gmail.com', - review: null, - setup_future_usage: null, - shipping: null, - source: null, - statement_descriptor: null, - statement_descriptor_suffix: null, - status: 'succeeded', - transfer_data: null, - transfer_group: null, - } - - const billingDetails = getPaymentData(mockPaymentIntentCreated) - expect(billingDetails.netAmount).toEqual(1000) - expect(billingDetails.chargedAmount).toEqual(1065) - }) -}) - -it('accept payment-intent.succeeded with BG tax not included in charge', async () => { - const mockPaymentIntentCreated: Stripe.PaymentIntent = { - id: 'pi_3LNwkHKApGjVGa9t1TLyVofD', - object: 'payment_intent', - amount: 1000, - amount_capturable: 0, - amount_details: { - tip: {}, - }, - amount_received: 1000, - application: null, - application_fee_amount: null, - automatic_payment_methods: null, - canceled_at: null, - cancellation_reason: null, - capture_method: 'automatic', - charges: { - object: 'list', - data: [ - { - id: 'ch_3LNwkHKApGjVGa9t1bkp20zi', - object: 'charge', - amount: 1000, - amount_captured: 1000, - amount_refunded: 0, - application: null, - application_fee: null, - application_fee_amount: null, - balance_transaction: 'txn_3LNwkHKApGjVGa9t1EH1EZxk', - billing_details: { - address: { - city: null, - country: 'BG', - line1: null, - line2: null, - postal_code: null, - state: null, - }, - email: 'test@gmail.com', - name: 'nepokriti', - phone: null, - }, - calculated_statement_descriptor: 'PODKREPI.BG', - captured: true, - created: 1658399823, - currency: 'bgn', - customer: 'cus_M692d4eal3rlWR', - 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: { - campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', - }, - on_behalf_of: null, - outcome: { - network_status: 'approved_by_network', - reason: null, - risk_level: 'normal', - risk_score: 20, - seller_message: 'Payment complete.', - type: 'authorized', - }, - paid: true, - payment_intent: 'pi_3LNwkHKApGjVGa9t1TLyVofD', - payment_method: 'pm_1LNwkbKApGjVGa9tmWVdg46e', - payment_method_details: { - card: { - brand: 'visa', - checks: { - address_line1_check: null, - address_postal_code_check: null, - cvc_check: 'pass', - }, - country: 'BG', - exp_month: 4, - exp_year: 2032, - fingerprint: 'iCySKWAAAZGp2hwr', - funding: 'credit', - installments: null, - last4: '0000', - mandate: null, - network: 'visa', - three_d_secure: null, - wallet: null, - }, - type: 'card', - }, - receipt_email: 'test@gmail.com', - receipt_number: null, - receipt_url: 'https://pay.stripe.com/receipts/', - refunded: false, - refunds: { - object: 'list', - data: [], - has_more: false, - url: '/v1/charges/ch_3LNwkHKApGjVGa9t1bkp20zi/refunds', - }, - review: null, - shipping: null, - source: null, - source_transfer: null, - statement_descriptor: null, - statement_descriptor_suffix: null, - status: 'succeeded', - transfer_data: null, - transfer_group: null, - }, - ], - has_more: false, - url: '/v1/charges?payment_intent=pi_3LNwkHKApGjVGa9t1TLyVofD', - }, - client_secret: null, - confirmation_method: 'automatic', - created: 1658399801, - currency: 'bgn', - customer: 'cus_M692d4eal3rlWR', - description: null, - invoice: null, - last_payment_error: null, - livemode: false, - metadata: { - campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', - }, - next_action: null, - on_behalf_of: null, - payment_method: 'pm_1LNwkbKApGjVGa9tmWVdg46e', - payment_method_options: { - card: { - installments: null, - mandate_options: null, - network: null, - request_three_d_secure: 'automatic', - }, - }, - payment_method_types: ['card'], - processing: null, - receipt_email: 'test@gmail.com', - review: null, - setup_future_usage: null, - shipping: null, - source: null, - statement_descriptor: null, - statement_descriptor_suffix: null, - status: 'succeeded', - transfer_data: null, - transfer_group: null, - } - - const billingDetails = getPaymentData(mockPaymentIntentCreated) - expect(billingDetails.netAmount).toEqual(936) - expect(billingDetails.chargedAmount).toEqual(1000) -}) - -it('accept payment-intent.succeeded with US tax included in charge', async () => { - const mockPaymentIntentCreated: Stripe.PaymentIntent = { - id: 'pi_3LNziFKApGjVGa9t0sfUl30h', - object: 'payment_intent', - amount: 10350, - amount_capturable: 0, - amount_details: { - tip: {}, - }, - amount_received: 10350, - application: null, - application_fee_amount: null, - automatic_payment_methods: null, - canceled_at: null, - cancellation_reason: null, - capture_method: 'automatic', - charges: { - object: 'list', - data: [ - { - id: 'ch_3LNziFKApGjVGa9t07WB0NNl', - object: 'charge', - amount: 10350, - amount_captured: 10350, - amount_refunded: 0, - application: null, - application_fee: null, - application_fee_amount: null, - balance_transaction: 'txn_3LNziFKApGjVGa9t0H3v9oKL', - billing_details: { - address: { - city: null, - country: 'BG', - line1: null, - line2: null, - postal_code: null, - state: null, - }, - email: 'test@gmail.com', - name: '42424242', - phone: null, - }, - calculated_statement_descriptor: 'PODKREPI.BG', - captured: true, - created: 1658411254, - currency: 'bgn', - customer: 'cus_M6C76vpsFglyGh', - 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: { - campaignId: 'ef592bd8-edd8-42a0-95c0-0e97d26d8045', - }, - on_behalf_of: null, - outcome: { - network_status: 'approved_by_network', - reason: null, - risk_level: 'normal', - risk_score: 56, - seller_message: 'Payment complete.', - type: 'authorized', - }, - paid: true, - payment_intent: 'pi_3LNziFKApGjVGa9t0sfUl30h', - payment_method: 'pm_1LNziyKApGjVGa9tOR1sWkMV', - payment_method_details: { - card: { - brand: 'visa', - checks: { - address_line1_check: null, - address_postal_code_check: null, - cvc_check: 'pass', - }, - country: 'US', - exp_month: 4, - exp_year: 2024, - fingerprint: '2BUDwUpZNgnepjrE', - funding: 'credit', - installments: null, - last4: '4242', - mandate: null, - network: 'visa', - three_d_secure: null, - wallet: null, - }, - type: 'card', - }, - receipt_email: 'test@gmail.com', - receipt_number: null, - receipt_url: 'https://pay.stripe.com/receipts/', - refunded: false, - refunds: { - object: 'list', - data: [], - has_more: false, - url: '/v1/charges/ch_3LNziFKApGjVGa9t07WB0NNl/refunds', - }, - review: null, - shipping: null, - source: null, - source_transfer: null, - statement_descriptor: null, - statement_descriptor_suffix: null, - status: 'succeeded', - transfer_data: null, - transfer_group: null, - }, - ], - has_more: false, - url: '/v1/charges?payment_intent=pi_3LNziFKApGjVGa9t0sfUl30h', - }, - client_secret: null, - confirmation_method: 'automatic', - created: 1658411207, - currency: 'bgn', - customer: 'cus_M6C76vpsFglyGh', - description: null, - invoice: null, - last_payment_error: null, - livemode: false, - metadata: { - campaignId: 'ef592bd8-edd8-42a0-95c0-0e97d26d8045', - }, - next_action: null, - on_behalf_of: null, - payment_method: 'pm_1LNziyKApGjVGa9tOR1sWkMV', - payment_method_options: { - card: { - installments: null, - mandate_options: null, - network: null, - request_three_d_secure: 'automatic', - }, - }, - payment_method_types: ['card'], - processing: null, - receipt_email: 'test@gmail.com', - review: null, - setup_future_usage: null, - shipping: null, - source: null, - statement_descriptor: null, - statement_descriptor_suffix: null, - status: 'succeeded', - transfer_data: null, - transfer_group: null, - } - - const billingDetails = getPaymentData(mockPaymentIntentCreated) - expect(billingDetails.netAmount).toEqual(10000) - expect(billingDetails.chargedAmount).toEqual(10350) -}) - -it('accept payment-intent.succeeded with GB tax included in charge', async () => { - const mockPaymentIntentCreated: Stripe.PaymentIntent = { - id: 'pi_3LO0M5KApGjVGa9t07SXIaeQ', - object: 'payment_intent', - amount: 51333, - amount_capturable: 0, - amount_details: { - tip: {}, - }, - amount_received: 51333, - application: null, - application_fee_amount: null, - automatic_payment_methods: null, - canceled_at: null, - cancellation_reason: null, - capture_method: 'automatic', - charges: { - object: 'list', - data: [ - { - id: 'ch_3LO0M5KApGjVGa9t0KGO6jEG', - object: 'charge', - amount: 51333, - amount_captured: 51333, - amount_refunded: 0, - application: null, - application_fee: null, - application_fee_amount: null, - balance_transaction: 'txn_3LO0M5KApGjVGa9t0nyzXKN6', - billing_details: { - address: { - city: null, - country: 'BG', - line1: null, - line2: null, - postal_code: null, - state: null, - }, - email: 'test@gmail.com', - name: 'uk card', - phone: null, - }, - calculated_statement_descriptor: 'PODKREPI.BG', - captured: true, - created: 1658413695, - currency: 'bgn', - customer: 'cus_M6ClvMHGb5Y4LI', - 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: { - campaignId: 'ef592bd8-edd8-42a0-95c0-0e97d26d8045', - }, - on_behalf_of: null, - outcome: { - network_status: 'approved_by_network', - reason: null, - risk_level: 'normal', - risk_score: 13, - seller_message: 'Payment complete.', - type: 'authorized', - }, - paid: true, - payment_intent: 'pi_3LO0M5KApGjVGa9t07SXIaeQ', - payment_method: 'pm_1LO0MLKApGjVGa9tT5zcUHVU', - payment_method_details: { - card: { - brand: 'visa', - checks: { - address_line1_check: null, - address_postal_code_check: null, - cvc_check: 'pass', - }, - country: 'GB', - exp_month: 12, - exp_year: 2031, - fingerprint: '4rDyVIWfTHNh1yf5', - funding: 'debit', - installments: null, - last4: '0005', - mandate: null, - network: 'visa', - three_d_secure: null, - wallet: null, - }, - type: 'card', - }, - receipt_email: 'test@gmail.com', - receipt_number: null, - receipt_url: 'https://pay.stripe.com/receipts/', - refunded: false, - refunds: { - object: 'list', - data: [], - has_more: false, - url: '/v1/charges/ch_3LO0M5KApGjVGa9t0KGO6jEG/refunds', - }, - review: null, - shipping: null, - source: null, - source_transfer: null, - statement_descriptor: null, - statement_descriptor_suffix: null, - status: 'succeeded', - transfer_data: null, - transfer_group: null, - }, - ], - has_more: false, - url: '/v1/charges?payment_intent=pi_3LO0M5KApGjVGa9t07SXIaeQ', - }, - client_secret: null, - confirmation_method: 'automatic', - created: 1658413677, - currency: 'bgn', - customer: 'cus_M6ClvMHGb5Y4LI', - description: null, - invoice: null, - last_payment_error: null, - livemode: false, - metadata: { - campaignId: 'ef592bd8-edd8-42a0-95c0-0e97d26d8045', - }, - next_action: null, - on_behalf_of: null, - payment_method: 'pm_1LO0MLKApGjVGa9tT5zcUHVU', - payment_method_options: { - card: { - installments: null, - mandate_options: null, - network: null, - request_three_d_secure: 'automatic', - }, - }, - payment_method_types: ['card'], - processing: null, - receipt_email: 'test@gmail.com', - review: null, - setup_future_usage: null, - shipping: null, - source: null, - statement_descriptor: null, - statement_descriptor_suffix: null, - status: 'succeeded', - transfer_data: null, - transfer_group: null, - } - - const billingDetails = getPaymentData(mockPaymentIntentCreated) - expect(billingDetails.netAmount).toEqual(50000) - expect(billingDetails.chargedAmount).toEqual(51333) -}) diff --git a/apps/api/src/donations/events/stripe-payment.service.spec.ts b/apps/api/src/donations/events/stripe-payment.service.spec.ts new file mode 100644 index 000000000..58544aa64 --- /dev/null +++ b/apps/api/src/donations/events/stripe-payment.service.spec.ts @@ -0,0 +1,229 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { ConfigService } from '@nestjs/config' +import { CampaignService } from '../../campaign/campaign.service' +import { StripePaymentService } from './stripe-payment.service' +import { getPaymentData } from '../helpers/payment-intent-helpers' +import Stripe from 'stripe' +import { VaultService } from '../../vault/vault.service' +import { PersonService } from '../../person/person.service' +import { MockPrismaService } from '../../prisma/prisma-client.mock' +import { INestApplication } from '@nestjs/common' +import request from 'supertest' +import { StripeModule, StripeModuleConfig, StripePayloadService } from '@golevelup/nestjs-stripe' + +import { + campaignId, + mockedCampaign, + mockPaymentEventCancelled, + mockPaymentEventCreated, + mockPaymentEventSucceeded, + mockPaymentIntentCreated, + mockPaymentIntentBGIncluded, + mockPaymentIntentBGIncludedNot, + mockPaymentIntentUSIncluded, + mockPaymentIntentUKIncluded, +} from './stripe-payment.testdata' +import { DonationStatus } from '@prisma/client' + +const defaultStripeWebhookEndpoint = '/stripe/webhook' +const stripeSecret = 'wh_123' + +describe('StripePaymentService', () => { + let stripePaymentService: StripePaymentService + let app: INestApplication + let hydratePayloadFn: jest.SpyInstance + const stripe = new Stripe(stripeSecret, { apiVersion: '2020-08-27' }) + + const moduleConfig: StripeModuleConfig = { + apiKey: stripeSecret, + webhookConfig: { + stripeWebhookSecret: stripeSecret, + loggingConfiguration: { + logMatchingEventHandlers: true, + }, + }, + } + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + StripeModule.forRootAsync(StripeModule, { + useFactory: () => moduleConfig, + }), + ], + providers: [ + ConfigService, + StripePaymentService, + CampaignService, + MockPrismaService, + VaultService, + PersonService, + ], + }).compile() + + app = module.createNestApplication() + await app.init() + + stripePaymentService = app.get(StripePaymentService) + + //this intercepts the request raw body and removes the exact signature check + const stripePayloadService = app.get(StripePayloadService) + + jest + .spyOn(stripePayloadService, 'tryHydratePayload') + .mockImplementation((_sig, buff) => buff as any) + }) + + afterEach(() => jest.resetAllMocks()) + + it('should be defined', () => { + expect(stripePaymentService).toBeDefined() + }) + + it('should handle payment_intent.created', () => { + const payloadString = JSON.stringify(mockPaymentEventCreated, null, 2) + + const header = stripe.webhooks.generateTestHeaderString({ + payload: payloadString, + secret: stripeSecret, + }) + + const campaignService = app.get(CampaignService) + const mockedCampaignById = jest + .spyOn(campaignService, 'getCampaignById') + .mockImplementation(() => Promise.resolve(mockedCampaign)) + + const paymentData = getPaymentData(mockPaymentEventCreated.data.object as Stripe.PaymentIntent) + + const mockedupdateDonationPayment = jest + .spyOn(campaignService, 'updateDonationPayment') + .mockImplementation(() => Promise.resolve()) + .mockName('updateDonationPayment') + + return request(app.getHttpServer()) + .post(defaultStripeWebhookEndpoint) + .set('stripe-signature', header) + .type('json') + .send(payloadString) + .expect(201) + .then(() => { + expect(mockedCampaignById).toHaveBeenCalledWith(campaignId) //campaignId from the Stripe Event + expect(mockedupdateDonationPayment).toHaveBeenCalledWith( + mockedCampaign, + paymentData, + DonationStatus.waiting, + ) + }) + }) + + it('should handle payment_intent.canceled', () => { + const payloadString = JSON.stringify(mockPaymentEventCancelled, null, 2) + + const header = stripe.webhooks.generateTestHeaderString({ + payload: payloadString, + secret: stripeSecret, + }) + + const campaignService = app.get(CampaignService) + const mockedCampaignById = jest + .spyOn(campaignService, 'getCampaignById') + .mockImplementation(() => Promise.resolve(mockedCampaign)) + + const paymentData = getPaymentData( + mockPaymentEventCancelled.data.object as Stripe.PaymentIntent, + ) + + const mockedupdateDonationPayment = jest + .spyOn(campaignService, 'updateDonationPayment') + .mockImplementation(() => Promise.resolve()) + .mockName('updateDonationPayment') + + return request(app.getHttpServer()) + .post(defaultStripeWebhookEndpoint) + .set('stripe-signature', header) + .type('json') + .send(payloadString) + .expect(201) + .then(() => { + expect(mockedCampaignById).toHaveBeenCalledWith(campaignId) //campaignId from the Stripe Event + expect(mockedupdateDonationPayment).toHaveBeenCalledWith( + mockedCampaign, + paymentData, + DonationStatus.cancelled, + ) + }) + }) + + it('should handle payment_intent.succeeded', () => { + const payloadString = JSON.stringify(mockPaymentEventSucceeded, null, 2) + + const header = stripe.webhooks.generateTestHeaderString({ + payload: payloadString, + secret: stripeSecret, + }) + + const campaignService = app.get(CampaignService) + const mockedCampaignById = jest + .spyOn(campaignService, 'getCampaignById') + .mockImplementation(() => Promise.resolve(mockedCampaign)) + + const paymentData = getPaymentData( + mockPaymentEventSucceeded.data.object as Stripe.PaymentIntent, + ) + + const mockedupdateDonationPayment = jest + .spyOn(campaignService, 'updateDonationPayment') + .mockImplementation(() => Promise.resolve()) + .mockName('updateDonationPayment') + + const mockedDonateToCampaign = jest + .spyOn(campaignService, 'donateToCampaign') + .mockName('donateToCampaign') + + return request(app.getHttpServer()) + .post(defaultStripeWebhookEndpoint) + .set('stripe-signature', header) + .type('json') + .send(payloadString) + .expect(201) + .then(() => { + expect(mockedCampaignById).toHaveBeenCalledWith(campaignId) //campaignId from the Stripe Event + expect(mockedDonateToCampaign).toHaveBeenCalledWith(mockedCampaign, paymentData) + expect(mockedupdateDonationPayment).toHaveBeenCalledWith( + mockedCampaign, + paymentData, + DonationStatus.succeeded, + ) + }) + }) + + it('calculate payment-intent.created', async () => { + const billingDetails = getPaymentData(mockPaymentIntentCreated) + expect(billingDetails.netAmount).toEqual(0) + expect(billingDetails.chargedAmount).toEqual(1065) + }) + + it('calculate payment-intent.succeeded with BG tax included in charge', async () => { + const billingDetails = getPaymentData(mockPaymentIntentBGIncluded) + expect(billingDetails.netAmount).toEqual(1000) + expect(billingDetails.chargedAmount).toEqual(1065) + }) +}) + +it('calculate payment-intent.succeeded with BG tax not included in charge', async () => { + const billingDetails = getPaymentData(mockPaymentIntentBGIncludedNot) + expect(billingDetails.netAmount).toEqual(936) + expect(billingDetails.chargedAmount).toEqual(1000) +}) + +it('calculate payment-intent.succeeded with US tax included in charge', async () => { + const billingDetails = getPaymentData(mockPaymentIntentUSIncluded) + expect(billingDetails.netAmount).toEqual(10000) + expect(billingDetails.chargedAmount).toEqual(10350) +}) + +it('calculate payment-intent.succeeded with GB tax included in charge', async () => { + const billingDetails = getPaymentData(mockPaymentIntentUKIncluded) + expect(billingDetails.netAmount).toEqual(50000) + expect(billingDetails.chargedAmount).toEqual(51333) +}) diff --git a/apps/api/src/donations/events/stripe-payment.service.ts b/apps/api/src/donations/events/stripe-payment.service.ts new file mode 100644 index 000000000..f4ef0ed92 --- /dev/null +++ b/apps/api/src/donations/events/stripe-payment.service.ts @@ -0,0 +1,115 @@ +import Stripe from 'stripe' +import { BadRequestException, Injectable, Logger } from '@nestjs/common' +import { StripeWebhookHandler } from '@golevelup/nestjs-stripe' + +import { DonationMetadata } from '../dontation-metadata.interface' +import { CampaignService } from '../../campaign/campaign.service' +import { getPaymentData } from '../helpers/payment-intent-helpers' +import { DonationStatus } from '@prisma/client' + +/** Testing Stripe on localhost is described here: + * https://github.com/podkrepi-bg/api/blob/master/TESTING.md#testing-stripe + */ +@Injectable() +export class StripePaymentService { + constructor(private campaignService: CampaignService) {} + + @StripeWebhookHandler('payment_intent.created') + async handlePaymentIntentCreated(event: Stripe.Event) { + const paymentIntent: Stripe.PaymentIntent = event.data.object as Stripe.PaymentIntent + + Logger.debug( + '[ handlePaymentIntentCreated ]', + paymentIntent, + paymentIntent.metadata as DonationMetadata, + ) + + const metadata: DonationMetadata = paymentIntent.metadata as DonationMetadata + if (!metadata.campaignId) { + throw new BadRequestException( + 'Payment intent metadata does not contain target campaignId. Probably wrong session initiation. Payment intent is: ' + + paymentIntent.id, + ) + } + + const campaign = await this.campaignService.getCampaignById(metadata.campaignId) + + if (campaign.currency !== paymentIntent.currency.toUpperCase()) { + throw new BadRequestException( + `Donation in different currency is not allowed. Campaign currency ${ + campaign.currency + } <> donation currency ${paymentIntent.currency.toUpperCase()}`, + ) + } + + const billingDetails = getPaymentData(paymentIntent) + + /* + * Handle the create event + */ + await this.campaignService.updateDonationPayment( + campaign, + billingDetails, + DonationStatus.waiting, + ) + } + + @StripeWebhookHandler('payment_intent.canceled') + async handlePaymentIntentCancelled(event: Stripe.Event) { + const paymentIntent: Stripe.PaymentIntent = event.data.object as Stripe.PaymentIntent + Logger.log( + '[ handlePaymentIntentCancelled ]', + paymentIntent, + paymentIntent.metadata as DonationMetadata, + ) + + const metadata: DonationMetadata = paymentIntent.metadata as DonationMetadata + if (!metadata.campaignId) { + throw new BadRequestException( + 'Payment intent metadata does not contain target campaignId. Probably wrong session initiation. Payment intent is: ' + + paymentIntent.id, + ) + } + + const campaign = await this.campaignService.getCampaignById(metadata.campaignId) + + const billingData = getPaymentData(paymentIntent) + await this.campaignService.updateDonationPayment( + campaign, + billingData, + DonationStatus.cancelled, + ) + } + + @StripeWebhookHandler('payment_intent.succeeded') + async handlePaymentIntentSucceeded(event: Stripe.Event) { + const paymentIntent: Stripe.PaymentIntent = event.data.object as Stripe.PaymentIntent + Logger.log( + '[ handlePaymentIntentSucceeded ]', + paymentIntent, + paymentIntent.metadata as DonationMetadata, + ) + + const metadata: DonationMetadata = paymentIntent.metadata as DonationMetadata + if (!metadata.campaignId) { + throw new BadRequestException( + 'Payment intent metadata does not contain target campaignId. Probably wrong session initiation. Payment intent is: ' + + paymentIntent.id, + ) + } + + const campaign = await this.campaignService.getCampaignById(metadata.campaignId) + + if (campaign.currency !== paymentIntent.currency.toUpperCase()) { + throw new BadRequestException( + `Donation in different currency is not allowed. Campaign currency ${ + campaign.currency + } <> donation currency ${paymentIntent.currency.toUpperCase()}`, + ) + } + + const billingData = getPaymentData(paymentIntent) + + await this.campaignService.donateToCampaign(campaign, billingData) + } +} diff --git a/apps/api/src/donations/events/stripe-payment.testdata.ts b/apps/api/src/donations/events/stripe-payment.testdata.ts new file mode 100644 index 000000000..8e67fa2dd --- /dev/null +++ b/apps/api/src/donations/events/stripe-payment.testdata.ts @@ -0,0 +1,1018 @@ +import { Campaign, CampaignState } from '@prisma/client' +import Stripe from 'stripe' + +export const campaignId = '4c1616b0-1284-4b7d-8b89-9098e7ded2c4' + +export const mockedCampaign: Campaign = { + id: campaignId, + slug: 'test-campaign', + state: CampaignState.active, + title: 'test-campaigns', + paymentReference: 'test-campaign', + essence: 'test-campaign', + coordinatorId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + organizerId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + beneficiaryId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + approvedById: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + campaignTypeId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + targetAmount: 1000000, + currency: 'BGN', + allowDonationOnComplete: true, + startDate: new Date(), + endDate: null, + description: 'test campaign', + createdAt: new Date(), + updatedAt: null, + deletedAt: null, +} + +export const mockPaymentEventCreated: Stripe.Event = { + id: 'evt_3LVL6OKApGjVGa9t0FRX71DK', + object: 'event', + api_version: '2020-08-27', + created: 1660161724, + data: { + object: { + id: 'pi_3LNwijKApGjVGa9t1F9QYd5s', + object: 'payment_intent', + amount: 1065, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 0, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: null, + cancellation_reason: null, + capture_method: 'automatic', + charges: { + object: 'list', + data: [], + has_more: false, + url: '/v1/charges?payment_intent=pi_3LNwijKApGjVGa9t1F9QYd5s', + }, + client_secret: null, + confirmation_method: 'automatic', + created: 1658399705, + currency: 'bgn', + customer: null, + description: null, + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: campaignId, + }, + next_action: null, + on_behalf_of: null, + payment_method: null, + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: null, + review: null, + setup_future_usage: null, + shipping: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'requires_payment_method', + transfer_data: null, + transfer_group: null, + }, + }, + livemode: true, + pending_webhooks: 1, + request: { + id: 'req_7euTKmWEGWWBSy', + idempotency_key: 'd2294891-8bf7-4955-8d78-e6e54e306eec', + }, + type: 'payment_intent.created', +} + +export const mockPaymentEventCancelled: Stripe.Event = { + id: 'evt_3LUzB4KApGjVGa9t0lyGsAk8', + object: 'event', + api_version: '2020-08-27', + created: 1660163846, + data: { + object: { + id: 'pi_3LUzB4KApGjVGa9t0NGvE94K', + object: 'payment_intent', + amount: 1065, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 0, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: 1660163846, + cancellation_reason: 'automatic', + capture_method: 'automatic', + charges: { + object: 'list', + data: [], + has_more: false, + total_count: 0, + url: '/v1/charges?payment_intent=pi_3LUzB4KApGjVGa9t0NGvE94K', + }, + client_secret: 'pi', + confirmation_method: 'automatic', + created: 1660077446, + currency: 'bgn', + customer: null, + description: null, + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: campaignId, + }, + next_action: null, + on_behalf_of: null, + payment_method: null, + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: null, + review: null, + setup_future_usage: null, + shipping: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'canceled', + transfer_data: null, + transfer_group: null, + }, + }, + livemode: false, + pending_webhooks: 1, + request: { + id: null, + idempotency_key: '8a2367c4-45bc-40a0-86c0-cff4c2229bec', + }, + type: 'payment_intent.canceled', +} + +export const mockPaymentEventSucceeded: Stripe.Event = { + id: 'evt_3LTS7pKApGjVGa9t1kScg2Sl', + object: 'event', + api_version: '2020-08-27', + created: 1659712067, + data: { + object: { + id: 'pi_3LTS7pKApGjVGa9t1TOCL7Fm', + object: 'payment_intent', + amount: 2000, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 2000, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: null, + cancellation_reason: null, + capture_method: 'automatic', + charges: { + object: 'list', + data: [ + { + id: 'ch_3LTS7pKApGjVGa9t1DYHNd5O', + object: 'charge', + amount: 2000, + amount_captured: 2000, + amount_refunded: 0, + application: null, + application_fee: null, + application_fee_amount: null, + balance_transaction: 'txn_3LTS7pKApGjVGa9t1Ucmz2e7', + billing_details: { + address: { + city: null, + country: null, + line1: null, + line2: null, + postal_code: null, + state: null, + }, + email: null, + name: null, + phone: null, + }, + calculated_statement_descriptor: 'PODKREPI.BG', + captured: true, + created: 1659712066, + currency: 'bgn', + customer: null, + description: '(created by Stripe CLI)', + destination: null, + dispute: null, + disputed: false, + failure_balance_transaction: null, + failure_code: null, + failure_message: null, + fraud_details: {}, + invoice: null, + livemode: false, + metadata: { + campaignId: campaignId, + }, + on_behalf_of: null, + order: null, + outcome: { + network_status: 'approved_by_network', + reason: null, + risk_level: 'normal', + risk_score: 56, + seller_message: 'Payment complete.', + type: 'authorized', + }, + paid: true, + payment_intent: 'pi_3LTS7pKApGjVGa9t1TOCL7Fm', + payment_method: 'pm_1LTS7pKApGjVGa9temSutZsY', + payment_method_details: { + card: { + brand: 'visa', + checks: { + address_line1_check: null, + address_postal_code_check: null, + cvc_check: null, + }, + country: 'US', + exp_month: 8, + exp_year: 2023, + fingerprint: 'test', + funding: 'credit', + installments: null, + last4: '4242', + mandate: null, + network: 'visa', + three_d_secure: null, + wallet: null, + }, + type: 'card', + }, + receipt_email: null, + receipt_number: null, + receipt_url: 'https://pay.stripe.com/receipts/', + refunded: false, + refunds: { + object: 'list', + data: [], + has_more: false, + total_count: 0, + url: '/v1/charges/ch_3LT/refunds', + }, + review: null, + source: null, + source_transfer: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, + }, + ], + has_more: false, + total_count: 1, + url: '/v1/charges?payment_intent=pi_3LTS7pKApGjVGa9t1TOCL7Fm', + }, + client_secret: 'test', + confirmation_method: 'automatic', + created: 1659712065, + currency: 'bgn', + customer: null, + description: '(created by Stripe CLI)', + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: campaignId, + }, + next_action: null, + on_behalf_of: null, + payment_method: 'pm_1LTS7pKApGjVGa9temSutZsY', + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: null, + review: null, + setup_future_usage: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, + }, + }, + livemode: false, + pending_webhooks: 1, + request: { + id: 'req_Qu7euzq1m8tuIF', + idempotency_key: 'e9653991-289c-449f-8544-ad0744ccc803', + }, + type: 'payment_intent.succeeded', +} + +export const mockPaymentIntentCreated: Stripe.PaymentIntent = { + id: 'pi_3LNwijKApGjVGa9t1F9QYd5s', + object: 'payment_intent', + amount: 1065, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 0, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: null, + cancellation_reason: null, + capture_method: 'automatic', + charges: { + object: 'list', + data: [], + has_more: false, + url: '/v1/charges?payment_intent=pi_3LNwijKApGjVGa9t1F9QYd5s', + }, + client_secret: null, + confirmation_method: 'automatic', + created: 1658399705, + currency: 'bgn', + customer: null, + description: null, + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + }, + next_action: null, + on_behalf_of: null, + payment_method: null, + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: null, + review: null, + setup_future_usage: null, + shipping: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'requires_payment_method', + transfer_data: null, + transfer_group: null, +} + +export const mockPaymentIntentBGIncluded: Stripe.PaymentIntent = { + id: 'pi_3LNwijKApGjVGa9t1F9QYd5s', + object: 'payment_intent', + amount: 1065, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 1065, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: null, + cancellation_reason: null, + capture_method: 'automatic', + charges: { + object: 'list', + data: [ + { + id: 'ch_3LNwijKApGjVGa9t1tuRzvbL', + object: 'charge', + amount: 1065, + amount_captured: 1065, + amount_refunded: 0, + application: null, + application_fee: null, + application_fee_amount: null, + balance_transaction: 'txn_3LNwijKApGjVGa9t100xnggj', + billing_details: { + address: { + city: null, + country: 'BG', + line1: null, + line2: null, + postal_code: null, + state: null, + }, + email: 'test@gmail.com', + name: 'First Last', + phone: null, + }, + calculated_statement_descriptor: 'PODKREPI.BG', + captured: true, + created: 1658399779, + currency: 'bgn', + customer: 'cus_M691kVNYuUp4po', + 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: { + campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + }, + on_behalf_of: null, + outcome: { + network_status: 'approved_by_network', + reason: null, + risk_level: 'normal', + risk_score: 33, + seller_message: 'Payment complete.', + type: 'authorized', + }, + paid: true, + payment_intent: 'pi_3LNwijKApGjVGa9t1F9QYd5s', + payment_method: 'pm_1LNwjtKApGjVGa9thtth9iu7', + payment_method_details: { + card: { + brand: 'visa', + checks: { + address_line1_check: null, + address_postal_code_check: null, + cvc_check: 'pass', + }, + country: 'BG', + exp_month: 4, + exp_year: 2024, + fingerprint: 'iCySKWAAAZGp2hwr', + funding: 'credit', + installments: null, + last4: '0000', + mandate: null, + network: 'visa', + three_d_secure: null, + wallet: null, + }, + type: 'card', + }, + receipt_email: 'test@gmail.com', + receipt_number: null, + receipt_url: 'https://pay.stripe.com/receipts/', + refunded: false, + refunds: { + object: 'list', + data: [], + has_more: false, + url: '/v1/charges/ch_3LNwijKApGjVGa9t1tuRzvbL/refunds', + }, + review: null, + shipping: null, + source: null, + source_transfer: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, + }, + ], + has_more: false, + url: '/v1/charges?payment_intent=pi_3LNwijKApGjVGa9t1F9QYd5s', + }, + client_secret: 'xxx', + confirmation_method: 'automatic', + created: 1658399705, + currency: 'bgn', + customer: 'cus_M691kVNYuUp4po', + description: null, + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + }, + next_action: null, + on_behalf_of: null, + payment_method: 'pm_1LNwjtKApGjVGa9thtth9iu7', + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: 'test@gmail.com', + review: null, + setup_future_usage: null, + shipping: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, +} + +export const mockPaymentIntentBGIncludedNot: Stripe.PaymentIntent = { + id: 'pi_3LNwkHKApGjVGa9t1TLyVofD', + object: 'payment_intent', + amount: 1000, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 1000, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: null, + cancellation_reason: null, + capture_method: 'automatic', + charges: { + object: 'list', + data: [ + { + id: 'ch_3LNwkHKApGjVGa9t1bkp20zi', + object: 'charge', + amount: 1000, + amount_captured: 1000, + amount_refunded: 0, + application: null, + application_fee: null, + application_fee_amount: null, + balance_transaction: 'txn_3LNwkHKApGjVGa9t1EH1EZxk', + billing_details: { + address: { + city: null, + country: 'BG', + line1: null, + line2: null, + postal_code: null, + state: null, + }, + email: 'test@gmail.com', + name: 'nepokriti', + phone: null, + }, + calculated_statement_descriptor: 'PODKREPI.BG', + captured: true, + created: 1658399823, + currency: 'bgn', + customer: 'cus_M692d4eal3rlWR', + 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: { + campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + }, + on_behalf_of: null, + outcome: { + network_status: 'approved_by_network', + reason: null, + risk_level: 'normal', + risk_score: 20, + seller_message: 'Payment complete.', + type: 'authorized', + }, + paid: true, + payment_intent: 'pi_3LNwkHKApGjVGa9t1TLyVofD', + payment_method: 'pm_1LNwkbKApGjVGa9tmWVdg46e', + payment_method_details: { + card: { + brand: 'visa', + checks: { + address_line1_check: null, + address_postal_code_check: null, + cvc_check: 'pass', + }, + country: 'BG', + exp_month: 4, + exp_year: 2032, + fingerprint: 'iCySKWAAAZGp2hwr', + funding: 'credit', + installments: null, + last4: '0000', + mandate: null, + network: 'visa', + three_d_secure: null, + wallet: null, + }, + type: 'card', + }, + receipt_email: 'test@gmail.com', + receipt_number: null, + receipt_url: 'https://pay.stripe.com/receipts/', + refunded: false, + refunds: { + object: 'list', + data: [], + has_more: false, + url: '/v1/charges/ch_3LNwkHKApGjVGa9t1bkp20zi/refunds', + }, + review: null, + shipping: null, + source: null, + source_transfer: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, + }, + ], + has_more: false, + url: '/v1/charges?payment_intent=pi_3LNwkHKApGjVGa9t1TLyVofD', + }, + client_secret: null, + confirmation_method: 'automatic', + created: 1658399801, + currency: 'bgn', + customer: 'cus_M692d4eal3rlWR', + description: null, + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: '4c1616b0-1284-4b7d-8b89-9098e7ded2c4', + }, + next_action: null, + on_behalf_of: null, + payment_method: 'pm_1LNwkbKApGjVGa9tmWVdg46e', + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: 'test@gmail.com', + review: null, + setup_future_usage: null, + shipping: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, +} + +export const mockPaymentIntentUSIncluded: Stripe.PaymentIntent = { + id: 'pi_3LNziFKApGjVGa9t0sfUl30h', + object: 'payment_intent', + amount: 10350, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 10350, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: null, + cancellation_reason: null, + capture_method: 'automatic', + charges: { + object: 'list', + data: [ + { + id: 'ch_3LNziFKApGjVGa9t07WB0NNl', + object: 'charge', + amount: 10350, + amount_captured: 10350, + amount_refunded: 0, + application: null, + application_fee: null, + application_fee_amount: null, + balance_transaction: 'txn_3LNziFKApGjVGa9t0H3v9oKL', + billing_details: { + address: { + city: null, + country: 'BG', + line1: null, + line2: null, + postal_code: null, + state: null, + }, + email: 'test@gmail.com', + name: '42424242', + phone: null, + }, + calculated_statement_descriptor: 'PODKREPI.BG', + captured: true, + created: 1658411254, + currency: 'bgn', + customer: 'cus_M6C76vpsFglyGh', + 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: { + campaignId: 'ef592bd8-edd8-42a0-95c0-0e97d26d8045', + }, + on_behalf_of: null, + outcome: { + network_status: 'approved_by_network', + reason: null, + risk_level: 'normal', + risk_score: 56, + seller_message: 'Payment complete.', + type: 'authorized', + }, + paid: true, + payment_intent: 'pi_3LNziFKApGjVGa9t0sfUl30h', + payment_method: 'pm_1LNziyKApGjVGa9tOR1sWkMV', + payment_method_details: { + card: { + brand: 'visa', + checks: { + address_line1_check: null, + address_postal_code_check: null, + cvc_check: 'pass', + }, + country: 'US', + exp_month: 4, + exp_year: 2024, + fingerprint: '2BUDwUpZNgnepjrE', + funding: 'credit', + installments: null, + last4: '4242', + mandate: null, + network: 'visa', + three_d_secure: null, + wallet: null, + }, + type: 'card', + }, + receipt_email: 'test@gmail.com', + receipt_number: null, + receipt_url: 'https://pay.stripe.com/receipts/', + refunded: false, + refunds: { + object: 'list', + data: [], + has_more: false, + url: '/v1/charges/ch_3LNziFKApGjVGa9t07WB0NNl/refunds', + }, + review: null, + shipping: null, + source: null, + source_transfer: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, + }, + ], + has_more: false, + url: '/v1/charges?payment_intent=pi_3LNziFKApGjVGa9t0sfUl30h', + }, + client_secret: null, + confirmation_method: 'automatic', + created: 1658411207, + currency: 'bgn', + customer: 'cus_M6C76vpsFglyGh', + description: null, + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: 'ef592bd8-edd8-42a0-95c0-0e97d26d8045', + }, + next_action: null, + on_behalf_of: null, + payment_method: 'pm_1LNziyKApGjVGa9tOR1sWkMV', + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: 'test@gmail.com', + review: null, + setup_future_usage: null, + shipping: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, +} + +export const mockPaymentIntentUKIncluded: Stripe.PaymentIntent = { + id: 'pi_3LO0M5KApGjVGa9t07SXIaeQ', + object: 'payment_intent', + amount: 51333, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 51333, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: null, + cancellation_reason: null, + capture_method: 'automatic', + charges: { + object: 'list', + data: [ + { + id: 'ch_3LO0M5KApGjVGa9t0KGO6jEG', + object: 'charge', + amount: 51333, + amount_captured: 51333, + amount_refunded: 0, + application: null, + application_fee: null, + application_fee_amount: null, + balance_transaction: 'txn_3LO0M5KApGjVGa9t0nyzXKN6', + billing_details: { + address: { + city: null, + country: 'BG', + line1: null, + line2: null, + postal_code: null, + state: null, + }, + email: 'test@gmail.com', + name: 'uk card', + phone: null, + }, + calculated_statement_descriptor: 'PODKREPI.BG', + captured: true, + created: 1658413695, + currency: 'bgn', + customer: 'cus_M6ClvMHGb5Y4LI', + 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: { + campaignId: 'ef592bd8-edd8-42a0-95c0-0e97d26d8045', + }, + on_behalf_of: null, + outcome: { + network_status: 'approved_by_network', + reason: null, + risk_level: 'normal', + risk_score: 13, + seller_message: 'Payment complete.', + type: 'authorized', + }, + paid: true, + payment_intent: 'pi_3LO0M5KApGjVGa9t07SXIaeQ', + payment_method: 'pm_1LO0MLKApGjVGa9tT5zcUHVU', + payment_method_details: { + card: { + brand: 'visa', + checks: { + address_line1_check: null, + address_postal_code_check: null, + cvc_check: 'pass', + }, + country: 'GB', + exp_month: 12, + exp_year: 2031, + fingerprint: '4rDyVIWfTHNh1yf5', + funding: 'debit', + installments: null, + last4: '0005', + mandate: null, + network: 'visa', + three_d_secure: null, + wallet: null, + }, + type: 'card', + }, + receipt_email: 'test@gmail.com', + receipt_number: null, + receipt_url: 'https://pay.stripe.com/receipts/', + refunded: false, + refunds: { + object: 'list', + data: [], + has_more: false, + url: '/v1/charges/ch_3LO0M5KApGjVGa9t0KGO6jEG/refunds', + }, + review: null, + shipping: null, + source: null, + source_transfer: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, + }, + ], + has_more: false, + url: '/v1/charges?payment_intent=pi_3LO0M5KApGjVGa9t07SXIaeQ', + }, + client_secret: null, + confirmation_method: 'automatic', + created: 1658413677, + currency: 'bgn', + customer: 'cus_M6ClvMHGb5Y4LI', + description: null, + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: 'ef592bd8-edd8-42a0-95c0-0e97d26d8045', + }, + next_action: null, + on_behalf_of: null, + payment_method: 'pm_1LO0MLKApGjVGa9tT5zcUHVU', + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: 'test@gmail.com', + review: null, + setup_future_usage: null, + shipping: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'succeeded', + transfer_data: null, + transfer_group: null, +} diff --git a/package.json b/package.json index 1fddfcfa9..e52b36625 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "reflect-metadata": "0.1.13", "rxjs": "7.5.5", "stripe": "^9.14.0", + "supertest": "^6.2.4", "swagger-ui-express": "^4.4.0", "tslib": "2.4.0" }, diff --git a/yarn.lock b/yarn.lock index 6695aa60b..5b142d955 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2497,6 +2497,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1.js@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -3181,6 +3186,11 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +component-emitter@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + compress-commons@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" @@ -3258,6 +3268,11 @@ cookie@^0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookiejar@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" + integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== + copy-webpack-plugin@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz#2d2c460c4c4695ec0a58afb2801a1205256c4e6b" @@ -3492,6 +3507,14 @@ detect-node@2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== +dezalgo@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ== + dependencies: + asap "^2.0.0" + wrappy "1" + dicer@0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" @@ -4063,7 +4086,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-safe-stringify@2.1.1: +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -4242,6 +4265,16 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formidable@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" + integrity sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ== + dependencies: + dezalgo "1.0.3" + hexoid "1.0.0" + once "1.4.0" + qs "6.9.3" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -4508,6 +4541,11 @@ helmet@5.0.2: resolved "https://registry.yarnpkg.com/helmet/-/helmet-5.0.2.tgz#3264ec6bab96c82deaf65e3403c369424cb2366c" integrity sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg== +hexoid@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -5952,7 +5990,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -5982,7 +6020,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.6: +mime@2.6.0, mime@^2.4.6: version "2.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== @@ -6636,7 +6674,7 @@ on-finished@2.4.1, on-finished@^2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -7055,6 +7093,11 @@ qs@6.10.3, qs@^6.10.3: dependencies: side-channel "^1.0.4" +qs@6.9.3: + version "6.9.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" + integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== + query-string@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1" @@ -7380,7 +7423,7 @@ semver@7.3.4: dependencies: lru-cache "^6.0.0" -semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: +semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== @@ -7712,6 +7755,31 @@ stripe@^9.14.0: "@types/node" ">=8.1.0" qs "^6.10.3" +superagent@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.0.tgz#2ea4587df4b81ef023ec01ebc6e1bcb9e2344cb6" + integrity sha512-iudipXEel+SzlP9y29UBWGDjB+Zzag+eeA1iLosaR2YHBRr1Q1kC29iBrF2zIVD9fqVbpZnXkN/VJmwFMVyNWg== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.10.3" + readable-stream "^3.6.0" + semver "^7.3.7" + +supertest@^6.2.4: + version "6.2.4" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.2.4.tgz#3dcebe42f7fd6f28dd7ac74c6cba881f7101b2f0" + integrity sha512-M8xVnCNv+q2T2WXVzxDECvL2695Uv2uUj2O0utxsld/HRyJvOU8W9f1gvsYxSNU4wmIe0/L/ItnpU4iKq0emDA== + dependencies: + methods "^1.1.2" + superagent "^8.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"