stripe-payment.e2e-spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. /* tslint:disable:no-non-null-assertion */
  2. import { EntityHydrator, mergeConfig } from '@vendure/core';
  3. import { CreateProduct, CreateProductVariants } from '@vendure/core/e2e/graphql/generated-e2e-admin-types';
  4. import { CREATE_PRODUCT, CREATE_PRODUCT_VARIANTS } from '@vendure/core/e2e/graphql/shared-definitions';
  5. import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
  6. import gql from 'graphql-tag';
  7. import nock from 'nock';
  8. import path from 'path';
  9. import { initialData } from '../../../e2e-common/e2e-initial-data';
  10. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  11. import { StripePlugin } from '../src/stripe';
  12. import { stripePaymentMethodHandler } from '../src/stripe/stripe.handler';
  13. import { CREATE_CHANNEL, CREATE_PAYMENT_METHOD, GET_CUSTOMER_LIST } from './graphql/admin-queries';
  14. import {
  15. CreateChannelMutation,
  16. CreateChannelMutationVariables,
  17. CreatePaymentMethod,
  18. CurrencyCode,
  19. GetCustomerList,
  20. GetCustomerListQuery,
  21. LanguageCode,
  22. } from './graphql/generated-admin-types';
  23. import {
  24. AddItemToOrder,
  25. GetActiveOrderQuery,
  26. TestOrderFragmentFragment,
  27. } from './graphql/generated-shop-types';
  28. import { ADD_ITEM_TO_ORDER, GET_ACTIVE_ORDER } from './graphql/shop-queries';
  29. import { setShipping } from './payment-helpers';
  30. export const CREATE_STRIPE_PAYMENT_INTENT = gql`
  31. mutation createStripePaymentIntent {
  32. createStripePaymentIntent
  33. }
  34. `;
  35. describe('Stripe payments', () => {
  36. const devConfig = mergeConfig(testConfig(), {
  37. plugins: [
  38. StripePlugin.init({
  39. apiKey: 'test-api-key',
  40. webhookSigningSecret: 'test-signing-secret',
  41. storeCustomersInStripe: true,
  42. }),
  43. ],
  44. });
  45. const { shopClient, adminClient, server } = createTestEnvironment(devConfig);
  46. let started = false;
  47. let customers: GetCustomerListQuery['customers']['items'];
  48. let order: TestOrderFragmentFragment;
  49. let serverPort: number;
  50. beforeAll(async () => {
  51. serverPort = devConfig.apiOptions.port;
  52. await server.init({
  53. initialData,
  54. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  55. customerCount: 2,
  56. });
  57. started = true;
  58. await adminClient.asSuperAdmin();
  59. ({
  60. customers: { items: customers },
  61. } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(GET_CUSTOMER_LIST, {
  62. options: {
  63. take: 2,
  64. },
  65. }));
  66. }, TEST_SETUP_TIMEOUT_MS);
  67. afterAll(async () => {
  68. await server.destroy();
  69. });
  70. it('Should start successfully', async () => {
  71. expect(started).toEqual(true);
  72. expect(customers).toHaveLength(2);
  73. });
  74. it('Should prepare an order', async () => {
  75. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  76. const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
  77. ADD_ITEM_TO_ORDER,
  78. {
  79. productVariantId: 'T_1',
  80. quantity: 2,
  81. },
  82. );
  83. order = addItemToOrder as TestOrderFragmentFragment;
  84. expect(order.code).toBeDefined();
  85. });
  86. it('Should add a Stripe paymentMethod', async () => {
  87. const { createPaymentMethod } = await adminClient.query<
  88. CreatePaymentMethod.Mutation,
  89. CreatePaymentMethod.Variables
  90. >(CREATE_PAYMENT_METHOD, {
  91. input: {
  92. code: `stripe-payment-${E2E_DEFAULT_CHANNEL_TOKEN}`,
  93. name: 'Stripe payment test',
  94. description: 'This is a Stripe test payment method',
  95. enabled: true,
  96. handler: {
  97. code: stripePaymentMethodHandler.code,
  98. arguments: [],
  99. },
  100. },
  101. });
  102. expect(createPaymentMethod.code).toBe(`stripe-payment-${E2E_DEFAULT_CHANNEL_TOKEN}`);
  103. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  104. await setShipping(shopClient);
  105. });
  106. it('if no customer id exists, makes a call to create', async () => {
  107. let createCustomerPayload: { name: string; email: string } | undefined;
  108. const emptyList = { data: [] };
  109. nock('https://api.stripe.com/')
  110. .get(/\/v1\/customers.*/)
  111. .reply(200, emptyList);
  112. nock('https://api.stripe.com/')
  113. .post('/v1/customers', body => {
  114. createCustomerPayload = body;
  115. return true;
  116. })
  117. .reply(201, {
  118. id: 'new-customer-id',
  119. });
  120. nock('https://api.stripe.com/').post('/v1/payment_intents').reply(200, {
  121. client_secret: 'test-client-secret',
  122. });
  123. const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
  124. expect(createCustomerPayload).toEqual({
  125. email: 'hayden.zieme12@hotmail.com',
  126. name: 'Hayden Zieme',
  127. });
  128. });
  129. it('should send correct payload to create payment intent', async () => {
  130. let createPaymentIntentPayload: any;
  131. const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  132. nock('https://api.stripe.com/')
  133. .post('/v1/payment_intents', body => {
  134. createPaymentIntentPayload = body;
  135. return true;
  136. })
  137. .reply(200, {
  138. client_secret: 'test-client-secret',
  139. });
  140. const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
  141. expect(createPaymentIntentPayload).toEqual({
  142. amount: activeOrder?.totalWithTax.toString(),
  143. currency: activeOrder?.currencyCode?.toLowerCase(),
  144. customer: 'new-customer-id',
  145. 'automatic_payment_methods[enabled]': 'true',
  146. 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
  147. 'metadata[orderId]': '1',
  148. 'metadata[orderCode]': activeOrder?.code,
  149. });
  150. expect(createStripePaymentIntent).toEqual('test-client-secret');
  151. });
  152. // https://github.com/vendure-ecommerce/vendure/issues/1935
  153. it('should attach metadata to stripe payment intent', async () => {
  154. StripePlugin.options.metadata = async (injector, ctx, currentOrder) => {
  155. const hydrator = await injector.get(EntityHydrator);
  156. await hydrator.hydrate(ctx, currentOrder, { relations: ['customer'] });
  157. return {
  158. customerEmail: currentOrder.customer?.emailAddress ?? 'demo',
  159. };
  160. };
  161. let createPaymentIntentPayload: any;
  162. const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  163. nock('https://api.stripe.com/')
  164. .post('/v1/payment_intents', body => {
  165. createPaymentIntentPayload = body;
  166. return true;
  167. })
  168. .reply(200, {
  169. client_secret: 'test-client-secret',
  170. });
  171. const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
  172. expect(createPaymentIntentPayload).toEqual({
  173. amount: activeOrder?.totalWithTax.toString(),
  174. currency: activeOrder?.currencyCode?.toLowerCase(),
  175. customer: 'new-customer-id',
  176. 'automatic_payment_methods[enabled]': 'true',
  177. 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
  178. 'metadata[orderId]': '1',
  179. 'metadata[orderCode]': activeOrder?.code,
  180. 'metadata[customerEmail]': customers[0].emailAddress,
  181. });
  182. expect(createStripePaymentIntent).toEqual('test-client-secret');
  183. StripePlugin.options.metadata = undefined;
  184. });
  185. // https://github.com/vendure-ecommerce/vendure/issues/1630
  186. describe('currencies with no fractional units', () => {
  187. let japanProductId: string;
  188. beforeAll(async () => {
  189. const JAPAN_CHANNEL_TOKEN = 'japan-channel-token';
  190. const { createChannel } = await adminClient.query<
  191. CreateChannelMutation,
  192. CreateChannelMutationVariables
  193. >(CREATE_CHANNEL, {
  194. input: {
  195. code: 'japan-channel',
  196. currencyCode: CurrencyCode.JPY,
  197. token: JAPAN_CHANNEL_TOKEN,
  198. defaultLanguageCode: LanguageCode.en,
  199. defaultShippingZoneId: 'T_1',
  200. defaultTaxZoneId: 'T_1',
  201. pricesIncludeTax: true,
  202. },
  203. });
  204. adminClient.setChannelToken(JAPAN_CHANNEL_TOKEN);
  205. shopClient.setChannelToken(JAPAN_CHANNEL_TOKEN);
  206. const { createProduct } = await adminClient.query<
  207. CreateProduct.Mutation,
  208. CreateProduct.Variables
  209. >(CREATE_PRODUCT, {
  210. input: {
  211. translations: [
  212. {
  213. languageCode: LanguageCode.en,
  214. name: 'Channel Product',
  215. slug: 'channel-product',
  216. description: 'Channel product',
  217. },
  218. ],
  219. },
  220. });
  221. const { createProductVariants } = await adminClient.query<
  222. CreateProductVariants.Mutation,
  223. CreateProductVariants.Variables
  224. >(CREATE_PRODUCT_VARIANTS, {
  225. input: [
  226. {
  227. productId: createProduct.id,
  228. sku: 'PV1',
  229. optionIds: [],
  230. price: 5000,
  231. stockOnHand: 100,
  232. translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
  233. },
  234. ],
  235. });
  236. japanProductId = createProductVariants[0]!.id;
  237. });
  238. it('prepares order', async () => {
  239. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  240. const { addItemToOrder } = await shopClient.query<
  241. AddItemToOrder.Mutation,
  242. AddItemToOrder.Variables
  243. >(ADD_ITEM_TO_ORDER, {
  244. productVariantId: japanProductId,
  245. quantity: 1,
  246. });
  247. expect((addItemToOrder as any).totalWithTax).toBe(5000);
  248. });
  249. it('sends correct amount when creating payment intent', async () => {
  250. let createPaymentIntentPayload: any;
  251. const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  252. nock('https://api.stripe.com/')
  253. .post('/v1/payment_intents', body => {
  254. createPaymentIntentPayload = body;
  255. return true;
  256. })
  257. .reply(200, {
  258. client_secret: 'test-client-secret',
  259. });
  260. const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
  261. expect(createPaymentIntentPayload.amount).toBe((activeOrder!.totalWithTax / 100).toString());
  262. expect(createPaymentIntentPayload.currency).toBe('jpy');
  263. });
  264. });
  265. });