1
0

order.e2e-spec.ts 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281
  1. /* tslint:disable:no-non-null-assertion */
  2. import gql from 'graphql-tag';
  3. import path from 'path';
  4. import { HistoryEntryType, StockMovementType } from '../../common/lib/generated-types';
  5. import { pick } from '../../common/lib/pick';
  6. import { ID } from '../../common/lib/shared-types';
  7. import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
  8. import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
  9. import { ORDER_FRAGMENT, ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
  10. import {
  11. AddNoteToOrder,
  12. CancelOrder,
  13. CreateFulfillment,
  14. GetCustomerList,
  15. GetOrder,
  16. GetOrderFulfillmentItems,
  17. GetOrderFulfillments,
  18. GetOrderHistory,
  19. GetOrderList,
  20. GetOrderListFulfillments,
  21. GetProductWithVariants,
  22. GetStockMovement,
  23. OrderItemFragment,
  24. RefundOrder,
  25. SettlePayment,
  26. SettleRefund,
  27. UpdateProductVariants,
  28. } from './graphql/generated-e2e-admin-types';
  29. import {
  30. AddItemToOrder,
  31. AddPaymentToOrder,
  32. GetShippingMethods,
  33. SetShippingAddress,
  34. SetShippingMethod,
  35. TransitionToState,
  36. } from './graphql/generated-e2e-shop-types';
  37. import { GET_CUSTOMER_LIST, GET_PRODUCT_WITH_VARIANTS, GET_STOCK_MOVEMENT, UPDATE_PRODUCT_VARIANTS } from './graphql/shared-definitions';
  38. import {
  39. ADD_ITEM_TO_ORDER,
  40. ADD_PAYMENT,
  41. GET_ELIGIBLE_SHIPPING_METHODS,
  42. SET_SHIPPING_ADDRESS,
  43. SET_SHIPPING_METHOD,
  44. TRANSITION_TO_STATE,
  45. } from './graphql/shop-definitions';
  46. import { TestAdminClient, TestShopClient } from './test-client';
  47. import { TestServer } from './test-server';
  48. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  49. describe('Orders resolver', () => {
  50. const adminClient = new TestAdminClient();
  51. const shopClient = new TestShopClient();
  52. const server = new TestServer();
  53. let customers: GetCustomerList.Items[];
  54. const password = 'test';
  55. beforeAll(async () => {
  56. const token = await server.init(
  57. {
  58. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  59. customerCount: 2,
  60. },
  61. {
  62. paymentOptions: {
  63. paymentMethodHandlers: [
  64. twoStagePaymentMethod,
  65. failsToSettlePaymentMethod,
  66. singleStageRefundablePaymentMethod,
  67. ],
  68. },
  69. },
  70. );
  71. await adminClient.init();
  72. // Create a couple of orders to be queried
  73. const result = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
  74. GET_CUSTOMER_LIST,
  75. {
  76. options: {
  77. take: 2,
  78. },
  79. },
  80. );
  81. customers = result.customers.items;
  82. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  83. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  84. productVariantId: 'T_1',
  85. quantity: 1,
  86. });
  87. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  88. productVariantId: 'T_2',
  89. quantity: 1,
  90. });
  91. await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
  92. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  93. productVariantId: 'T_2',
  94. quantity: 1,
  95. });
  96. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  97. productVariantId: 'T_3',
  98. quantity: 3,
  99. });
  100. }, TEST_SETUP_TIMEOUT_MS);
  101. afterAll(async () => {
  102. await server.destroy();
  103. });
  104. it('orders', async () => {
  105. const result = await adminClient.query<GetOrderList.Query>(GET_ORDERS_LIST);
  106. expect(result.orders.items.map(o => o.id)).toEqual(['T_1', 'T_2']);
  107. });
  108. it('order', async () => {
  109. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
  110. expect(result.order!.id).toBe('T_2');
  111. });
  112. it('order history initially empty', async () => {
  113. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, { id: 'T_1' });
  114. expect(order!.history.totalItems).toBe(0);
  115. expect(order!.history.items).toEqual([]);
  116. });
  117. describe('payments', () => {
  118. it('settlePayment fails', async () => {
  119. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  120. await proceedToArrangingPayment(shopClient);
  121. const { addPaymentToOrder } = await shopClient.query<
  122. AddPaymentToOrder.Mutation,
  123. AddPaymentToOrder.Variables
  124. >(ADD_PAYMENT, {
  125. input: {
  126. method: failsToSettlePaymentMethod.code,
  127. metadata: {
  128. baz: 'quux',
  129. },
  130. },
  131. });
  132. const order = addPaymentToOrder!;
  133. expect(order.state).toBe('PaymentAuthorized');
  134. const payment = order.payments![0];
  135. const { settlePayment } = await adminClient.query<
  136. SettlePayment.Mutation,
  137. SettlePayment.Variables
  138. >(SETTLE_PAYMENT, {
  139. id: payment.id,
  140. });
  141. expect(settlePayment!.id).toBe(payment.id);
  142. expect(settlePayment!.state).toBe('Authorized');
  143. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  144. id: order.id,
  145. });
  146. expect(result.order!.state).toBe('PaymentAuthorized');
  147. });
  148. it('settlePayment succeeds', async () => {
  149. await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
  150. await proceedToArrangingPayment(shopClient);
  151. const { addPaymentToOrder } = await shopClient.query<
  152. AddPaymentToOrder.Mutation,
  153. AddPaymentToOrder.Variables
  154. >(ADD_PAYMENT, {
  155. input: {
  156. method: twoStagePaymentMethod.code,
  157. metadata: {
  158. baz: 'quux',
  159. },
  160. },
  161. });
  162. const order = addPaymentToOrder!;
  163. expect(order.state).toBe('PaymentAuthorized');
  164. const payment = order.payments![0];
  165. const { settlePayment } = await adminClient.query<
  166. SettlePayment.Mutation,
  167. SettlePayment.Variables
  168. >(SETTLE_PAYMENT, {
  169. id: payment.id,
  170. });
  171. expect(settlePayment!.id).toBe(payment.id);
  172. expect(settlePayment!.state).toBe('Settled');
  173. // further metadata is combined into existing object
  174. expect(settlePayment!.metadata).toEqual({
  175. baz: 'quux',
  176. moreData: 42,
  177. });
  178. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  179. id: order.id,
  180. });
  181. expect(result.order!.state).toBe('PaymentSettled');
  182. expect(result.order!.payments![0].state).toBe('Settled');
  183. });
  184. it('order history contains expected entries', async () => {
  185. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, { id: 'T_2' });
  186. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  187. {
  188. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  189. from: 'AddingItems',
  190. to: 'ArrangingPayment',
  191. },
  192. },
  193. {
  194. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
  195. paymentId: 'T_2',
  196. from: 'Created',
  197. to: 'Authorized',
  198. },
  199. },
  200. {
  201. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  202. from: 'ArrangingPayment',
  203. to: 'PaymentAuthorized',
  204. },
  205. },
  206. {
  207. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
  208. paymentId: 'T_2',
  209. from: 'Authorized',
  210. to: 'Settled',
  211. },
  212. },
  213. {
  214. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  215. from: 'PaymentAuthorized',
  216. to: 'PaymentSettled',
  217. },
  218. },
  219. ]);
  220. });
  221. });
  222. describe('fulfillment', () => {
  223. it(
  224. 'throws if Order is not in "PaymentSettled" state',
  225. assertThrowsWithMessage(async () => {
  226. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  227. id: 'T_1',
  228. });
  229. expect(order!.state).toBe('PaymentAuthorized');
  230. await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
  231. CREATE_FULFILLMENT,
  232. {
  233. input: {
  234. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  235. method: 'Test',
  236. },
  237. },
  238. );
  239. }, 'One or more OrderItems belong to an Order which is in an invalid state'),
  240. );
  241. it(
  242. 'throws if lines is empty',
  243. assertThrowsWithMessage(async () => {
  244. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  245. id: 'T_2',
  246. });
  247. expect(order!.state).toBe('PaymentSettled');
  248. await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
  249. CREATE_FULFILLMENT,
  250. {
  251. input: {
  252. lines: [],
  253. method: 'Test',
  254. },
  255. },
  256. );
  257. }, 'Nothing to fulfill'),
  258. );
  259. it(
  260. 'throws if all quantities are zero',
  261. assertThrowsWithMessage(async () => {
  262. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  263. id: 'T_2',
  264. });
  265. expect(order!.state).toBe('PaymentSettled');
  266. await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
  267. CREATE_FULFILLMENT,
  268. {
  269. input: {
  270. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
  271. method: 'Test',
  272. },
  273. },
  274. );
  275. }, 'Nothing to fulfill'),
  276. );
  277. it('creates a partial fulfillment', async () => {
  278. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  279. id: 'T_2',
  280. });
  281. expect(order!.state).toBe('PaymentSettled');
  282. const lines = order!.lines;
  283. const { fulfillOrder } = await adminClient.query<
  284. CreateFulfillment.Mutation,
  285. CreateFulfillment.Variables
  286. >(CREATE_FULFILLMENT, {
  287. input: {
  288. lines: lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  289. method: 'Test1',
  290. trackingCode: '111',
  291. },
  292. });
  293. expect(fulfillOrder!.method).toBe('Test1');
  294. expect(fulfillOrder!.trackingCode).toBe('111');
  295. expect(fulfillOrder!.orderItems).toEqual([
  296. { id: lines[0].items[0].id },
  297. { id: lines[1].items[0].id },
  298. ]);
  299. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  300. id: 'T_2',
  301. });
  302. expect(result.order!.state).toBe('PartiallyFulfilled');
  303. expect(result.order!.lines[0].items[0].fulfillment!.id).toBe(fulfillOrder!.id);
  304. expect(result.order!.lines[1].items[2].fulfillment!.id).toBe(fulfillOrder!.id);
  305. expect(result.order!.lines[1].items[1].fulfillment).toBeNull();
  306. expect(result.order!.lines[1].items[0].fulfillment).toBeNull();
  307. });
  308. it('creates a second partial fulfillment', async () => {
  309. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  310. id: 'T_2',
  311. });
  312. expect(order!.state).toBe('PartiallyFulfilled');
  313. const lines = order!.lines;
  314. const { fulfillOrder } = await adminClient.query<
  315. CreateFulfillment.Mutation,
  316. CreateFulfillment.Variables
  317. >(CREATE_FULFILLMENT, {
  318. input: {
  319. lines: [{ orderLineId: lines[1].id, quantity: 1 }],
  320. method: 'Test2',
  321. trackingCode: '222',
  322. },
  323. });
  324. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  325. id: 'T_2',
  326. });
  327. // expect(result.order!.lines).toEqual({});
  328. expect(result.order!.state).toBe('PartiallyFulfilled');
  329. expect(result.order!.lines[1].items[2].fulfillment).not.toBeNull();
  330. expect(result.order!.lines[1].items[1].fulfillment).not.toBeNull();
  331. expect(result.order!.lines[1].items[0].fulfillment).toBeNull();
  332. });
  333. it(
  334. 'throws if an OrderItem already part of a Fulfillment',
  335. assertThrowsWithMessage(async () => {
  336. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  337. id: 'T_2',
  338. });
  339. expect(order!.state).toBe('PartiallyFulfilled');
  340. await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
  341. CREATE_FULFILLMENT,
  342. {
  343. input: {
  344. method: 'Test',
  345. lines: [
  346. {
  347. orderLineId: order!.lines[0].id,
  348. quantity: 1,
  349. },
  350. ],
  351. },
  352. },
  353. );
  354. }, 'One or more OrderItems have already been fulfilled'),
  355. );
  356. it('completes fulfillment', async () => {
  357. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  358. id: 'T_2',
  359. });
  360. expect(order!.state).toBe('PartiallyFulfilled');
  361. const orderItems = order!.lines.reduce(
  362. (items, line) => [...items, ...line.items],
  363. [] as OrderItemFragment[],
  364. );
  365. const { fulfillOrder } = await adminClient.query<
  366. CreateFulfillment.Mutation,
  367. CreateFulfillment.Variables
  368. >(CREATE_FULFILLMENT, {
  369. input: {
  370. lines: [
  371. {
  372. orderLineId: order!.lines[1].id,
  373. quantity: 1,
  374. },
  375. ],
  376. method: 'Test3',
  377. trackingCode: '333',
  378. },
  379. });
  380. expect(fulfillOrder!.method).toBe('Test3');
  381. expect(fulfillOrder!.trackingCode).toBe('333');
  382. expect(fulfillOrder!.orderItems).toEqual([{ id: orderItems[1].id }]);
  383. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  384. id: 'T_2',
  385. });
  386. expect(result.order!.state).toBe('Fulfilled');
  387. });
  388. it('order history contains expected entries', async () => {
  389. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, {
  390. id: 'T_2',
  391. options: {
  392. skip: 5,
  393. },
  394. });
  395. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  396. {
  397. type: HistoryEntryType.ORDER_FULLFILLMENT, data: {
  398. fulfillmentId: 'T_1',
  399. },
  400. },
  401. {
  402. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  403. from: 'PaymentSettled',
  404. to: 'PartiallyFulfilled',
  405. },
  406. },
  407. {
  408. type: HistoryEntryType.ORDER_FULLFILLMENT, data: {
  409. fulfillmentId: 'T_2',
  410. },
  411. },
  412. {
  413. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  414. from: 'PartiallyFulfilled',
  415. to: 'PartiallyFulfilled',
  416. },
  417. },
  418. {
  419. type: HistoryEntryType.ORDER_FULLFILLMENT, data: {
  420. fulfillmentId: 'T_3',
  421. },
  422. },
  423. {
  424. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  425. from: 'PartiallyFulfilled',
  426. to: 'Fulfilled',
  427. },
  428. },
  429. ]);
  430. });
  431. it('order.fullfillments resolver for single order', async () => {
  432. const { order } = await adminClient.query<
  433. GetOrderFulfillments.Query,
  434. GetOrderFulfillments.Variables
  435. >(GET_ORDER_FULFILLMENTS, {
  436. id: 'T_2',
  437. });
  438. expect(order!.fulfillments).toEqual([
  439. { id: 'T_1', method: 'Test1' },
  440. { id: 'T_2', method: 'Test2' },
  441. { id: 'T_3', method: 'Test3' },
  442. ]);
  443. });
  444. it('order.fullfillments resolver for order list', async () => {
  445. const { orders } = await adminClient.query<GetOrderListFulfillments.Query>(
  446. GET_ORDER_LIST_FULFILLMENTS,
  447. );
  448. expect(orders.items[0].fulfillments).toEqual([]);
  449. expect(orders.items[1].fulfillments).toEqual([
  450. { id: 'T_1', method: 'Test1' },
  451. { id: 'T_2', method: 'Test2' },
  452. { id: 'T_3', method: 'Test3' },
  453. ]);
  454. });
  455. it('order.fullfillments.orderItems resolver', async () => {
  456. const { order } = await adminClient.query<
  457. GetOrderFulfillmentItems.Query,
  458. GetOrderFulfillmentItems.Variables
  459. >(GET_ORDER_FULFILLMENT_ITEMS, {
  460. id: 'T_2',
  461. });
  462. expect(order!.fulfillments![0].orderItems).toEqual([{ id: 'T_3' }, { id: 'T_4' }]);
  463. expect(order!.fulfillments![1].orderItems).toEqual([{ id: 'T_5' }]);
  464. });
  465. });
  466. describe('cancellation', () => {
  467. let orderId: string;
  468. let product: GetProductWithVariants.Product;
  469. let productVariantId: string;
  470. beforeAll(async () => {
  471. const result = await adminClient.query<
  472. GetProductWithVariants.Query,
  473. GetProductWithVariants.Variables
  474. >(GET_PRODUCT_WITH_VARIANTS, {
  475. id: 'T_3',
  476. });
  477. product = result.product!;
  478. productVariantId = product.variants[0].id;
  479. // Set the ProductVariant to trackInventory
  480. const { updateProductVariants } = await adminClient.query<
  481. UpdateProductVariants.Mutation,
  482. UpdateProductVariants.Variables
  483. >(UPDATE_PRODUCT_VARIANTS, {
  484. input: [
  485. {
  486. id: productVariantId,
  487. trackInventory: true,
  488. },
  489. ],
  490. });
  491. // Add the ProductVariant to the Order
  492. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  493. const { addItemToOrder } = await shopClient.query<
  494. AddItemToOrder.Mutation,
  495. AddItemToOrder.Variables
  496. >(ADD_ITEM_TO_ORDER, {
  497. productVariantId,
  498. quantity: 2,
  499. });
  500. orderId = addItemToOrder!.id;
  501. });
  502. it(
  503. 'cannot cancel from AddingItems state',
  504. assertThrowsWithMessage(async () => {
  505. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  506. id: orderId,
  507. });
  508. expect(order!.state).toBe('AddingItems');
  509. await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
  510. input: {
  511. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  512. },
  513. });
  514. }, 'Cannot cancel OrderLines from an Order in the "AddingItems" state'),
  515. );
  516. it(
  517. 'cannot cancel from ArrangingPayment state',
  518. assertThrowsWithMessage(async () => {
  519. await proceedToArrangingPayment(shopClient);
  520. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  521. id: orderId,
  522. });
  523. expect(order!.state).toBe('ArrangingPayment');
  524. await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
  525. input: {
  526. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  527. },
  528. });
  529. }, 'Cannot cancel OrderLines from an Order in the "ArrangingPayment" state'),
  530. );
  531. it(
  532. 'throws if lines are ampty',
  533. assertThrowsWithMessage(async () => {
  534. const { addPaymentToOrder } = await shopClient.query<
  535. AddPaymentToOrder.Mutation,
  536. AddPaymentToOrder.Variables
  537. >(ADD_PAYMENT, {
  538. input: {
  539. method: twoStagePaymentMethod.code,
  540. metadata: {
  541. baz: 'quux',
  542. },
  543. },
  544. });
  545. expect(addPaymentToOrder!.state).toBe('PaymentAuthorized');
  546. await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
  547. input: {
  548. lines: [],
  549. },
  550. });
  551. }, 'Nothing to cancel',
  552. ),
  553. );
  554. it(
  555. 'throws if all quantities zero',
  556. assertThrowsWithMessage(async () => {
  557. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  558. id: orderId,
  559. });
  560. await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
  561. input: {
  562. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
  563. },
  564. });
  565. }, 'Nothing to cancel',
  566. ),
  567. );
  568. it('partial cancellation', async () => {
  569. const result1 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  570. GET_STOCK_MOVEMENT,
  571. {
  572. id: product.id,
  573. },
  574. );
  575. const variant1 = result1.product!.variants[0];
  576. expect(variant1.stockOnHand).toBe(98);
  577. expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  578. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  579. { type: StockMovementType.SALE, quantity: -2 },
  580. ]);
  581. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  582. id: orderId,
  583. });
  584. const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
  585. input: {
  586. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  587. reason: 'cancel reason 1',
  588. },
  589. });
  590. expect(cancelOrder.lines[0].quantity).toBe(1);
  591. expect(cancelOrder.lines[0].items).toEqual([
  592. { id: 'T_7', cancelled: true },
  593. { id: 'T_8', cancelled: false },
  594. ]);
  595. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  596. id: orderId,
  597. });
  598. expect(order2!.state).toBe('PaymentAuthorized');
  599. expect(order2!.lines[0].quantity).toBe(1);
  600. const result2 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  601. GET_STOCK_MOVEMENT,
  602. {
  603. id: product.id,
  604. },
  605. );
  606. const variant2 = result2.product!.variants[0];
  607. expect(variant2.stockOnHand).toBe(99);
  608. expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  609. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  610. { type: StockMovementType.SALE, quantity: -2 },
  611. { type: StockMovementType.CANCELLATION, quantity: 1 },
  612. ]);
  613. });
  614. it('complete cancellation', async () => {
  615. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  616. id: orderId,
  617. });
  618. await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
  619. input: {
  620. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  621. reason: 'cancel reason 2',
  622. },
  623. });
  624. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  625. id: orderId,
  626. });
  627. expect(order2!.state).toBe('Cancelled');
  628. const result = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  629. GET_STOCK_MOVEMENT,
  630. {
  631. id: product.id,
  632. },
  633. );
  634. const variant2 = result.product!.variants[0];
  635. expect(variant2.stockOnHand).toBe(100);
  636. expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  637. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  638. { type: StockMovementType.SALE, quantity: -2 },
  639. { type: StockMovementType.CANCELLATION, quantity: 1 },
  640. { type: StockMovementType.CANCELLATION, quantity: 1 },
  641. ]);
  642. });
  643. it('order history contains expected entries', async () => {
  644. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, {
  645. id: orderId,
  646. options: {
  647. skip: 0,
  648. },
  649. });
  650. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  651. {
  652. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  653. from: 'AddingItems',
  654. to: 'ArrangingPayment',
  655. },
  656. },
  657. {
  658. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
  659. paymentId: 'T_3',
  660. from: 'Created',
  661. to: 'Authorized',
  662. },
  663. },
  664. {
  665. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  666. from: 'ArrangingPayment',
  667. to: 'PaymentAuthorized',
  668. },
  669. },
  670. {
  671. type: HistoryEntryType.ORDER_CANCELLATION, data: {
  672. orderItemIds: ['T_7'],
  673. reason: 'cancel reason 1',
  674. },
  675. },
  676. {
  677. type: HistoryEntryType.ORDER_CANCELLATION, data: {
  678. orderItemIds: ['T_8'],
  679. reason: 'cancel reason 2',
  680. },
  681. },
  682. {
  683. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  684. from: 'PaymentAuthorized',
  685. to: 'Cancelled',
  686. },
  687. },
  688. ]);
  689. });
  690. });
  691. describe('refunds', () => {
  692. let orderId: string;
  693. let product: GetProductWithVariants.Product;
  694. let productVariantId: string;
  695. let paymentId: string;
  696. let refundId: string;
  697. beforeAll(async () => {
  698. const result = await adminClient.query<
  699. GetProductWithVariants.Query,
  700. GetProductWithVariants.Variables
  701. >(GET_PRODUCT_WITH_VARIANTS, {
  702. id: 'T_3',
  703. });
  704. product = result.product!;
  705. productVariantId = product.variants[0].id;
  706. // Set the ProductVariant to trackInventory
  707. const { updateProductVariants } = await adminClient.query<
  708. UpdateProductVariants.Mutation,
  709. UpdateProductVariants.Variables
  710. >(UPDATE_PRODUCT_VARIANTS, {
  711. input: [
  712. {
  713. id: productVariantId,
  714. trackInventory: true,
  715. },
  716. ],
  717. });
  718. // Add the ProductVariant to the Order
  719. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  720. const { addItemToOrder } = await shopClient.query<
  721. AddItemToOrder.Mutation,
  722. AddItemToOrder.Variables
  723. >(ADD_ITEM_TO_ORDER, {
  724. productVariantId,
  725. quantity: 2,
  726. });
  727. orderId = addItemToOrder!.id;
  728. });
  729. it(
  730. 'cannot refund from PaymentAuthorized state',
  731. assertThrowsWithMessage(async () => {
  732. await proceedToArrangingPayment(shopClient);
  733. const { addPaymentToOrder } = await shopClient.query<
  734. AddPaymentToOrder.Mutation,
  735. AddPaymentToOrder.Variables
  736. >(ADD_PAYMENT, {
  737. input: {
  738. method: twoStagePaymentMethod.code,
  739. metadata: {
  740. baz: 'quux',
  741. },
  742. },
  743. });
  744. expect(addPaymentToOrder!.state).toBe('PaymentAuthorized');
  745. paymentId = addPaymentToOrder!.payments![0].id;
  746. await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(REFUND_ORDER, {
  747. input: {
  748. lines: addPaymentToOrder!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  749. shipping: 0,
  750. adjustment: 0,
  751. paymentId,
  752. },
  753. });
  754. }, 'Cannot refund an Order in the "PaymentAuthorized" state'),
  755. );
  756. it(
  757. 'throws if no lines and no shipping',
  758. assertThrowsWithMessage(async () => {
  759. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  760. id: orderId,
  761. });
  762. const { settlePayment } = await adminClient.query<
  763. SettlePayment.Mutation,
  764. SettlePayment.Variables
  765. >(SETTLE_PAYMENT, {
  766. id: order!.payments![0].id,
  767. });
  768. expect(settlePayment!.state).toBe('Settled');
  769. await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(REFUND_ORDER, {
  770. input: {
  771. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
  772. shipping: 0,
  773. adjustment: 0,
  774. paymentId,
  775. },
  776. });
  777. }, 'Nothing to refund',
  778. ),
  779. );
  780. it(
  781. 'throws if paymentId not valid',
  782. assertThrowsWithMessage(async () => {
  783. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  784. id: orderId,
  785. });
  786. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(REFUND_ORDER, {
  787. input: {
  788. lines: [],
  789. shipping: 100,
  790. adjustment: 0,
  791. paymentId: 'T_999',
  792. },
  793. });
  794. }, 'No Payment with the id \'999\' could be found',
  795. ),
  796. );
  797. it(
  798. 'throws if payment and order lines do not belong to the same Order',
  799. assertThrowsWithMessage(async () => {
  800. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  801. id: orderId,
  802. });
  803. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(REFUND_ORDER, {
  804. input: {
  805. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  806. shipping: 100,
  807. adjustment: 0,
  808. paymentId: 'T_1',
  809. },
  810. });
  811. }, 'The Payment and OrderLines do not belong to the same Order',
  812. ),
  813. );
  814. it('creates a Refund to be manually settled', async () => {
  815. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  816. id: orderId,
  817. });
  818. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(REFUND_ORDER, {
  819. input: {
  820. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  821. shipping: order!.shipping,
  822. adjustment: 0,
  823. reason: 'foo',
  824. paymentId,
  825. },
  826. });
  827. expect(refundOrder.shipping).toBe(order!.shipping);
  828. expect(refundOrder.items).toBe(order!.subTotal);
  829. expect(refundOrder.total).toBe(order!.total);
  830. expect(refundOrder.transactionId).toBe(null);
  831. expect(refundOrder.state).toBe('Pending');
  832. refundId = refundOrder.id;
  833. });
  834. it('throws if attempting to refund the same item more than once', assertThrowsWithMessage(async () => {
  835. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  836. id: orderId,
  837. });
  838. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(REFUND_ORDER, {
  839. input: {
  840. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  841. shipping: order!.shipping,
  842. adjustment: 0,
  843. paymentId,
  844. },
  845. });
  846. },
  847. 'Cannot refund an OrderItem which has already been refunded',
  848. ),
  849. );
  850. it('manually settle a Refund', async () => {
  851. const { settleRefund } = await adminClient.query<SettleRefund.Mutation, SettleRefund.Variables>(SETTLE_REFUND, {
  852. input: {
  853. id: refundId,
  854. transactionId: 'aaabbb',
  855. },
  856. });
  857. expect(settleRefund.state).toBe('Settled');
  858. expect(settleRefund.transactionId).toBe('aaabbb');
  859. });
  860. it('order history contains expected entries', async () => {
  861. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, {
  862. id: orderId,
  863. options: {
  864. skip: 0,
  865. },
  866. });
  867. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  868. {
  869. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  870. from: 'AddingItems',
  871. to: 'ArrangingPayment',
  872. },
  873. },
  874. {
  875. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
  876. paymentId: 'T_4',
  877. from: 'Created',
  878. to: 'Authorized',
  879. },
  880. },
  881. {
  882. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  883. from: 'ArrangingPayment',
  884. to: 'PaymentAuthorized',
  885. },
  886. },
  887. {
  888. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
  889. paymentId: 'T_4',
  890. from: 'Authorized',
  891. to: 'Settled',
  892. },
  893. },
  894. {
  895. type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
  896. from: 'PaymentAuthorized',
  897. to: 'PaymentSettled',
  898. },
  899. },
  900. {
  901. type: HistoryEntryType.ORDER_REFUND_TRANSITION, data: {
  902. refundId: 'T_1',
  903. reason: 'foo',
  904. from: 'Pending',
  905. to: 'Settled',
  906. },
  907. },
  908. ]);
  909. });
  910. });
  911. it('addNoteToOrder', async () => {
  912. const { addNoteToOrder } = await adminClient.query<AddNoteToOrder.Mutation, AddNoteToOrder.Variables>(ADD_NOTE_TO_ORDER, {
  913. input: {
  914. id: 'T_4',
  915. note: 'A test note',
  916. },
  917. });
  918. expect(addNoteToOrder.id).toBe('T_4');
  919. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, {
  920. id: 'T_4',
  921. options: {
  922. skip: 6,
  923. },
  924. });
  925. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  926. {
  927. type: HistoryEntryType.ORDER_NOTE, data: {
  928. note: 'A test note',
  929. },
  930. },
  931. ]);
  932. });
  933. });
  934. /**
  935. * A two-stage (authorize, capture) payment method, with no createRefund method.
  936. */
  937. const twoStagePaymentMethod = new PaymentMethodHandler({
  938. code: 'authorize-only-payment-method',
  939. description: 'Test Payment Method',
  940. args: {},
  941. createPayment: (order, args, metadata) => {
  942. return {
  943. amount: order.total,
  944. state: 'Authorized',
  945. transactionId: '12345',
  946. metadata,
  947. };
  948. },
  949. settlePayment: () => {
  950. return {
  951. success: true,
  952. metadata: {
  953. moreData: 42,
  954. },
  955. };
  956. },
  957. });
  958. /**
  959. * A payment method which includes a createRefund method.
  960. */
  961. const singleStageRefundablePaymentMethod = new PaymentMethodHandler({
  962. code: 'single-stage-refundable-payment-method',
  963. description: 'Test Payment Method',
  964. args: {},
  965. createPayment: (order, args, metadata) => {
  966. return {
  967. amount: order.total,
  968. state: 'Settled',
  969. transactionId: '12345',
  970. metadata,
  971. };
  972. },
  973. settlePayment: () => {
  974. return { success: true };
  975. },
  976. createRefund: (input, total, order, payment, args) => {
  977. return {
  978. amount: total,
  979. state: 'Settled',
  980. transactionId: 'abc123',
  981. };
  982. },
  983. });
  984. /**
  985. * A payment method where calling `settlePayment` always fails.
  986. */
  987. const failsToSettlePaymentMethod = new PaymentMethodHandler({
  988. code: 'fails-to-settle-payment-method',
  989. description: 'Test Payment Method',
  990. args: {},
  991. createPayment: (order, args, metadata) => {
  992. return {
  993. amount: order.total,
  994. state: 'Authorized',
  995. transactionId: '12345',
  996. metadata,
  997. };
  998. },
  999. settlePayment: () => {
  1000. return {
  1001. success: false,
  1002. errorMessage: 'Something went horribly wrong',
  1003. };
  1004. },
  1005. });
  1006. async function proceedToArrangingPayment(shopClient: TestShopClient): Promise<ID> {
  1007. await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(SET_SHIPPING_ADDRESS, {
  1008. input: {
  1009. fullName: 'name',
  1010. streetLine1: '12 the street',
  1011. city: 'foo',
  1012. postalCode: '123456',
  1013. countryCode: 'US',
  1014. },
  1015. });
  1016. const { eligibleShippingMethods } = await shopClient.query<GetShippingMethods.Query>(
  1017. GET_ELIGIBLE_SHIPPING_METHODS,
  1018. );
  1019. await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(SET_SHIPPING_METHOD, {
  1020. id: eligibleShippingMethods[1].id,
  1021. });
  1022. const { transitionOrderToState } = await shopClient.query<
  1023. TransitionToState.Mutation,
  1024. TransitionToState.Variables
  1025. >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
  1026. return transitionOrderToState!.id;
  1027. }
  1028. export const GET_ORDERS_LIST = gql`
  1029. query GetOrderList($options: OrderListOptions) {
  1030. orders(options: $options) {
  1031. items {
  1032. ...Order
  1033. }
  1034. totalItems
  1035. }
  1036. }
  1037. ${ORDER_FRAGMENT}
  1038. `;
  1039. export const GET_ORDER = gql`
  1040. query GetOrder($id: ID!) {
  1041. order(id: $id) {
  1042. ...OrderWithLines
  1043. }
  1044. }
  1045. ${ORDER_WITH_LINES_FRAGMENT}
  1046. `;
  1047. export const SETTLE_PAYMENT = gql`
  1048. mutation SettlePayment($id: ID!) {
  1049. settlePayment(id: $id) {
  1050. id
  1051. state
  1052. metadata
  1053. }
  1054. }
  1055. `;
  1056. export const CREATE_FULFILLMENT = gql`
  1057. mutation CreateFulfillment($input: FulfillOrderInput!) {
  1058. fulfillOrder(input: $input) {
  1059. id
  1060. method
  1061. trackingCode
  1062. orderItems {
  1063. id
  1064. }
  1065. }
  1066. }
  1067. `;
  1068. export const GET_ORDER_FULFILLMENTS = gql`
  1069. query GetOrderFulfillments($id: ID!) {
  1070. order(id: $id) {
  1071. id
  1072. fulfillments {
  1073. id
  1074. method
  1075. }
  1076. }
  1077. }
  1078. `;
  1079. export const GET_ORDER_LIST_FULFILLMENTS = gql`
  1080. query GetOrderListFulfillments {
  1081. orders {
  1082. items {
  1083. id
  1084. fulfillments {
  1085. id
  1086. method
  1087. }
  1088. }
  1089. }
  1090. }
  1091. `;
  1092. export const GET_ORDER_FULFILLMENT_ITEMS = gql`
  1093. query GetOrderFulfillmentItems($id: ID!) {
  1094. order(id: $id) {
  1095. id
  1096. fulfillments {
  1097. id
  1098. orderItems {
  1099. id
  1100. }
  1101. }
  1102. }
  1103. }
  1104. `;
  1105. export const CANCEL_ORDER = gql`
  1106. mutation CancelOrder($input: CancelOrderInput!) {
  1107. cancelOrder(input: $input) {
  1108. id
  1109. lines {
  1110. quantity
  1111. items {
  1112. id
  1113. cancelled
  1114. }
  1115. }
  1116. }
  1117. }
  1118. `;
  1119. export const REFUND_ORDER = gql`
  1120. mutation RefundOrder($input: RefundOrderInput!) {
  1121. refundOrder(input: $input) {
  1122. id
  1123. state
  1124. items
  1125. transactionId
  1126. shipping
  1127. total
  1128. metadata
  1129. }
  1130. }
  1131. `;
  1132. export const SETTLE_REFUND = gql`
  1133. mutation SettleRefund($input: SettleRefundInput!) {
  1134. settleRefund(input: $input) {
  1135. id
  1136. state
  1137. items
  1138. transactionId
  1139. shipping
  1140. total
  1141. metadata
  1142. }
  1143. }
  1144. `;
  1145. export const GET_ORDER_HISTORY = gql`
  1146. query GetOrderHistory($id: ID!, $options: HistoryEntryListOptions) {
  1147. order(id: $id) {
  1148. id
  1149. history(options: $options) {
  1150. totalItems
  1151. items {
  1152. id
  1153. type
  1154. administrator {
  1155. id
  1156. }
  1157. data
  1158. }
  1159. }
  1160. }
  1161. }
  1162. `;
  1163. export const ADD_NOTE_TO_ORDER = gql`
  1164. mutation AddNoteToOrder($input: AddNoteToOrderInput!) {
  1165. addNoteToOrder(input: $input) {
  1166. id
  1167. }
  1168. }
  1169. `;