order.e2e-spec.ts 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710
  1. /* tslint:disable:no-non-null-assertion */
  2. import { pick } from '@vendure/common/lib/pick';
  3. import {
  4. createErrorResultGuard,
  5. createTestEnvironment,
  6. ErrorResultGuard,
  7. SimpleGraphQLClient,
  8. } from '@vendure/testing';
  9. import gql from 'graphql-tag';
  10. import path from 'path';
  11. import { initialData } from '../../../e2e-common/e2e-initial-data';
  12. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  13. import {
  14. failsToSettlePaymentMethod,
  15. onTransitionSpy,
  16. singleStageRefundablePaymentMethod,
  17. twoStagePaymentMethod,
  18. } from './fixtures/test-payment-methods';
  19. import { FULFILLMENT_FRAGMENT } from './graphql/fragments';
  20. import {
  21. AddNoteToOrder,
  22. CanceledOrderFragment,
  23. CancelOrder,
  24. CreateFulfillment,
  25. DeleteOrderNote,
  26. ErrorCode,
  27. FulfillmentFragment,
  28. GetCustomerList,
  29. GetOrder,
  30. GetOrderFulfillmentItems,
  31. GetOrderFulfillments,
  32. GetOrderHistory,
  33. GetOrderList,
  34. GetOrderListFulfillments,
  35. GetOrderWithPayments,
  36. GetProductWithVariants,
  37. GetStockMovement,
  38. GlobalFlag,
  39. HistoryEntryType,
  40. PaymentFragment,
  41. RefundFragment,
  42. RefundOrder,
  43. SettlePayment,
  44. SettleRefund,
  45. SortOrder,
  46. StockMovementType,
  47. TransitFulfillment,
  48. UpdateOrderNote,
  49. UpdateProductVariants,
  50. } from './graphql/generated-e2e-admin-types';
  51. import {
  52. AddItemToOrder,
  53. DeletionResult,
  54. GetActiveOrder,
  55. GetOrderByCodeWithPayments,
  56. TestOrderFragmentFragment,
  57. UpdatedOrder,
  58. } from './graphql/generated-e2e-shop-types';
  59. import {
  60. CANCEL_ORDER,
  61. CREATE_FULFILLMENT,
  62. GET_CUSTOMER_LIST,
  63. GET_ORDER,
  64. GET_ORDERS_LIST,
  65. GET_ORDER_FULFILLMENTS,
  66. GET_PRODUCT_WITH_VARIANTS,
  67. GET_STOCK_MOVEMENT,
  68. TRANSIT_FULFILLMENT,
  69. UPDATE_PRODUCT_VARIANTS,
  70. } from './graphql/shared-definitions';
  71. import {
  72. ADD_ITEM_TO_ORDER,
  73. GET_ACTIVE_ORDER,
  74. GET_ORDER_BY_CODE_WITH_PAYMENTS,
  75. } from './graphql/shop-definitions';
  76. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  77. import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
  78. describe('Orders resolver', () => {
  79. const { server, adminClient, shopClient } = createTestEnvironment({
  80. ...testConfig,
  81. paymentOptions: {
  82. paymentMethodHandlers: [
  83. twoStagePaymentMethod,
  84. failsToSettlePaymentMethod,
  85. singleStageRefundablePaymentMethod,
  86. ],
  87. },
  88. });
  89. let customers: GetCustomerList.Items[];
  90. const password = 'test';
  91. const orderGuard: ErrorResultGuard<
  92. TestOrderFragmentFragment | CanceledOrderFragment
  93. > = createErrorResultGuard<TestOrderFragmentFragment | CanceledOrderFragment>(input => !!input.lines);
  94. const paymentGuard: ErrorResultGuard<PaymentFragment> = createErrorResultGuard<PaymentFragment>(
  95. input => !!input.state,
  96. );
  97. const fulfillmentGuard: ErrorResultGuard<FulfillmentFragment> = createErrorResultGuard<
  98. FulfillmentFragment
  99. >(input => !!input.method);
  100. const refundGuard: ErrorResultGuard<RefundFragment> = createErrorResultGuard<RefundFragment>(
  101. input => !!input.items,
  102. );
  103. beforeAll(async () => {
  104. await server.init({
  105. initialData,
  106. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  107. customerCount: 3,
  108. });
  109. await adminClient.asSuperAdmin();
  110. // Create a couple of orders to be queried
  111. const result = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
  112. GET_CUSTOMER_LIST,
  113. {
  114. options: {
  115. take: 3,
  116. },
  117. },
  118. );
  119. customers = result.customers.items;
  120. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  121. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  122. productVariantId: 'T_1',
  123. quantity: 1,
  124. });
  125. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  126. productVariantId: 'T_2',
  127. quantity: 1,
  128. });
  129. await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
  130. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  131. productVariantId: 'T_2',
  132. quantity: 1,
  133. });
  134. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  135. productVariantId: 'T_3',
  136. quantity: 3,
  137. });
  138. }, TEST_SETUP_TIMEOUT_MS);
  139. afterAll(async () => {
  140. await server.destroy();
  141. });
  142. it('orders', async () => {
  143. const result = await adminClient.query<GetOrderList.Query>(GET_ORDERS_LIST);
  144. expect(result.orders.items.map(o => o.id)).toEqual(['T_1', 'T_2']);
  145. });
  146. it('order', async () => {
  147. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, { id: 'T_2' });
  148. expect(result.order!.id).toBe('T_2');
  149. });
  150. it('order history initially contains Created -> AddingItems transition', async () => {
  151. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
  152. GET_ORDER_HISTORY,
  153. { id: 'T_1' },
  154. );
  155. expect(order!.history.totalItems).toBe(1);
  156. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  157. {
  158. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  159. data: {
  160. from: 'Created',
  161. to: 'AddingItems',
  162. },
  163. },
  164. ]);
  165. });
  166. describe('payments', () => {
  167. let firstOrderCode: string;
  168. let firstOrderId: string;
  169. it('settlePayment fails', async () => {
  170. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  171. await proceedToArrangingPayment(shopClient);
  172. const order = await addPaymentToOrder(shopClient, failsToSettlePaymentMethod);
  173. orderGuard.assertSuccess(order);
  174. expect(order.state).toBe('PaymentAuthorized');
  175. const payment = order.payments![0];
  176. const { settlePayment } = await adminClient.query<
  177. SettlePayment.Mutation,
  178. SettlePayment.Variables
  179. >(SETTLE_PAYMENT, {
  180. id: payment.id,
  181. });
  182. paymentGuard.assertErrorResult(settlePayment);
  183. expect(settlePayment.message).toBe('Settling the payment failed');
  184. expect(settlePayment.errorCode).toBe(ErrorCode.SETTLE_PAYMENT_ERROR);
  185. expect((settlePayment as any).paymentErrorMessage).toBe('Something went horribly wrong');
  186. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  187. id: order.id,
  188. });
  189. expect(result.order!.state).toBe('PaymentAuthorized');
  190. firstOrderCode = order.code;
  191. firstOrderId = order.id;
  192. });
  193. it('public payment metadata available in Shop API', async () => {
  194. const { orderByCode } = await shopClient.query<
  195. GetOrderByCodeWithPayments.Query,
  196. GetOrderByCodeWithPayments.Variables
  197. >(GET_ORDER_BY_CODE_WITH_PAYMENTS, { code: firstOrderCode });
  198. expect(orderByCode?.payments?.[0].metadata).toEqual({
  199. public: {
  200. publicCreatePaymentData: 'public',
  201. publicSettlePaymentData: 'public',
  202. },
  203. });
  204. });
  205. it('public and private payment metadata available in Admin API', async () => {
  206. const { order } = await adminClient.query<
  207. GetOrderWithPayments.Query,
  208. GetOrderWithPayments.Variables
  209. >(GET_ORDER_WITH_PAYMENTS, { id: firstOrderId });
  210. expect(order?.payments?.[0].metadata).toEqual({
  211. privateCreatePaymentData: 'secret',
  212. privateSettlePaymentData: 'secret',
  213. public: {
  214. publicCreatePaymentData: 'public',
  215. publicSettlePaymentData: 'public',
  216. },
  217. });
  218. });
  219. it('settlePayment succeeds, onStateTransitionStart called', async () => {
  220. onTransitionSpy.mockClear();
  221. await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
  222. await proceedToArrangingPayment(shopClient);
  223. const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
  224. orderGuard.assertSuccess(order);
  225. expect(order.state).toBe('PaymentAuthorized');
  226. expect(onTransitionSpy).toHaveBeenCalledTimes(1);
  227. expect(onTransitionSpy.mock.calls[0][0]).toBe('Created');
  228. expect(onTransitionSpy.mock.calls[0][1]).toBe('Authorized');
  229. const payment = order.payments![0];
  230. const { settlePayment } = await adminClient.query<
  231. SettlePayment.Mutation,
  232. SettlePayment.Variables
  233. >(SETTLE_PAYMENT, {
  234. id: payment.id,
  235. });
  236. paymentGuard.assertSuccess(settlePayment);
  237. expect(settlePayment!.id).toBe(payment.id);
  238. expect(settlePayment!.state).toBe('Settled');
  239. // further metadata is combined into existing object
  240. expect(settlePayment!.metadata).toEqual({
  241. moreData: 42,
  242. public: {
  243. baz: 'quux',
  244. },
  245. });
  246. expect(onTransitionSpy).toHaveBeenCalledTimes(2);
  247. expect(onTransitionSpy.mock.calls[1][0]).toBe('Authorized');
  248. expect(onTransitionSpy.mock.calls[1][1]).toBe('Settled');
  249. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  250. id: order.id,
  251. });
  252. expect(result.order!.state).toBe('PaymentSettled');
  253. expect(result.order!.payments![0].state).toBe('Settled');
  254. });
  255. it('order history contains expected entries', async () => {
  256. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
  257. GET_ORDER_HISTORY,
  258. { id: 'T_2', options: { sort: { id: SortOrder.ASC } } },
  259. );
  260. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  261. {
  262. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  263. data: {
  264. from: 'Created',
  265. to: 'AddingItems',
  266. },
  267. },
  268. {
  269. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  270. data: {
  271. from: 'AddingItems',
  272. to: 'ArrangingPayment',
  273. },
  274. },
  275. {
  276. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  277. data: {
  278. paymentId: 'T_2',
  279. from: 'Created',
  280. to: 'Authorized',
  281. },
  282. },
  283. {
  284. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  285. data: {
  286. from: 'ArrangingPayment',
  287. to: 'PaymentAuthorized',
  288. },
  289. },
  290. {
  291. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  292. data: {
  293. paymentId: 'T_2',
  294. from: 'Authorized',
  295. to: 'Settled',
  296. },
  297. },
  298. {
  299. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  300. data: {
  301. from: 'PaymentAuthorized',
  302. to: 'PaymentSettled',
  303. },
  304. },
  305. ]);
  306. });
  307. });
  308. describe('fulfillment', () => {
  309. it('return error result if lines is empty', async () => {
  310. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  311. id: 'T_2',
  312. });
  313. expect(order!.state).toBe('PaymentSettled');
  314. const { addFulfillmentToOrder } = await adminClient.query<
  315. CreateFulfillment.Mutation,
  316. CreateFulfillment.Variables
  317. >(CREATE_FULFILLMENT, {
  318. input: {
  319. lines: [],
  320. method: 'Test',
  321. },
  322. });
  323. fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
  324. expect(addFulfillmentToOrder.message).toBe('At least one OrderLine must be specified');
  325. expect(addFulfillmentToOrder.errorCode).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
  326. });
  327. it('returns error result if all quantities are zero', async () => {
  328. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  329. id: 'T_2',
  330. });
  331. expect(order!.state).toBe('PaymentSettled');
  332. const { addFulfillmentToOrder } = await adminClient.query<
  333. CreateFulfillment.Mutation,
  334. CreateFulfillment.Variables
  335. >(CREATE_FULFILLMENT, {
  336. input: {
  337. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
  338. method: 'Test',
  339. },
  340. });
  341. fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
  342. expect(addFulfillmentToOrder.message).toBe('At least one OrderLine must be specified');
  343. expect(addFulfillmentToOrder.errorCode).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
  344. });
  345. it('creates the first fulfillment', async () => {
  346. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  347. id: 'T_2',
  348. });
  349. expect(order!.state).toBe('PaymentSettled');
  350. const lines = order!.lines;
  351. const { addFulfillmentToOrder } = await adminClient.query<
  352. CreateFulfillment.Mutation,
  353. CreateFulfillment.Variables
  354. >(CREATE_FULFILLMENT, {
  355. input: {
  356. lines: [{ orderLineId: lines[0].id, quantity: lines[0].quantity }],
  357. method: 'Test1',
  358. trackingCode: '111',
  359. },
  360. });
  361. fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
  362. expect(addFulfillmentToOrder.id).toBe('T_1');
  363. expect(addFulfillmentToOrder.method).toBe('Test1');
  364. expect(addFulfillmentToOrder.trackingCode).toBe('111');
  365. expect(addFulfillmentToOrder.state).toBe('Pending');
  366. expect(addFulfillmentToOrder.orderItems).toEqual([{ id: lines[0].items[0].id }]);
  367. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  368. id: 'T_2',
  369. });
  370. expect(result.order!.lines[0].items[0].fulfillment!.id).toBe(addFulfillmentToOrder!.id);
  371. expect(
  372. result.order!.lines[1].items.filter(
  373. i => i.fulfillment && i.fulfillment.id === addFulfillmentToOrder.id,
  374. ).length,
  375. ).toBe(0);
  376. expect(result.order!.lines[1].items.filter(i => i.fulfillment == null).length).toBe(3);
  377. });
  378. it('creates the second fulfillment', async () => {
  379. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  380. id: 'T_2',
  381. });
  382. const unfulfilledItems =
  383. order?.lines.filter(l => {
  384. const items = l.items.filter(i => i.fulfillment === null);
  385. return items.length > 0 ? true : false;
  386. }) || [];
  387. const { addFulfillmentToOrder } = await adminClient.query<
  388. CreateFulfillment.Mutation,
  389. CreateFulfillment.Variables
  390. >(CREATE_FULFILLMENT, {
  391. input: {
  392. lines: unfulfilledItems.map(l => ({
  393. orderLineId: l.id,
  394. quantity: l.items.length,
  395. })),
  396. method: 'Test2',
  397. trackingCode: '222',
  398. },
  399. });
  400. fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
  401. expect(addFulfillmentToOrder.id).toBe('T_2');
  402. expect(addFulfillmentToOrder.method).toBe('Test2');
  403. expect(addFulfillmentToOrder.trackingCode).toBe('222');
  404. expect(addFulfillmentToOrder.state).toBe('Pending');
  405. });
  406. it('returns error result if an OrderItem already part of a Fulfillment', async () => {
  407. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  408. id: 'T_2',
  409. });
  410. const { addFulfillmentToOrder } = await adminClient.query<
  411. CreateFulfillment.Mutation,
  412. CreateFulfillment.Variables
  413. >(CREATE_FULFILLMENT, {
  414. input: {
  415. method: 'Test',
  416. lines: [
  417. {
  418. orderLineId: order!.lines[0].id,
  419. quantity: 1,
  420. },
  421. ],
  422. },
  423. });
  424. fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
  425. expect(addFulfillmentToOrder.message).toBe(
  426. 'One or more OrderItems are already part of a Fulfillment',
  427. );
  428. expect(addFulfillmentToOrder.errorCode).toBe(ErrorCode.ITEMS_ALREADY_FULFILLED_ERROR);
  429. });
  430. it('transits the first fulfillment from created to Shipped and automatically change the order state to PartiallyShipped', async () => {
  431. const { transitionFulfillmentToState } = await adminClient.query<
  432. TransitFulfillment.Mutation,
  433. TransitFulfillment.Variables
  434. >(TRANSIT_FULFILLMENT, {
  435. id: 'T_1',
  436. state: 'Shipped',
  437. });
  438. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  439. expect(transitionFulfillmentToState.id).toBe('T_1');
  440. expect(transitionFulfillmentToState.state).toBe('Shipped');
  441. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  442. id: 'T_2',
  443. });
  444. expect(order?.state).toBe('PartiallyShipped');
  445. });
  446. it('transits the second fulfillment from created to Shipped and automatically change the order state to Shipped', async () => {
  447. const { transitionFulfillmentToState } = await adminClient.query<
  448. TransitFulfillment.Mutation,
  449. TransitFulfillment.Variables
  450. >(TRANSIT_FULFILLMENT, {
  451. id: 'T_2',
  452. state: 'Shipped',
  453. });
  454. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  455. expect(transitionFulfillmentToState.id).toBe('T_2');
  456. expect(transitionFulfillmentToState.state).toBe('Shipped');
  457. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  458. id: 'T_2',
  459. });
  460. expect(order?.state).toBe('Shipped');
  461. });
  462. it('transits the first fulfillment from Shipped to Delivered and change the order state to PartiallyDelivered', async () => {
  463. const { transitionFulfillmentToState } = await adminClient.query<
  464. TransitFulfillment.Mutation,
  465. TransitFulfillment.Variables
  466. >(TRANSIT_FULFILLMENT, {
  467. id: 'T_1',
  468. state: 'Delivered',
  469. });
  470. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  471. expect(transitionFulfillmentToState.id).toBe('T_1');
  472. expect(transitionFulfillmentToState.state).toBe('Delivered');
  473. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  474. id: 'T_2',
  475. });
  476. expect(order?.state).toBe('PartiallyDelivered');
  477. });
  478. it('transits the second fulfillment from Shipped to Delivered and change the order state to Delivered', async () => {
  479. const { transitionFulfillmentToState } = await adminClient.query<
  480. TransitFulfillment.Mutation,
  481. TransitFulfillment.Variables
  482. >(TRANSIT_FULFILLMENT, {
  483. id: 'T_2',
  484. state: 'Delivered',
  485. });
  486. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  487. expect(transitionFulfillmentToState.id).toBe('T_2');
  488. expect(transitionFulfillmentToState.state).toBe('Delivered');
  489. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  490. id: 'T_2',
  491. });
  492. expect(order?.state).toBe('Delivered');
  493. });
  494. it('order history contains expected entries', async () => {
  495. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
  496. GET_ORDER_HISTORY,
  497. {
  498. id: 'T_2',
  499. options: {
  500. skip: 6,
  501. },
  502. },
  503. );
  504. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  505. {
  506. data: {
  507. fulfillmentId: 'T_1',
  508. },
  509. type: HistoryEntryType.ORDER_FULFILLMENT,
  510. },
  511. {
  512. data: {
  513. from: 'Created',
  514. fulfillmentId: 'T_1',
  515. to: 'Pending',
  516. },
  517. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  518. },
  519. {
  520. data: {
  521. fulfillmentId: 'T_2',
  522. },
  523. type: HistoryEntryType.ORDER_FULFILLMENT,
  524. },
  525. {
  526. data: {
  527. from: 'Created',
  528. fulfillmentId: 'T_2',
  529. to: 'Pending',
  530. },
  531. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  532. },
  533. {
  534. data: {
  535. from: 'Pending',
  536. fulfillmentId: 'T_1',
  537. to: 'Shipped',
  538. },
  539. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  540. },
  541. {
  542. data: {
  543. from: 'PaymentSettled',
  544. to: 'PartiallyShipped',
  545. },
  546. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  547. },
  548. {
  549. data: {
  550. from: 'Pending',
  551. fulfillmentId: 'T_2',
  552. to: 'Shipped',
  553. },
  554. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  555. },
  556. {
  557. data: {
  558. from: 'PartiallyShipped',
  559. to: 'Shipped',
  560. },
  561. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  562. },
  563. {
  564. data: {
  565. from: 'Shipped',
  566. fulfillmentId: 'T_1',
  567. to: 'Delivered',
  568. },
  569. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  570. },
  571. {
  572. data: {
  573. from: 'Shipped',
  574. to: 'PartiallyDelivered',
  575. },
  576. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  577. },
  578. {
  579. data: {
  580. from: 'Shipped',
  581. fulfillmentId: 'T_2',
  582. to: 'Delivered',
  583. },
  584. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  585. },
  586. {
  587. data: {
  588. from: 'PartiallyDelivered',
  589. to: 'Delivered',
  590. },
  591. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  592. },
  593. ]);
  594. });
  595. it('order.fulfillments resolver for single order', async () => {
  596. const { order } = await adminClient.query<
  597. GetOrderFulfillments.Query,
  598. GetOrderFulfillments.Variables
  599. >(GET_ORDER_FULFILLMENTS, {
  600. id: 'T_2',
  601. });
  602. expect(order!.fulfillments).toEqual([
  603. { id: 'T_1', method: 'Test1', state: 'Delivered', nextStates: ['Cancelled'] },
  604. { id: 'T_2', method: 'Test2', state: 'Delivered', nextStates: ['Cancelled'] },
  605. ]);
  606. });
  607. it('order.fulfillments resolver for order list', async () => {
  608. const { orders } = await adminClient.query<GetOrderListFulfillments.Query>(
  609. GET_ORDER_LIST_FULFILLMENTS,
  610. );
  611. expect(orders.items[0].fulfillments).toEqual([]);
  612. expect(orders.items[1].fulfillments).toEqual([
  613. { id: 'T_1', method: 'Test1', state: 'Delivered', nextStates: ['Cancelled'] },
  614. { id: 'T_2', method: 'Test2', state: 'Delivered', nextStates: ['Cancelled'] },
  615. ]);
  616. });
  617. it('order.fulfillments.orderItems resolver', async () => {
  618. const { order } = await adminClient.query<
  619. GetOrderFulfillmentItems.Query,
  620. GetOrderFulfillmentItems.Variables
  621. >(GET_ORDER_FULFILLMENT_ITEMS, {
  622. id: 'T_2',
  623. });
  624. expect(order!.fulfillments![0].orderItems).toEqual([{ id: 'T_3' }]);
  625. expect(order!.fulfillments![1].orderItems).toEqual([{ id: 'T_4' }, { id: 'T_5' }, { id: 'T_6' }]);
  626. });
  627. });
  628. describe('cancellation by orderId', () => {
  629. it('cancel from AddingItems state', async () => {
  630. const testOrder = await createTestOrder(
  631. adminClient,
  632. shopClient,
  633. customers[0].emailAddress,
  634. password,
  635. );
  636. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  637. id: testOrder.orderId,
  638. });
  639. expect(order!.state).toBe('AddingItems');
  640. const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
  641. CANCEL_ORDER,
  642. {
  643. input: {
  644. orderId: testOrder.orderId,
  645. },
  646. },
  647. );
  648. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  649. id: testOrder.orderId,
  650. });
  651. expect(order2!.state).toBe('Cancelled');
  652. expect(order2!.active).toBe(false);
  653. await assertNoStockMovementsCreated(testOrder.product.id);
  654. });
  655. it('cancel from ArrangingPayment state', async () => {
  656. const testOrder = await createTestOrder(
  657. adminClient,
  658. shopClient,
  659. customers[0].emailAddress,
  660. password,
  661. );
  662. await proceedToArrangingPayment(shopClient);
  663. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  664. id: testOrder.orderId,
  665. });
  666. expect(order!.state).toBe('ArrangingPayment');
  667. await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
  668. input: {
  669. orderId: testOrder.orderId,
  670. },
  671. });
  672. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  673. id: testOrder.orderId,
  674. });
  675. expect(order2!.state).toBe('Cancelled');
  676. expect(order2!.active).toBe(false);
  677. await assertNoStockMovementsCreated(testOrder.product.id);
  678. });
  679. it('cancel from PaymentAuthorized state', async () => {
  680. const testOrder = await createTestOrder(
  681. adminClient,
  682. shopClient,
  683. customers[0].emailAddress,
  684. password,
  685. );
  686. await proceedToArrangingPayment(shopClient);
  687. const order = await addPaymentToOrder(shopClient, failsToSettlePaymentMethod);
  688. orderGuard.assertSuccess(order);
  689. expect(order.state).toBe('PaymentAuthorized');
  690. const result1 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  691. GET_STOCK_MOVEMENT,
  692. {
  693. id: 'T_3',
  694. },
  695. );
  696. let variant1 = result1.product!.variants[0];
  697. expect(variant1.stockOnHand).toBe(100);
  698. expect(variant1.stockAllocated).toBe(2);
  699. expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  700. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  701. { type: StockMovementType.ALLOCATION, quantity: 2 },
  702. ]);
  703. const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
  704. CANCEL_ORDER,
  705. {
  706. input: {
  707. orderId: testOrder.orderId,
  708. },
  709. },
  710. );
  711. orderGuard.assertSuccess(cancelOrder);
  712. expect(
  713. cancelOrder.lines.map(l =>
  714. l.items.map(pick(['id', 'cancelled'])).sort((a, b) => (a.id > b.id ? 1 : -1)),
  715. ),
  716. ).toEqual([
  717. [
  718. { id: 'T_11', cancelled: true },
  719. { id: 'T_12', cancelled: true },
  720. ],
  721. ]);
  722. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  723. id: testOrder.orderId,
  724. });
  725. expect(order2!.active).toBe(false);
  726. expect(order2!.state).toBe('Cancelled');
  727. const result2 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  728. GET_STOCK_MOVEMENT,
  729. {
  730. id: 'T_3',
  731. },
  732. );
  733. variant1 = result2.product!.variants[0];
  734. expect(variant1.stockOnHand).toBe(100);
  735. expect(variant1.stockAllocated).toBe(0);
  736. expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  737. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  738. { type: StockMovementType.ALLOCATION, quantity: 2 },
  739. { type: StockMovementType.RELEASE, quantity: 1 },
  740. { type: StockMovementType.RELEASE, quantity: 1 },
  741. ]);
  742. });
  743. async function assertNoStockMovementsCreated(productId: string) {
  744. const result = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  745. GET_STOCK_MOVEMENT,
  746. {
  747. id: productId,
  748. },
  749. );
  750. const variant2 = result.product!.variants[0];
  751. expect(variant2.stockOnHand).toBe(100);
  752. expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  753. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  754. ]);
  755. }
  756. });
  757. describe('cancellation by OrderLine', () => {
  758. let orderId: string;
  759. let product: GetProductWithVariants.Product;
  760. let productVariantId: string;
  761. beforeAll(async () => {
  762. const result = await createTestOrder(
  763. adminClient,
  764. shopClient,
  765. customers[0].emailAddress,
  766. password,
  767. );
  768. orderId = result.orderId;
  769. product = result.product;
  770. productVariantId = result.productVariantId;
  771. });
  772. it('cannot cancel from AddingItems state', async () => {
  773. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  774. id: orderId,
  775. });
  776. expect(order!.state).toBe('AddingItems');
  777. const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
  778. CANCEL_ORDER,
  779. {
  780. input: {
  781. orderId,
  782. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  783. },
  784. },
  785. );
  786. orderGuard.assertErrorResult(cancelOrder);
  787. expect(cancelOrder.message).toBe(
  788. 'Cannot cancel OrderLines from an Order in the "AddingItems" state',
  789. );
  790. expect(cancelOrder.errorCode).toBe(ErrorCode.CANCEL_ACTIVE_ORDER_ERROR);
  791. });
  792. it('cannot cancel from ArrangingPayment state', async () => {
  793. await proceedToArrangingPayment(shopClient);
  794. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  795. id: orderId,
  796. });
  797. expect(order!.state).toBe('ArrangingPayment');
  798. const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
  799. CANCEL_ORDER,
  800. {
  801. input: {
  802. orderId,
  803. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  804. },
  805. },
  806. );
  807. orderGuard.assertErrorResult(cancelOrder);
  808. expect(cancelOrder.message).toBe(
  809. 'Cannot cancel OrderLines from an Order in the "ArrangingPayment" state',
  810. );
  811. expect(cancelOrder.errorCode).toBe(ErrorCode.CANCEL_ACTIVE_ORDER_ERROR);
  812. });
  813. it('returns error result if lines are empty', async () => {
  814. const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
  815. orderGuard.assertSuccess(order);
  816. expect(order.state).toBe('PaymentAuthorized');
  817. const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
  818. CANCEL_ORDER,
  819. {
  820. input: {
  821. orderId,
  822. lines: [],
  823. },
  824. },
  825. );
  826. orderGuard.assertErrorResult(cancelOrder);
  827. expect(cancelOrder.message).toBe('At least one OrderLine must be specified');
  828. expect(cancelOrder.errorCode).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
  829. });
  830. it('returns error result if all quantities zero', async () => {
  831. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  832. id: orderId,
  833. });
  834. const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
  835. CANCEL_ORDER,
  836. {
  837. input: {
  838. orderId,
  839. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
  840. },
  841. },
  842. );
  843. orderGuard.assertErrorResult(cancelOrder);
  844. expect(cancelOrder.message).toBe('At least one OrderLine must be specified');
  845. expect(cancelOrder.errorCode).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
  846. });
  847. it('partial cancellation', async () => {
  848. const result1 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  849. GET_STOCK_MOVEMENT,
  850. {
  851. id: product.id,
  852. },
  853. );
  854. const variant1 = result1.product!.variants[0];
  855. expect(variant1.stockOnHand).toBe(100);
  856. expect(variant1.stockAllocated).toBe(2);
  857. expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  858. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  859. { type: StockMovementType.ALLOCATION, quantity: 2 },
  860. { type: StockMovementType.RELEASE, quantity: 1 },
  861. { type: StockMovementType.RELEASE, quantity: 1 },
  862. { type: StockMovementType.ALLOCATION, quantity: 2 },
  863. ]);
  864. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  865. id: orderId,
  866. });
  867. const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
  868. CANCEL_ORDER,
  869. {
  870. input: {
  871. orderId,
  872. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  873. reason: 'cancel reason 1',
  874. },
  875. },
  876. );
  877. orderGuard.assertSuccess(cancelOrder);
  878. expect(cancelOrder.lines[0].quantity).toBe(1);
  879. expect(cancelOrder.lines[0].items.sort((a, b) => (a.id < b.id ? -1 : 1))).toEqual([
  880. { id: 'T_13', cancelled: true },
  881. { id: 'T_14', cancelled: false },
  882. ]);
  883. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  884. id: orderId,
  885. });
  886. expect(order2!.state).toBe('PaymentAuthorized');
  887. expect(order2!.lines[0].quantity).toBe(1);
  888. const result2 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  889. GET_STOCK_MOVEMENT,
  890. {
  891. id: product.id,
  892. },
  893. );
  894. const variant2 = result2.product!.variants[0];
  895. expect(variant2.stockOnHand).toBe(100);
  896. expect(variant2.stockAllocated).toBe(1);
  897. expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  898. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  899. { type: StockMovementType.ALLOCATION, quantity: 2 },
  900. { type: StockMovementType.RELEASE, quantity: 1 },
  901. { type: StockMovementType.RELEASE, quantity: 1 },
  902. { type: StockMovementType.ALLOCATION, quantity: 2 },
  903. { type: StockMovementType.RELEASE, quantity: 1 },
  904. ]);
  905. });
  906. it('returns error result if attempting to cancel already cancelled item', async () => {
  907. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  908. id: orderId,
  909. });
  910. const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
  911. CANCEL_ORDER,
  912. {
  913. input: {
  914. orderId,
  915. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 2 })),
  916. },
  917. },
  918. );
  919. orderGuard.assertErrorResult(cancelOrder);
  920. expect(cancelOrder.message).toBe(
  921. 'The specified quantity is greater than the available OrderItems',
  922. );
  923. expect(cancelOrder.errorCode).toBe(ErrorCode.QUANTITY_TOO_GREAT_ERROR);
  924. });
  925. it('complete cancellation', async () => {
  926. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  927. id: orderId,
  928. });
  929. await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
  930. input: {
  931. orderId,
  932. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  933. reason: 'cancel reason 2',
  934. },
  935. });
  936. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  937. id: orderId,
  938. });
  939. expect(order2!.state).toBe('Cancelled');
  940. const result = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  941. GET_STOCK_MOVEMENT,
  942. {
  943. id: product.id,
  944. },
  945. );
  946. const variant2 = result.product!.variants[0];
  947. expect(variant2.stockOnHand).toBe(100);
  948. expect(variant2.stockAllocated).toBe(0);
  949. expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  950. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  951. { type: StockMovementType.ALLOCATION, quantity: 2 },
  952. { type: StockMovementType.RELEASE, quantity: 1 },
  953. { type: StockMovementType.RELEASE, quantity: 1 },
  954. { type: StockMovementType.ALLOCATION, quantity: 2 },
  955. { type: StockMovementType.RELEASE, quantity: 1 },
  956. { type: StockMovementType.RELEASE, quantity: 1 },
  957. ]);
  958. });
  959. it('order history contains expected entries', async () => {
  960. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
  961. GET_ORDER_HISTORY,
  962. {
  963. id: orderId,
  964. options: {
  965. skip: 0,
  966. },
  967. },
  968. );
  969. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  970. {
  971. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  972. data: {
  973. from: 'Created',
  974. to: 'AddingItems',
  975. },
  976. },
  977. {
  978. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  979. data: {
  980. from: 'AddingItems',
  981. to: 'ArrangingPayment',
  982. },
  983. },
  984. {
  985. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  986. data: {
  987. paymentId: 'T_4',
  988. from: 'Created',
  989. to: 'Authorized',
  990. },
  991. },
  992. {
  993. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  994. data: {
  995. from: 'ArrangingPayment',
  996. to: 'PaymentAuthorized',
  997. },
  998. },
  999. {
  1000. type: HistoryEntryType.ORDER_CANCELLATION,
  1001. data: {
  1002. orderItemIds: ['T_13'],
  1003. reason: 'cancel reason 1',
  1004. },
  1005. },
  1006. {
  1007. type: HistoryEntryType.ORDER_CANCELLATION,
  1008. data: {
  1009. orderItemIds: ['T_14'],
  1010. reason: 'cancel reason 2',
  1011. },
  1012. },
  1013. {
  1014. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1015. data: {
  1016. from: 'PaymentAuthorized',
  1017. to: 'Cancelled',
  1018. },
  1019. },
  1020. ]);
  1021. });
  1022. });
  1023. describe('refunds', () => {
  1024. let orderId: string;
  1025. let product: GetProductWithVariants.Product;
  1026. let productVariantId: string;
  1027. let paymentId: string;
  1028. let refundId: string;
  1029. beforeAll(async () => {
  1030. const result = await createTestOrder(
  1031. adminClient,
  1032. shopClient,
  1033. customers[0].emailAddress,
  1034. password,
  1035. );
  1036. orderId = result.orderId;
  1037. product = result.product;
  1038. productVariantId = result.productVariantId;
  1039. });
  1040. it('cannot refund from PaymentAuthorized state', async () => {
  1041. await proceedToArrangingPayment(shopClient);
  1042. const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
  1043. orderGuard.assertSuccess(order);
  1044. expect(order.state).toBe('PaymentAuthorized');
  1045. paymentId = order.payments![0].id;
  1046. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
  1047. REFUND_ORDER,
  1048. {
  1049. input: {
  1050. lines: order.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  1051. shipping: 0,
  1052. adjustment: 0,
  1053. paymentId,
  1054. },
  1055. },
  1056. );
  1057. refundGuard.assertErrorResult(refundOrder);
  1058. expect(refundOrder.message).toBe('Cannot refund an Order in the "PaymentAuthorized" state');
  1059. expect(refundOrder.errorCode).toBe(ErrorCode.REFUND_ORDER_STATE_ERROR);
  1060. });
  1061. it('returns error result if no lines and no shipping', async () => {
  1062. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1063. id: orderId,
  1064. });
  1065. const { settlePayment } = await adminClient.query<
  1066. SettlePayment.Mutation,
  1067. SettlePayment.Variables
  1068. >(SETTLE_PAYMENT, {
  1069. id: order!.payments![0].id,
  1070. });
  1071. paymentGuard.assertSuccess(settlePayment);
  1072. expect(settlePayment!.state).toBe('Settled');
  1073. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
  1074. REFUND_ORDER,
  1075. {
  1076. input: {
  1077. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
  1078. shipping: 0,
  1079. adjustment: 0,
  1080. paymentId,
  1081. },
  1082. },
  1083. );
  1084. refundGuard.assertErrorResult(refundOrder);
  1085. expect(refundOrder.message).toBe('Nothing to refund');
  1086. expect(refundOrder.errorCode).toBe(ErrorCode.NOTHING_TO_REFUND_ERROR);
  1087. });
  1088. it(
  1089. 'throws if paymentId not valid',
  1090. assertThrowsWithMessage(async () => {
  1091. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1092. id: orderId,
  1093. });
  1094. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
  1095. REFUND_ORDER,
  1096. {
  1097. input: {
  1098. lines: [],
  1099. shipping: 100,
  1100. adjustment: 0,
  1101. paymentId: 'T_999',
  1102. },
  1103. },
  1104. );
  1105. }, `No Payment with the id '999' could be found`),
  1106. );
  1107. it('returns error result if payment and order lines do not belong to the same Order', async () => {
  1108. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1109. id: orderId,
  1110. });
  1111. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
  1112. REFUND_ORDER,
  1113. {
  1114. input: {
  1115. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  1116. shipping: 100,
  1117. adjustment: 0,
  1118. paymentId: 'T_1',
  1119. },
  1120. },
  1121. );
  1122. refundGuard.assertErrorResult(refundOrder);
  1123. expect(refundOrder.message).toBe('The Payment and OrderLines do not belong to the same Order');
  1124. expect(refundOrder.errorCode).toBe(ErrorCode.PAYMENT_ORDER_MISMATCH_ERROR);
  1125. });
  1126. it('creates a Refund to be manually settled', async () => {
  1127. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1128. id: orderId,
  1129. });
  1130. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
  1131. REFUND_ORDER,
  1132. {
  1133. input: {
  1134. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  1135. shipping: order!.shipping,
  1136. adjustment: 0,
  1137. reason: 'foo',
  1138. paymentId,
  1139. },
  1140. },
  1141. );
  1142. refundGuard.assertSuccess(refundOrder);
  1143. expect(refundOrder.shipping).toBe(order!.shipping);
  1144. expect(refundOrder.items).toBe(order!.subTotal);
  1145. expect(refundOrder.total).toBe(order!.total);
  1146. expect(refundOrder.transactionId).toBe(null);
  1147. expect(refundOrder.state).toBe('Pending');
  1148. refundId = refundOrder.id;
  1149. });
  1150. it('returns error result if attempting to refund the same item more than once', async () => {
  1151. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1152. id: orderId,
  1153. });
  1154. const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
  1155. REFUND_ORDER,
  1156. {
  1157. input: {
  1158. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  1159. shipping: order!.shipping,
  1160. adjustment: 0,
  1161. paymentId,
  1162. },
  1163. },
  1164. );
  1165. refundGuard.assertErrorResult(refundOrder);
  1166. expect(refundOrder.message).toBe('Cannot refund an OrderItem which has already been refunded');
  1167. expect(refundOrder.errorCode).toBe(ErrorCode.ALREADY_REFUNDED_ERROR);
  1168. });
  1169. it('manually settle a Refund', async () => {
  1170. const { settleRefund } = await adminClient.query<SettleRefund.Mutation, SettleRefund.Variables>(
  1171. SETTLE_REFUND,
  1172. {
  1173. input: {
  1174. id: refundId,
  1175. transactionId: 'aaabbb',
  1176. },
  1177. },
  1178. );
  1179. refundGuard.assertSuccess(settleRefund);
  1180. expect(settleRefund.state).toBe('Settled');
  1181. expect(settleRefund.transactionId).toBe('aaabbb');
  1182. });
  1183. it('order history contains expected entries', async () => {
  1184. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
  1185. GET_ORDER_HISTORY,
  1186. {
  1187. id: orderId,
  1188. options: {
  1189. skip: 0,
  1190. },
  1191. },
  1192. );
  1193. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  1194. {
  1195. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1196. data: {
  1197. from: 'Created',
  1198. to: 'AddingItems',
  1199. },
  1200. },
  1201. {
  1202. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1203. data: {
  1204. from: 'AddingItems',
  1205. to: 'ArrangingPayment',
  1206. },
  1207. },
  1208. {
  1209. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  1210. data: {
  1211. paymentId: 'T_5',
  1212. from: 'Created',
  1213. to: 'Authorized',
  1214. },
  1215. },
  1216. {
  1217. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1218. data: {
  1219. from: 'ArrangingPayment',
  1220. to: 'PaymentAuthorized',
  1221. },
  1222. },
  1223. {
  1224. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  1225. data: {
  1226. paymentId: 'T_5',
  1227. from: 'Authorized',
  1228. to: 'Settled',
  1229. },
  1230. },
  1231. {
  1232. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1233. data: {
  1234. from: 'PaymentAuthorized',
  1235. to: 'PaymentSettled',
  1236. },
  1237. },
  1238. {
  1239. type: HistoryEntryType.ORDER_REFUND_TRANSITION,
  1240. data: {
  1241. refundId: 'T_1',
  1242. reason: 'foo',
  1243. from: 'Pending',
  1244. to: 'Settled',
  1245. },
  1246. },
  1247. ]);
  1248. });
  1249. });
  1250. describe('order notes', () => {
  1251. let orderId: string;
  1252. let firstNoteId: string;
  1253. beforeAll(async () => {
  1254. const result = await createTestOrder(
  1255. adminClient,
  1256. shopClient,
  1257. customers[2].emailAddress,
  1258. password,
  1259. );
  1260. orderId = result.orderId;
  1261. });
  1262. it('private note', async () => {
  1263. const { addNoteToOrder } = await adminClient.query<
  1264. AddNoteToOrder.Mutation,
  1265. AddNoteToOrder.Variables
  1266. >(ADD_NOTE_TO_ORDER, {
  1267. input: {
  1268. id: orderId,
  1269. note: 'A private note',
  1270. isPublic: false,
  1271. },
  1272. });
  1273. expect(addNoteToOrder.id).toBe(orderId);
  1274. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
  1275. GET_ORDER_HISTORY,
  1276. {
  1277. id: orderId,
  1278. options: {
  1279. skip: 1,
  1280. },
  1281. },
  1282. );
  1283. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  1284. {
  1285. type: HistoryEntryType.ORDER_NOTE,
  1286. data: {
  1287. note: 'A private note',
  1288. },
  1289. },
  1290. ]);
  1291. firstNoteId = order!.history.items[0].id;
  1292. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  1293. expect(activeOrder!.history.items.map(pick(['type']))).toEqual([
  1294. { type: HistoryEntryType.ORDER_STATE_TRANSITION },
  1295. ]);
  1296. });
  1297. it('public note', async () => {
  1298. const { addNoteToOrder } = await adminClient.query<
  1299. AddNoteToOrder.Mutation,
  1300. AddNoteToOrder.Variables
  1301. >(ADD_NOTE_TO_ORDER, {
  1302. input: {
  1303. id: orderId,
  1304. note: 'A public note',
  1305. isPublic: true,
  1306. },
  1307. });
  1308. expect(addNoteToOrder.id).toBe(orderId);
  1309. const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
  1310. GET_ORDER_HISTORY,
  1311. {
  1312. id: orderId,
  1313. options: {
  1314. skip: 2,
  1315. },
  1316. },
  1317. );
  1318. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  1319. {
  1320. type: HistoryEntryType.ORDER_NOTE,
  1321. data: {
  1322. note: 'A public note',
  1323. },
  1324. },
  1325. ]);
  1326. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  1327. expect(activeOrder!.history.items.map(pick(['type', 'data']))).toEqual([
  1328. {
  1329. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1330. data: {
  1331. from: 'Created',
  1332. to: 'AddingItems',
  1333. },
  1334. },
  1335. {
  1336. type: HistoryEntryType.ORDER_NOTE,
  1337. data: {
  1338. note: 'A public note',
  1339. },
  1340. },
  1341. ]);
  1342. });
  1343. it('update note', async () => {
  1344. const { updateOrderNote } = await adminClient.query<
  1345. UpdateOrderNote.Mutation,
  1346. UpdateOrderNote.Variables
  1347. >(UPDATE_ORDER_NOTE, {
  1348. input: {
  1349. noteId: firstNoteId,
  1350. note: 'An updated note',
  1351. },
  1352. });
  1353. expect(updateOrderNote.data).toEqual({
  1354. note: 'An updated note',
  1355. });
  1356. });
  1357. it('delete note', async () => {
  1358. const { order: before } = await adminClient.query<
  1359. GetOrderHistory.Query,
  1360. GetOrderHistory.Variables
  1361. >(GET_ORDER_HISTORY, { id: orderId });
  1362. expect(before?.history.totalItems).toBe(3);
  1363. const { deleteOrderNote } = await adminClient.query<
  1364. DeleteOrderNote.Mutation,
  1365. DeleteOrderNote.Variables
  1366. >(DELETE_ORDER_NOTE, {
  1367. id: firstNoteId,
  1368. });
  1369. expect(deleteOrderNote.result).toBe(DeletionResult.DELETED);
  1370. const { order: after } = await adminClient.query<
  1371. GetOrderHistory.Query,
  1372. GetOrderHistory.Variables
  1373. >(GET_ORDER_HISTORY, { id: orderId });
  1374. expect(after?.history.totalItems).toBe(2);
  1375. });
  1376. });
  1377. });
  1378. async function createTestOrder(
  1379. adminClient: SimpleGraphQLClient,
  1380. shopClient: SimpleGraphQLClient,
  1381. emailAddress: string,
  1382. password: string,
  1383. ): Promise<{
  1384. orderId: string;
  1385. product: GetProductWithVariants.Product;
  1386. productVariantId: string;
  1387. }> {
  1388. const result = await adminClient.query<GetProductWithVariants.Query, GetProductWithVariants.Variables>(
  1389. GET_PRODUCT_WITH_VARIANTS,
  1390. {
  1391. id: 'T_3',
  1392. },
  1393. );
  1394. const product = result.product!;
  1395. const productVariantId = product.variants[0].id;
  1396. // Set the ProductVariant to trackInventory
  1397. const { updateProductVariants } = await adminClient.query<
  1398. UpdateProductVariants.Mutation,
  1399. UpdateProductVariants.Variables
  1400. >(UPDATE_PRODUCT_VARIANTS, {
  1401. input: [
  1402. {
  1403. id: productVariantId,
  1404. trackInventory: GlobalFlag.TRUE,
  1405. },
  1406. ],
  1407. });
  1408. // Add the ProductVariant to the Order
  1409. await shopClient.asUserWithCredentials(emailAddress, password);
  1410. const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
  1411. ADD_ITEM_TO_ORDER,
  1412. {
  1413. productVariantId,
  1414. quantity: 2,
  1415. },
  1416. );
  1417. const orderId = (addItemToOrder as UpdatedOrder.Fragment).id;
  1418. return { product, productVariantId, orderId };
  1419. }
  1420. export const SETTLE_PAYMENT = gql`
  1421. mutation SettlePayment($id: ID!) {
  1422. settlePayment(id: $id) {
  1423. ...Payment
  1424. ... on ErrorResult {
  1425. errorCode
  1426. message
  1427. }
  1428. ... on SettlePaymentError {
  1429. paymentErrorMessage
  1430. }
  1431. }
  1432. }
  1433. fragment Payment on Payment {
  1434. id
  1435. state
  1436. metadata
  1437. }
  1438. `;
  1439. export const GET_ORDER_LIST_FULFILLMENTS = gql`
  1440. query GetOrderListFulfillments {
  1441. orders {
  1442. items {
  1443. id
  1444. state
  1445. fulfillments {
  1446. id
  1447. state
  1448. nextStates
  1449. method
  1450. }
  1451. }
  1452. }
  1453. }
  1454. `;
  1455. export const GET_ORDER_FULFILLMENT_ITEMS = gql`
  1456. query GetOrderFulfillmentItems($id: ID!) {
  1457. order(id: $id) {
  1458. id
  1459. state
  1460. fulfillments {
  1461. ...Fulfillment
  1462. }
  1463. }
  1464. }
  1465. ${FULFILLMENT_FRAGMENT}
  1466. `;
  1467. const REFUND_FRAGMENT = gql`
  1468. fragment Refund on Refund {
  1469. id
  1470. state
  1471. items
  1472. transactionId
  1473. shipping
  1474. total
  1475. metadata
  1476. }
  1477. `;
  1478. export const REFUND_ORDER = gql`
  1479. mutation RefundOrder($input: RefundOrderInput!) {
  1480. refundOrder(input: $input) {
  1481. ...Refund
  1482. ... on ErrorResult {
  1483. errorCode
  1484. message
  1485. }
  1486. }
  1487. }
  1488. ${REFUND_FRAGMENT}
  1489. `;
  1490. export const SETTLE_REFUND = gql`
  1491. mutation SettleRefund($input: SettleRefundInput!) {
  1492. settleRefund(input: $input) {
  1493. ...Refund
  1494. ... on ErrorResult {
  1495. errorCode
  1496. message
  1497. }
  1498. }
  1499. }
  1500. ${REFUND_FRAGMENT}
  1501. `;
  1502. export const GET_ORDER_HISTORY = gql`
  1503. query GetOrderHistory($id: ID!, $options: HistoryEntryListOptions) {
  1504. order(id: $id) {
  1505. id
  1506. history(options: $options) {
  1507. totalItems
  1508. items {
  1509. id
  1510. type
  1511. administrator {
  1512. id
  1513. }
  1514. data
  1515. }
  1516. }
  1517. }
  1518. }
  1519. `;
  1520. export const ADD_NOTE_TO_ORDER = gql`
  1521. mutation AddNoteToOrder($input: AddNoteToOrderInput!) {
  1522. addNoteToOrder(input: $input) {
  1523. id
  1524. }
  1525. }
  1526. `;
  1527. export const UPDATE_ORDER_NOTE = gql`
  1528. mutation UpdateOrderNote($input: UpdateOrderNoteInput!) {
  1529. updateOrderNote(input: $input) {
  1530. id
  1531. data
  1532. isPublic
  1533. }
  1534. }
  1535. `;
  1536. export const DELETE_ORDER_NOTE = gql`
  1537. mutation DeleteOrderNote($id: ID!) {
  1538. deleteOrderNote(id: $id) {
  1539. result
  1540. message
  1541. }
  1542. }
  1543. `;
  1544. const GET_ORDER_WITH_PAYMENTS = gql`
  1545. query GetOrderWithPayments($id: ID!) {
  1546. order(id: $id) {
  1547. id
  1548. payments {
  1549. id
  1550. errorMessage
  1551. metadata
  1552. }
  1553. }
  1554. }
  1555. `;