order.e2e-spec.ts 66 KB

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