| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601 |
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
- import { EntityHydrator, mergeConfig } from '@vendure/core';
- import {
- CreateProductMutation,
- CreateProductMutationVariables,
- CreateProductVariantsMutation,
- CreateProductVariantsMutationVariables,
- TestCreateStockLocationDocument,
- } from '@vendure/core/e2e/graphql/generated-e2e-admin-types';
- import { CREATE_PRODUCT, CREATE_PRODUCT_VARIANTS } from '@vendure/core/e2e/graphql/shared-definitions';
- import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
- import gql from 'graphql-tag';
- import nock from 'nock';
- import fetch from 'node-fetch';
- import path from 'path';
- import { Stripe } from 'stripe';
- import { afterAll, beforeAll, describe, expect, it } from 'vitest';
- import { initialData } from '../../../e2e-common/e2e-initial-data';
- import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
- import { StripePlugin } from '../src/stripe';
- import { stripePaymentMethodHandler } from '../src/stripe/stripe.handler';
- import { CREATE_CHANNEL, CREATE_PAYMENT_METHOD, GET_CUSTOMER_LIST } from './graphql/admin-queries';
- import {
- CreateChannelMutation,
- CreateChannelMutationVariables,
- CreatePaymentMethodMutation,
- CreatePaymentMethodMutationVariables,
- CurrencyCode,
- GetCustomerListQuery,
- GetCustomerListQueryVariables,
- LanguageCode,
- } from './graphql/generated-admin-types';
- import {
- AddItemToOrderMutation,
- AddItemToOrderMutationVariables,
- GetActiveOrderQuery,
- TestOrderFragmentFragment,
- } from './graphql/generated-shop-types';
- import { ADD_ITEM_TO_ORDER, GET_ACTIVE_ORDER } from './graphql/shop-queries';
- import { setShipping } from './payment-helpers';
- export const CREATE_STRIPE_PAYMENT_INTENT = gql`
- mutation createStripePaymentIntent {
- createStripePaymentIntent
- }
- `;
- describe('Stripe payments', () => {
- const devConfig = mergeConfig(testConfig(), {
- plugins: [
- StripePlugin.init({
- storeCustomersInStripe: true,
- }),
- ],
- });
- const { shopClient, adminClient, server } = createTestEnvironment(devConfig);
- let started = false;
- let customers: GetCustomerListQuery['customers']['items'];
- let order: TestOrderFragmentFragment;
- let serverPort: number;
- beforeAll(async () => {
- serverPort = devConfig.apiOptions.port;
- await server.init({
- initialData,
- productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
- customerCount: 2,
- });
- started = true;
- await adminClient.asSuperAdmin();
- ({
- customers: { items: customers },
- } = await adminClient.query<GetCustomerListQuery, GetCustomerListQueryVariables>(GET_CUSTOMER_LIST, {
- options: {
- take: 2,
- },
- }));
- }, TEST_SETUP_TIMEOUT_MS);
- afterAll(async () => {
- await server.destroy();
- });
- it('Should start successfully', () => {
- expect(started).toEqual(true);
- expect(customers).toHaveLength(2);
- });
- it('Should prepare an order', async () => {
- await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
- const { addItemToOrder } = await shopClient.query<
- AddItemToOrderMutation,
- AddItemToOrderMutationVariables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: 'T_1',
- quantity: 2,
- });
- order = addItemToOrder as TestOrderFragmentFragment;
- expect(order.code).toBeDefined();
- });
- it('Should add a Stripe paymentMethod', async () => {
- const { createPaymentMethod } = await adminClient.query<
- CreatePaymentMethodMutation,
- CreatePaymentMethodMutationVariables
- >(CREATE_PAYMENT_METHOD, {
- input: {
- code: `stripe-payment-${E2E_DEFAULT_CHANNEL_TOKEN}`,
- translations: [
- {
- name: 'Stripe payment test',
- description: 'This is a Stripe test payment method',
- languageCode: LanguageCode.en,
- },
- ],
- enabled: true,
- handler: {
- code: stripePaymentMethodHandler.code,
- arguments: [
- { name: 'apiKey', value: 'test-api-key' },
- { name: 'webhookSecret', value: 'test-signing-secret' },
- ],
- },
- },
- });
- expect(createPaymentMethod.code).toBe(`stripe-payment-${E2E_DEFAULT_CHANNEL_TOKEN}`);
- await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
- await setShipping(shopClient);
- });
- it('if no customer id exists, makes a call to create', async () => {
- let createCustomerPayload: { name: string; email: string } | undefined;
- const emptyList = { data: [] };
- nock('https://api.stripe.com/')
- .get(/\/v1\/customers.*/)
- .reply(200, emptyList);
- nock('https://api.stripe.com/')
- .post('/v1/customers', body => {
- createCustomerPayload = body;
- return true;
- })
- .reply(201, {
- id: 'new-customer-id',
- });
- nock('https://api.stripe.com/').post('/v1/payment_intents').reply(200, {
- client_secret: 'test-client-secret',
- });
- const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
- expect(createCustomerPayload).toEqual({
- email: 'hayden.zieme12@hotmail.com',
- name: 'Hayden Zieme',
- });
- });
- it('should send correct payload to create payment intent', async () => {
- let createPaymentIntentPayload: any;
- const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
- nock('https://api.stripe.com/')
- .post('/v1/payment_intents', body => {
- createPaymentIntentPayload = body;
- return true;
- })
- .reply(200, {
- client_secret: 'test-client-secret',
- });
- const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
- expect(createPaymentIntentPayload).toEqual({
- amount: activeOrder?.totalWithTax.toString(),
- currency: activeOrder?.currencyCode?.toLowerCase(),
- customer: 'new-customer-id',
- 'automatic_payment_methods[enabled]': 'true',
- 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
- 'metadata[orderId]': '1',
- 'metadata[orderCode]': activeOrder?.code,
- 'metadata[languageCode]': 'en',
- });
- expect(createStripePaymentIntent).toEqual('test-client-secret');
- });
- // https://github.com/vendurehq/vendure/issues/1935
- it('should attach metadata to stripe payment intent', async () => {
- StripePlugin.options.metadata = async (injector, ctx, currentOrder) => {
- const hydrator = injector.get(EntityHydrator);
- await hydrator.hydrate(ctx, currentOrder, { relations: ['customer'] });
- return {
- customerEmail: currentOrder.customer?.emailAddress ?? 'demo',
- };
- };
- let createPaymentIntentPayload: any;
- const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
- nock('https://api.stripe.com/')
- .post('/v1/payment_intents', body => {
- createPaymentIntentPayload = body;
- return true;
- })
- .reply(200, {
- client_secret: 'test-client-secret',
- });
- const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
- expect(createPaymentIntentPayload).toEqual({
- amount: activeOrder?.totalWithTax.toString(),
- currency: activeOrder?.currencyCode?.toLowerCase(),
- customer: 'new-customer-id',
- 'automatic_payment_methods[enabled]': 'true',
- 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
- 'metadata[orderId]': '1',
- 'metadata[orderCode]': activeOrder?.code,
- 'metadata[languageCode]': 'en',
- 'metadata[customerEmail]': customers[0].emailAddress,
- });
- expect(createStripePaymentIntent).toEqual('test-client-secret');
- StripePlugin.options.metadata = undefined;
- });
- // https://github.com/vendurehq/vendure/issues/2412
- it('should attach additional params to payment intent using paymentIntentCreateParams', async () => {
- StripePlugin.options.paymentIntentCreateParams = async (injector, ctx, currentOrder) => {
- const hydrator = injector.get(EntityHydrator);
- await hydrator.hydrate(ctx, currentOrder, { relations: ['customer'] });
- return {
- description: `Order #${currentOrder.code} for ${currentOrder.customer!.emailAddress}`,
- };
- };
- let createPaymentIntentPayload: any;
- const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
- nock('https://api.stripe.com/')
- .post('/v1/payment_intents', body => {
- createPaymentIntentPayload = body;
- return true;
- })
- .reply(200, {
- client_secret: 'test-client-secret',
- });
- const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
- expect(createPaymentIntentPayload).toEqual({
- amount: activeOrder?.totalWithTax.toString(),
- currency: activeOrder?.currencyCode?.toLowerCase(),
- customer: 'new-customer-id',
- description: `Order #${activeOrder!.code} for ${activeOrder!.customer!.emailAddress}`,
- 'automatic_payment_methods[enabled]': 'true',
- 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
- 'metadata[languageCode]': 'en',
- 'metadata[orderId]': '1',
- 'metadata[orderCode]': activeOrder?.code,
- });
- expect(createStripePaymentIntent).toEqual('test-client-secret');
- StripePlugin.options.paymentIntentCreateParams = undefined;
- });
- // https://github.com/vendurehq/vendure/issues/3183
- it('should attach additional options to payment intent using requestOptions', async () => {
- StripePlugin.options.requestOptions = (injector, ctx, currentOrder) => {
- return {
- stripeAccount: 'acct_connected',
- };
- };
- let connectedAccountHeader: any;
- let createPaymentIntentPayload: any;
- const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
- nock('https://api.stripe.com/', {
- reqheaders: {
- 'Stripe-Account': headerValue => {
- connectedAccountHeader = headerValue;
- return true;
- },
- },
- })
- .post('/v1/payment_intents', body => {
- createPaymentIntentPayload = body;
- return true;
- })
- .reply(200, {
- client_secret: 'test-client-secret',
- });
- const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
- expect(createPaymentIntentPayload).toEqual({
- amount: activeOrder?.totalWithTax.toString(),
- currency: activeOrder?.currencyCode?.toLowerCase(),
- customer: 'new-customer-id',
- 'automatic_payment_methods[enabled]': 'true',
- 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
- 'metadata[orderId]': '1',
- 'metadata[languageCode]': 'en',
- 'metadata[orderCode]': activeOrder?.code,
- });
- expect(connectedAccountHeader).toEqual('acct_connected');
- expect(createStripePaymentIntent).toEqual('test-client-secret');
- StripePlugin.options.paymentIntentCreateParams = undefined;
- });
- // https://github.com/vendurehq/vendure/issues/2412
- it('should attach additional params to customer using customerCreateParams', async () => {
- StripePlugin.options.customerCreateParams = async (injector, ctx, currentOrder) => {
- const hydrator = injector.get(EntityHydrator);
- await hydrator.hydrate(ctx, currentOrder, { relations: ['customer'] });
- return {
- description: `Description for ${currentOrder.customer!.emailAddress}`,
- phone: '12345',
- };
- };
- await shopClient.asUserWithCredentials(customers[1].emailAddress, 'test');
- const { addItemToOrder } = await shopClient.query<
- AddItemToOrderMutation,
- AddItemToOrderMutationVariables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: 'T_1',
- quantity: 2,
- });
- order = addItemToOrder as TestOrderFragmentFragment;
- let createCustomerPayload: { name: string; email: string } | undefined;
- const emptyList = { data: [] };
- nock('https://api.stripe.com/')
- .get(/\/v1\/customers.*/)
- .reply(200, emptyList);
- nock('https://api.stripe.com/')
- .post('/v1/customers', body => {
- createCustomerPayload = body;
- return true;
- })
- .reply(201, {
- id: 'new-customer-id',
- });
- nock('https://api.stripe.com/').post('/v1/payment_intents').reply(200, {
- client_secret: 'test-client-secret',
- });
- const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
- await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
- expect(createCustomerPayload).toEqual({
- email: 'trevor_donnelly96@hotmail.com',
- name: 'Trevor Donnelly',
- description: `Description for ${activeOrder!.customer!.emailAddress}`,
- phone: '12345',
- });
- });
- // https://github.com/vendurehq/vendure/issues/2450
- it('Should not crash on signature validation failure', async () => {
- const MOCKED_WEBHOOK_PAYLOAD = {
- id: 'evt_0',
- object: 'event',
- api_version: '2022-11-15',
- data: {
- object: {
- id: 'pi_0',
- currency: 'usd',
- 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 result = await fetch(`http://localhost:${serverPort}/payments/stripe`, {
- method: 'post',
- body: payloadString,
- headers: { 'Content-Type': 'application/json' },
- });
- // We didn't provided any signatures, it should result in a 400 - Bad request
- expect(result.status).toEqual(400);
- });
- // TODO: Contribution welcome: test webhook handling and order settlement
- // https://github.com/vendurehq/vendure/issues/2450
- it("Should validate the webhook's signature properly", async () => {
- await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
- const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
- order = activeOrder!;
- const MOCKED_WEBHOOK_PAYLOAD = {
- id: 'evt_0',
- object: 'event',
- api_version: '2022-11-15',
- data: {
- object: {
- id: 'pi_0',
- currency: 'usd',
- metadata: {
- orderCode: order.code,
- orderId: parseInt(order.id.replace('T_', ''), 10),
- channelToken: E2E_DEFAULT_CHANNEL_TOKEN,
- },
- amount_received: order.totalWithTax,
- 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 event = stripeWebhooks.constructEvent(payloadString, header, 'test-signing-secret');
- expect(event.id).to.equal(MOCKED_WEBHOOK_PAYLOAD.id);
- await setShipping(shopClient);
- // Due to the `this.orderService.transitionToState(...)` fails with the internal lookup by id,
- // we need to put the order into `ArrangingPayment` state manually before calling the webhook handler.
- // const transitionResult = await adminClient.query(TRANSITION_TO_ARRANGING_PAYMENT, { id: order.id });
- // expect(transitionResult.transitionOrderToState.__typename).toBe('Order')
- const result = await fetch(`http://localhost:${serverPort}/payments/stripe`, {
- method: 'post',
- body: payloadString,
- headers: { 'Content-Type': 'application/json', 'Stripe-Signature': header },
- });
- // I would expect to the status to be 200, but at the moment either the
- // `orderService.transitionToState()` or the `orderService.addPaymentToOrder()`
- // throws an error of 'error.entity-with-id-not-found'
- expect(result.status).toEqual(200);
- });
- // https://github.com/vendurehq/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/vendurehq/vendure/issues/1630
- describe('currencies with no fractional units', () => {
- let japanProductId: string;
- beforeAll(async () => {
- const JAPAN_CHANNEL_TOKEN = 'japan-channel-token';
- const { createChannel } = await adminClient.query<
- CreateChannelMutation,
- CreateChannelMutationVariables
- >(CREATE_CHANNEL, {
- input: {
- code: 'japan-channel',
- currencyCode: CurrencyCode.JPY,
- token: JAPAN_CHANNEL_TOKEN,
- defaultLanguageCode: LanguageCode.en,
- defaultShippingZoneId: 'T_1',
- defaultTaxZoneId: 'T_1',
- pricesIncludeTax: true,
- },
- });
- adminClient.setChannelToken(JAPAN_CHANNEL_TOKEN);
- shopClient.setChannelToken(JAPAN_CHANNEL_TOKEN);
- const { createStockLocation } = await adminClient.query(TestCreateStockLocationDocument, {
- input: {
- name: 'Japan warehouse',
- },
- });
- const { createProduct } = await adminClient.query<
- CreateProductMutation,
- CreateProductMutationVariables
- >(CREATE_PRODUCT, {
- input: {
- translations: [
- {
- languageCode: LanguageCode.en,
- name: 'Channel Product',
- slug: 'channel-product',
- description: 'Channel product',
- },
- ],
- },
- });
- const { createProductVariants } = await adminClient.query<
- CreateProductVariantsMutation,
- CreateProductVariantsMutationVariables
- >(CREATE_PRODUCT_VARIANTS, {
- input: [
- {
- productId: createProduct.id,
- sku: 'PV1',
- optionIds: [],
- price: 5000,
- stockLevels: [
- {
- stockLocationId: createStockLocation.id,
- stockOnHand: 100,
- },
- ],
- translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
- },
- ],
- });
- japanProductId = createProductVariants[0]!.id;
- // Create a payment method for the Japan channel
- await adminClient.query<CreatePaymentMethodMutation, CreatePaymentMethodMutationVariables>(
- CREATE_PAYMENT_METHOD,
- {
- input: {
- code: `stripe-payment-${E2E_DEFAULT_CHANNEL_TOKEN}`,
- translations: [
- {
- name: 'Stripe payment test',
- description: 'This is a Stripe test payment method',
- languageCode: LanguageCode.en,
- },
- ],
- enabled: true,
- handler: {
- code: stripePaymentMethodHandler.code,
- arguments: [
- { name: 'apiKey', value: 'test-api-key' },
- { name: 'webhookSecret', value: 'test-signing-secret' },
- ],
- },
- },
- },
- );
- });
- it('prepares order', async () => {
- await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
- const { addItemToOrder } = await shopClient.query<
- AddItemToOrderMutation,
- AddItemToOrderMutationVariables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: japanProductId,
- quantity: 1,
- });
- expect((addItemToOrder as any).totalWithTax).toBe(5000);
- });
- it('sends correct amount when creating payment intent', async () => {
- let createPaymentIntentPayload: any;
- const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
- nock('https://api.stripe.com/')
- .post('/v1/payment_intents', body => {
- createPaymentIntentPayload = body;
- return true;
- })
- .reply(200, {
- client_secret: 'test-client-secret',
- });
- const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
- expect(createPaymentIntentPayload.amount).toBe((activeOrder!.totalWithTax / 100).toString());
- expect(createPaymentIntentPayload.currency).toBe('jpy');
- });
- });
- });
|