Skip to content

Commit

Permalink
feat(payments-plugin): Add option to StripePlugin to handle payment i…
Browse files Browse the repository at this point in the history
…ntent that doesn't have Vendure metadata (#3250)
  • Loading branch information
jezzzm authored Dec 3, 2024
1 parent f56ce3a commit ec934dd
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 7 deletions.
46 changes: 45 additions & 1 deletion packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Stripe } from 'stripe';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { initialData } from '../../../e2e-common/e2e-initial-data';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
import { StripePlugin } from '../src/stripe';
import { stripePaymentMethodHandler } from '../src/stripe/stripe.handler';

Expand Down Expand Up @@ -431,6 +431,50 @@ describe('Stripe payments', () => {
expect(result.status).toEqual(200);
});

// https://github.com/vendure-ecommerce/vendure/issues/3249
it('Should skip events without expected metadata, when the plugin option is set', async () => {
StripePlugin.options.skipPaymentIntentsWithoutExpectedMetadata = true;

const MOCKED_WEBHOOK_PAYLOAD = {
id: 'evt_0',
object: 'event',
api_version: '2022-11-15',
data: {
object: {
id: 'pi_0',
currency: 'usd',
metadata: {
dummy: 'not a vendure payload',
},
amount_received: 10000,
status: 'succeeded',
},
},
livemode: false,
pending_webhooks: 1,
request: {
id: 'req_0',
idempotency_key: '00000000-0000-0000-0000-000000000000',
},
type: 'payment_intent.succeeded',
};

const payloadString = JSON.stringify(MOCKED_WEBHOOK_PAYLOAD, null, 2);
const stripeWebhooks = new Stripe('test-api-secret', { apiVersion: '2023-08-16' }).webhooks;
const header = stripeWebhooks.generateTestHeaderString({
payload: payloadString,
secret: 'test-signing-secret',
});

const result = await fetch(`http://localhost:${serverPort}/payments/stripe`, {
method: 'post',
body: payloadString,
headers: { 'Content-Type': 'application/json', 'Stripe-Signature': header },
});

expect(result.status).toEqual(200);
});

// https://github.com/vendure-ecommerce/vendure/issues/1630
describe('currencies with no fractional units', () => {
let japanProductId: string;
Expand Down
14 changes: 14 additions & 0 deletions packages/payments-plugin/src/stripe/stripe-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CurrencyCode, Order } from '@vendure/core';
import Stripe from 'stripe';

/**
* @description
Expand Down Expand Up @@ -35,3 +36,16 @@ function currencyHasFractionPart(currencyCode: CurrencyCode): boolean {

return !!parts.find(p => p.type === 'fraction');
}

/**
*
* @description
* Ensures that the payment intent metadata object contains the expected properties, as defined by the plugin.
*/
export function isExpectedVendureStripeEventMetadata(metadata: Stripe.Metadata): metadata is {
channelToken: string;
orderCode: string;
orderId: string;
} {
return !!metadata.channelToken && !!metadata.orderCode && !!metadata.orderId;
}
26 changes: 21 additions & 5 deletions packages/payments-plugin/src/stripe/stripe.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Controller, Headers, HttpStatus, Post, Req, Res } from '@nestjs/common';
import { Controller, Headers, HttpStatus, Inject, Post, Req, Res } from '@nestjs/common';
import type { PaymentMethod, RequestContext } from '@vendure/core';
import { ChannelService } from '@vendure/core';
import {
ChannelService,
InternalServerError,
LanguageCode,
Logger,
Expand All @@ -15,18 +15,21 @@ import { OrderStateTransitionError } from '@vendure/core/dist/common/error/gener
import type { Response } from 'express';
import type Stripe from 'stripe';

import { loggerCtx } from './constants';
import { loggerCtx, STRIPE_PLUGIN_OPTIONS } from './constants';
import { isExpectedVendureStripeEventMetadata } from './stripe-utils';
import { stripePaymentMethodHandler } from './stripe.handler';
import { StripeService } from './stripe.service';
import { RequestWithRawBody } from './types';
import { RequestWithRawBody, StripePluginOptions } from './types';

const missingHeaderErrorMessage = 'Missing stripe-signature header';
const signatureErrorMessage = 'Error verifying Stripe webhook signature';
const noPaymentIntentErrorMessage = 'No payment intent in the event payload';
const ignorePaymentIntentEvent = 'Event has no Vendure metadata, skipped.';

@Controller('payments')
export class StripeController {
constructor(
@Inject(STRIPE_PLUGIN_OPTIONS) private options: StripePluginOptions,
private paymentMethodService: PaymentMethodService,
private orderService: OrderService,
private stripeService: StripeService,
Expand Down Expand Up @@ -56,7 +59,20 @@ export class StripeController {
return;
}

const { metadata: { channelToken, orderCode, orderId } = {} } = paymentIntent;
const { metadata } = paymentIntent;

if (!isExpectedVendureStripeEventMetadata(metadata)) {
if (this.options.skipPaymentIntentsWithoutExpectedMetadata) {
response.status(HttpStatus.OK).send(ignorePaymentIntentEvent);
return;
}
throw new Error(
`Missing expected payment intent metadata, unable to settle payment ${paymentIntent.id}!`,
);
}

const { channelToken, orderCode, orderId } = metadata;

const outerCtx = await this.createContext(channelToken, request);

await this.connection.withTransaction(outerCtx, async (ctx: RequestContext) => {
Expand Down
8 changes: 7 additions & 1 deletion packages/payments-plugin/src/stripe/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import '@vendure/core/dist/entity/custom-entity-fields';
import type { Injector, Order, RequestContext } from '@vendure/core';
import '@vendure/core/dist/entity/custom-entity-fields';
import type { Request } from 'express';
import type Stripe from 'stripe';

Expand Down Expand Up @@ -188,6 +188,12 @@ export interface StripePluginOptions {
ctx: RequestContext,
order: Order,
) => AdditionalCustomerCreateParams | Promise<AdditionalCustomerCreateParams>;
/**
* @description
* If your Stripe account also generates payment intents which are independent of Vendure orders, you can set this
* to `true` to skip processing those payment intents.
*/
skipPaymentIntentsWithoutExpectedMetadata?: boolean;
}

export interface RequestWithRawBody extends Request {
Expand Down

0 comments on commit ec934dd

Please sign in to comment.