diff --git a/apps/api/src/stripe/stripe.controller.spec.ts b/apps/api/src/stripe/stripe.controller.spec.ts index e6690324..7338dc41 100644 --- a/apps/api/src/stripe/stripe.controller.spec.ts +++ b/apps/api/src/stripe/stripe.controller.spec.ts @@ -29,7 +29,6 @@ import { NotificationModule } from '../sockets/notifications/notification.module import { KeycloakTokenParsed } from '../auth/keycloak' describe('StripeController', () => { let controller: StripeController - const idempotencyKey = 'test_123' const stripeMock = { checkout: { sessions: { create: jest.fn() } }, paymentIntents: { retrieve: jest.fn() }, @@ -212,7 +211,7 @@ describe('StripeController', () => { }, } - await expect(controller.updateSetupIntent('123', idempotencyKey, payload)).rejects.toThrow( + await expect(controller.updateSetupIntent('123', payload)).rejects.toThrow( new NotAcceptableException('Campaign cannot accept donations in state: complete'), ) }) @@ -228,11 +227,16 @@ describe('StripeController', () => { state: CampaignState.complete, title: 'active-campaign', } as Campaign) - await expect(controller.setupIntentToSubscription('123', idempotencyKey)).toResolve() - expect(stripeMock.setupIntents.retrieve).toHaveBeenCalledWith('123', { - expand: ['payment_method'], - }) - expect(stripeMock.customers.create).not.toHaveBeenCalled() - expect(stripeMock.products.create).not.toHaveBeenCalled() + try { + + await expect(controller.setupIntentToSubscription('123')).toResolve() + expect(stripeMock.setupIntents.retrieve).toHaveBeenCalledWith('123', { + expand: ['payment_method'], + }) + expect(stripeMock.customers.create).not.toHaveBeenCalled() + expect(stripeMock.products.create).not.toHaveBeenCalled() + } catch (err) { + throw new Error(JSON.stringify(err)) + } }) }) diff --git a/apps/api/src/stripe/stripe.controller.ts b/apps/api/src/stripe/stripe.controller.ts index 4e3629f5..2a6d4436 100644 --- a/apps/api/src/stripe/stripe.controller.ts +++ b/apps/api/src/stripe/stripe.controller.ts @@ -7,18 +7,15 @@ import { Param, Patch, Post, - Query, UnauthorizedException, } from '@nestjs/common' import { ApiBody, ApiTags } from '@nestjs/swagger' -import { AuthenticatedUser, Public, RoleMatchingMode, Roles } from 'nest-keycloak-connect' +import { Public, RoleMatchingMode, Roles } from 'nest-keycloak-connect' import { CancelPaymentIntentDto } from './dto/cancel-payment-intent.dto' import { CreatePaymentIntentDto } from './dto/create-payment-intent.dto' import { UpdatePaymentIntentDto } from './dto/update-payment-intent.dto' import { UpdateSetupIntentDto } from './dto/update-setup-intent.dto' import { StripeService } from './stripe.service' -import { KeycloakTokenParsed } from '../auth/keycloak' -import { CreateSubscriptionPaymentDto } from './dto/create-subscription-payment.dto' import { EditFinancialsRequests } from '@podkrepi-bg/podkrepi-types' import { CreateSessionDto } from '../donations/dto/create-session.dto' import { PersonService } from '../person/person.service' @@ -33,8 +30,8 @@ export class StripeController { @Post('setup-intent') @Public() - createSetupIntent(@Body() body: { idempotencyKey: string }) { - return this.stripeService.createSetupIntent(body) + createSetupIntent() { + return this.stripeService.createSetupIntent() } @Post('create-checkout-session') @@ -78,12 +75,8 @@ export class StripeController { @Post('setup-intent/:id') @Public() - updateSetupIntent( - @Param('id') id: string, - @Query('idempotency-key') idempotencyKey: string, - @Body() updateSetupIntentDto: UpdateSetupIntentDto, - ) { - return this.stripeService.updateSetupIntent(id, idempotencyKey, updateSetupIntentDto) + updateSetupIntent(@Param('id') id: string, @Body() updateSetupIntentDto: UpdateSetupIntentDto) { + return this.stripeService.updateSetupIntent(id, updateSetupIntentDto) } @Patch('setup-intent/:id/cancel') @@ -97,22 +90,16 @@ export class StripeController { description: 'Create payment intent from setup intent', }) @Public() - setupIntentToPaymentIntent( - @Param('id') id: string, - @Query('idempotency-key') idempotencyKey: string, - ) { - return this.stripeService.setupIntentToPaymentIntent(id, idempotencyKey) + setupIntentToPaymentIntent(@Param('id') id: string) { + return this.stripeService.setupIntentToPaymentIntent(id) } @Post('setup-intent/:id/subscription') @ApiBody({ description: 'Create payment intent from setup intent', }) - setupIntentToSubscription( - @Param('id') id: string, - @Query('idempotency-key') idempotencyKey: string, - ) { - return this.stripeService.setupIntentToSubscription(id, idempotencyKey) + setupIntentToSubscription(@Param('id') id: string) { + return this.stripeService.setupIntentToSubscription(id) } @Post('payment-intent') diff --git a/apps/api/src/stripe/stripe.service.ts b/apps/api/src/stripe/stripe.service.ts index ee76a707..c2bfc611 100644 --- a/apps/api/src/stripe/stripe.service.ts +++ b/apps/api/src/stripe/stripe.service.ts @@ -13,6 +13,7 @@ import { ConfigService } from '@nestjs/config' import { StripeMetadata } from './stripe-metadata.interface' import { CreateStripePaymentDto } from '../donations/dto/create-stripe-payment.dto' import { RecurringDonationService } from '../recurring-donation/recurring-donation.service' +import * as crypto from 'crypto' @Injectable() export class StripeService { @@ -32,14 +33,12 @@ export class StripeService { */ async updateSetupIntent( id: string, - idempotencyKey: string, inputDto: UpdateSetupIntentDto, ): Promise> { if (!inputDto.metadata.campaignId) throw new BadRequestException('campaignId is missing from metadata') - await this.campaignService.validateCampaignId( - inputDto.metadata.campaignId as string, - ) + await this.campaignService.validateCampaignId(inputDto.metadata.campaignId as string) + const idempotencyKey = crypto.randomUUID() return await this.stripeClient.setupIntents.update(id, inputDto, { idempotencyKey }) } /** @@ -74,8 +73,9 @@ export class StripeService { async attachPaymentMethodToCustomer( paymentMethod: Stripe.PaymentMethod, customer: Stripe.Customer, - idempotencyKey: string, ) { + const idempotencyKey = crypto.randomUUID() + return await this.stripeClient.paymentMethods.attach( paymentMethod.id, { @@ -84,10 +84,7 @@ export class StripeService { { idempotencyKey: `${idempotencyKey}--pm` }, ) } - async setupIntentToPaymentIntent( - setupIntentId: string, - idempotencyKey: string, - ): Promise { + async setupIntentToPaymentIntent(setupIntentId: string): Promise { const setupIntent = await this.findSetupIntentById(setupIntentId) if (setupIntent instanceof Error) throw new BadRequestException(setupIntent.message) @@ -96,9 +93,10 @@ export class StripeService { const name = paymentMethod.billing_details.name as string const metadata = setupIntent.metadata as Stripe.Metadata - const customer = await this.createCustomer(email, name, paymentMethod, idempotencyKey) + const customer = await this.createCustomer(email, name, paymentMethod) - await this.attachPaymentMethodToCustomer(paymentMethod, customer, idempotencyKey) + await this.attachPaymentMethodToCustomer(paymentMethod, customer) + const idempotencyKey = crypto.randomUUID() const paymentIntent = await this.stripeClient.paymentIntents.create( { @@ -120,18 +118,12 @@ export class StripeService { * @param inputDto Payment intent create params * @returns {Promise>} */ - async createSetupIntent({ - idempotencyKey, - }: { - idempotencyKey: string - }): Promise> { + async createSetupIntent(): Promise> { + const idempotencyKey = crypto.randomUUID() return await this.stripeClient.setupIntents.create({}, { idempotencyKey }) } - async setupIntentToSubscription( - setupIntentId: string, - idempotencyKey: string, - ): Promise { + async setupIntentToSubscription(setupIntentId: string): Promise { const setupIntent = await this.findSetupIntentById(setupIntentId) if (setupIntent instanceof Error) throw new BadRequestException(setupIntent.message) const paymentMethod = setupIntent.payment_method as Stripe.PaymentMethod @@ -139,11 +131,11 @@ export class StripeService { const name = paymentMethod.billing_details.name as string const metadata = setupIntent.metadata as Stripe.Metadata - const customer = await this.createCustomer(email, name, paymentMethod, idempotencyKey) - await this.attachPaymentMethodToCustomer(paymentMethod, customer, idempotencyKey) + const customer = await this.createCustomer(email, name, paymentMethod) + await this.attachPaymentMethodToCustomer(paymentMethod, customer) - const product = await this.createProduct(metadata.campaignId, idempotencyKey) - return await this.createSubscription(metadata, customer, product, paymentMethod, idempotencyKey) + const product = await this.createProduct(metadata.campaignId) + return await this.createSubscription(metadata, customer, product, paymentMethod) } /** @@ -196,15 +188,11 @@ export class StripeService { } else return new Array() } - async createCustomer( - email: string, - name: string, - paymentMethod: Stripe.PaymentMethod, - idempotencyKey: string, - ) { + async createCustomer(email: string, name: string, paymentMethod: Stripe.PaymentMethod) { const customerLookup = await this.stripeClient.customers.list({ email, }) + const idempotencyKey = crypto.randomUUID() const customer = customerLookup.data[0] //Customer not found. Create new onw if (!customer) @@ -220,19 +208,23 @@ export class StripeService { return customer } - async createProduct(campaignId: string, idempotencyKey: string): Promise { + async createProduct(campaignId: string): Promise { const campaign = await this.campaignService.getCampaignById(campaignId) + const idempotencyKey = crypto.randomUUID() if (!campaign) throw new Error(`Campaign with id ${campaignId} not found`) const productLookup = await this.stripeClient.products.search({ - query: `-name:'${campaign.title}'`, + query: `metadata["campaignId"]:"${campaign.id}"`, }) - if (productLookup) return productLookup.data[0] + if (productLookup.data.length) return productLookup.data[0] return await this.stripeClient.products.create( { name: campaign.title, description: `Donate to ${campaign.title}`, + metadata: { + campaignId: campaign.id, + }, }, { idempotencyKey: `${idempotencyKey}--product` }, ) @@ -242,8 +234,9 @@ export class StripeService { customer: Stripe.Customer, product: Stripe.Product, paymentMethod: Stripe.PaymentMethod, - idempotencyKey: string, ) { + const idempotencyKey = crypto.randomUUID() + const subscription = await this.stripeClient.subscriptions.create( { customer: customer.id,