order.e2e-spec.ts 37 KB

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