order.e2e-spec.ts 102 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import {
  3. DeletionResult,
  4. ErrorCode,
  5. GlobalFlag,
  6. HistoryEntryType,
  7. LanguageCode,
  8. SortOrder,
  9. StockMovementType,
  10. } from '@vendure/common/lib/generated-types';
  11. import { omit } from '@vendure/common/lib/omit';
  12. import { pick } from '@vendure/common/lib/pick';
  13. import {
  14. defaultShippingCalculator,
  15. defaultShippingEligibilityChecker,
  16. manualFulfillmentHandler,
  17. mergeConfig,
  18. } from '@vendure/core';
  19. import {
  20. ErrorResultGuard,
  21. SimpleGraphQLClient,
  22. createErrorResultGuard,
  23. createTestEnvironment,
  24. } from '@vendure/testing';
  25. import path from 'path';
  26. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  27. import { initialData } from '../../../e2e-common/e2e-initial-data';
  28. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  29. import {
  30. failsToCancelPaymentMethod,
  31. failsToSettlePaymentMethod,
  32. onCancelPaymentSpy,
  33. onTransitionSpy,
  34. partialPaymentMethod,
  35. singleStageRefundFailingPaymentMethod,
  36. singleStageRefundablePaymentMethod,
  37. twoStagePaymentMethod,
  38. } from './fixtures/test-payment-methods';
  39. import {
  40. canceledOrderFragment,
  41. fulfillmentFragment,
  42. paymentFragment,
  43. refundFragment,
  44. } from './graphql/fragments-admin';
  45. import { FragmentOf, ResultOf } from './graphql/graphql-admin';
  46. import { FragmentOf as FragmentOfShop } from './graphql/graphql-shop';
  47. import {
  48. addManualPaymentDocument,
  49. addNoteToOrderDocument,
  50. cancelOrderDocument,
  51. cancelPaymentDocument,
  52. createFulfillmentDocument,
  53. createShippingMethodDocument,
  54. deleteOrderNoteDocument,
  55. deleteProductDocument,
  56. deleteShippingMethodDocument,
  57. getCustomerListDocument,
  58. getOrderAssetEdgeCaseDocument,
  59. getOrderDocument,
  60. getOrderFulfillmentsDocument,
  61. getOrderHistoryDocument,
  62. getOrderLineFulfillmentsDocument,
  63. getOrderListDocument,
  64. getOrderListFulfillmentsDocument,
  65. getOrderListWithQtyDocument,
  66. getOrderWithLineCalculatedPropsDocument,
  67. getOrderWithPaymentsDocument,
  68. getOrderWithRefundLinesDocument,
  69. getProductWithVariantsDocument,
  70. getStockMovementDocument,
  71. refundOrderDocument,
  72. setOrderCustomerDocument,
  73. settlePaymentDocument,
  74. settleRefundDocument,
  75. transitFulfillmentDocument,
  76. updateOrderNoteDocument,
  77. updateProductVariantsDocument,
  78. } from './graphql/shared-definitions';
  79. import {
  80. addItemToOrderDocument,
  81. addMultipleItemsToOrderDocument,
  82. addPaymentDocument,
  83. applyCouponCodeDocument,
  84. getActiveCustomerWithOrdersProductPriceDocument,
  85. getActiveCustomerWithOrdersProductSlugDocument,
  86. getActiveOrderDocument,
  87. getOrderByCodeWithPaymentsDocument,
  88. setShippingAddressDocument,
  89. setShippingMethodDocument,
  90. testOrderWithPaymentsFragment,
  91. } from './graphql/shop-definitions';
  92. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  93. import { addPaymentToOrder, proceedToArrangingPayment, sortById } from './utils/test-order-utils';
  94. describe('Orders resolver', () => {
  95. const { server, adminClient, shopClient } = createTestEnvironment(
  96. mergeConfig(testConfig(), {
  97. paymentOptions: {
  98. paymentMethodHandlers: [
  99. twoStagePaymentMethod,
  100. failsToSettlePaymentMethod,
  101. singleStageRefundablePaymentMethod,
  102. partialPaymentMethod,
  103. singleStageRefundFailingPaymentMethod,
  104. failsToCancelPaymentMethod,
  105. ],
  106. },
  107. }),
  108. );
  109. let customers: ResultOf<typeof getCustomerListDocument>['customers']['items'];
  110. const password = 'test';
  111. type RefundFragment = FragmentOf<typeof refundFragment>;
  112. type PaymentFragment = FragmentOf<typeof paymentFragment>;
  113. type FulfillmentFragment = FragmentOf<typeof fulfillmentFragment>;
  114. type CanceledOrderFragment = FragmentOf<typeof canceledOrderFragment>;
  115. type ShopOrderFragment = FragmentOfShop<typeof testOrderWithPaymentsFragment>;
  116. const canceledOrderGuard: ErrorResultGuard<CanceledOrderFragment> = createErrorResultGuard(
  117. input => !!input.lines,
  118. );
  119. const shopOrderGuard: ErrorResultGuard<ShopOrderFragment> = createErrorResultGuard(
  120. input => !!input.lines,
  121. );
  122. const paymentGuard: ErrorResultGuard<PaymentFragment> = createErrorResultGuard(input => !!input.state);
  123. const fulfillmentGuard: ErrorResultGuard<FulfillmentFragment> = createErrorResultGuard(
  124. input => !!input.method,
  125. );
  126. const refundGuard: ErrorResultGuard<RefundFragment> = createErrorResultGuard(input => !!input.total);
  127. beforeAll(async () => {
  128. await server.init({
  129. initialData: {
  130. ...initialData,
  131. paymentMethods: [
  132. {
  133. name: twoStagePaymentMethod.code,
  134. handler: { code: twoStagePaymentMethod.code, arguments: [] },
  135. },
  136. {
  137. name: failsToSettlePaymentMethod.code,
  138. handler: { code: failsToSettlePaymentMethod.code, arguments: [] },
  139. },
  140. {
  141. name: failsToCancelPaymentMethod.code,
  142. handler: { code: failsToCancelPaymentMethod.code, arguments: [] },
  143. },
  144. {
  145. name: singleStageRefundablePaymentMethod.code,
  146. handler: { code: singleStageRefundablePaymentMethod.code, arguments: [] },
  147. },
  148. {
  149. name: singleStageRefundFailingPaymentMethod.code,
  150. handler: { code: singleStageRefundFailingPaymentMethod.code, arguments: [] },
  151. },
  152. {
  153. name: partialPaymentMethod.code,
  154. handler: { code: partialPaymentMethod.code, arguments: [] },
  155. },
  156. ],
  157. },
  158. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  159. customerCount: 3,
  160. });
  161. await adminClient.asSuperAdmin();
  162. // Create a couple of orders to be queried
  163. const result = await adminClient.query(getCustomerListDocument, {
  164. options: {
  165. take: 3,
  166. },
  167. });
  168. customers = result.customers.items;
  169. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  170. await shopClient.query(addItemToOrderDocument, {
  171. productVariantId: 'T_1',
  172. quantity: 1,
  173. });
  174. await shopClient.query(addItemToOrderDocument, {
  175. productVariantId: 'T_2',
  176. quantity: 1,
  177. });
  178. await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
  179. await shopClient.query(addItemToOrderDocument, {
  180. productVariantId: 'T_2',
  181. quantity: 1,
  182. });
  183. await shopClient.query(addItemToOrderDocument, {
  184. productVariantId: 'T_3',
  185. quantity: 3,
  186. });
  187. }, TEST_SETUP_TIMEOUT_MS);
  188. afterAll(async () => {
  189. await server.destroy();
  190. });
  191. it('order history initially contains Created -> AddingItems transition', async () => {
  192. const { order } = await adminClient.query(getOrderHistoryDocument, { id: 'T_1' });
  193. expect(order!.history.totalItems).toBe(1);
  194. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  195. {
  196. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  197. data: {
  198. from: 'Created',
  199. to: 'AddingItems',
  200. },
  201. },
  202. ]);
  203. });
  204. describe('querying', () => {
  205. it('orders', async () => {
  206. const result = await adminClient.query(getOrderListDocument);
  207. expect(result.orders.items.map(o => o.id).sort()).toEqual(['T_1', 'T_2']);
  208. });
  209. it('filtering by total', async () => {
  210. const result = await adminClient.query(getOrderListDocument, {
  211. options: {
  212. filter: { total: { gt: 1000_00 } },
  213. },
  214. });
  215. expect(result.orders.items.map(o => o.id).sort()).toEqual(['T_1', 'T_2']);
  216. });
  217. it('filtering by total using boolean expression', async () => {
  218. const result = await adminClient.query(getOrderListDocument, {
  219. options: {
  220. filter: {
  221. _and: [{ total: { gt: 1000_00 } }],
  222. },
  223. },
  224. });
  225. expect(result.orders.items.map(o => o.id).sort()).toEqual(['T_1', 'T_2']);
  226. });
  227. it('order', async () => {
  228. const result = await adminClient.query(getOrderDocument, {
  229. id: 'T_2',
  230. });
  231. expect(result.order!.id).toBe('T_2');
  232. });
  233. it('correctly resolves asset preview urls with edge-case query', async () => {
  234. // This came up as a strange edge-case where the AssetInterceptorPlugin was unable to
  235. // correctly transform the asset preview URL. It is not directly to do with Orders per se,
  236. // but manifested when attempting an order-related query.
  237. const result = await adminClient.query(getOrderAssetEdgeCaseDocument, {
  238. id: 'T_2',
  239. });
  240. expect(result.order!.lines.length).toBe(2);
  241. expect(result.order!.lines.map((l: any) => l.featuredAsset?.preview)).toEqual([
  242. 'test-url/test-assets/derick-david-409858-unsplash__preview.jpg',
  243. 'test-url/test-assets/derick-david-409858-unsplash__preview.jpg',
  244. ]);
  245. });
  246. it('order with calculated line properties', async () => {
  247. const result = await adminClient.query(getOrderWithLineCalculatedPropsDocument, {
  248. id: 'T_2',
  249. });
  250. expect(result.order!.lines).toEqual([
  251. {
  252. id: 'T_3',
  253. linePriceWithTax: 167880,
  254. quantity: 1,
  255. },
  256. {
  257. id: 'T_4',
  258. linePriceWithTax: 791640,
  259. quantity: 3,
  260. },
  261. ]);
  262. });
  263. it('sort by total', async () => {
  264. const result = await adminClient.query(getOrderListDocument, {
  265. options: {
  266. sort: {
  267. total: SortOrder.DESC,
  268. },
  269. take: 10,
  270. },
  271. });
  272. expect(result.orders.items.map(o => pick(o, ['id', 'total']))).toEqual([
  273. { id: 'T_2', total: 799600 },
  274. { id: 'T_1', total: 269800 },
  275. ]);
  276. });
  277. it('sort by totalWithTax', async () => {
  278. const result = await adminClient.query(getOrderListDocument, {
  279. options: {
  280. sort: {
  281. totalWithTax: SortOrder.DESC,
  282. },
  283. take: 10,
  284. },
  285. });
  286. expect(result.orders.items.map(o => pick(o, ['id', 'totalWithTax']))).toEqual([
  287. { id: 'T_2', totalWithTax: 959520 },
  288. { id: 'T_1', totalWithTax: 323760 },
  289. ]);
  290. });
  291. it('sort by totalQuantity', async () => {
  292. const result = await adminClient.query(getOrderListDocument, {
  293. options: {
  294. sort: {
  295. totalQuantity: SortOrder.DESC,
  296. },
  297. take: 10,
  298. },
  299. });
  300. expect(result.orders.items.map(o => pick(o, ['id', 'totalQuantity']))).toEqual([
  301. { id: 'T_2', totalQuantity: 4 },
  302. { id: 'T_1', totalQuantity: 2 },
  303. ]);
  304. });
  305. it('sort by customerLastName', async () => {
  306. async function sortOrdersByLastName(sortOrder: SortOrder) {
  307. const { orders } = await adminClient.query(getOrderListDocument, {
  308. options: {
  309. sort: {
  310. customerLastName: sortOrder,
  311. },
  312. },
  313. });
  314. return orders;
  315. }
  316. const result1 = await sortOrdersByLastName(SortOrder.ASC);
  317. expect(result1.totalItems).toEqual(2);
  318. expect(result1.items.map(order => order.customer?.lastName)).toEqual(['Donnelly', 'Zieme']);
  319. const result2 = await sortOrdersByLastName(SortOrder.DESC);
  320. expect(result2.totalItems).toEqual(2);
  321. expect(result2.items.map(order => order.customer?.lastName)).toEqual(['Zieme', 'Donnelly']);
  322. });
  323. it('filter by total', async () => {
  324. const result = await adminClient.query(getOrderListDocument, {
  325. options: {
  326. filter: {
  327. total: { gt: 323760 },
  328. },
  329. take: 10,
  330. },
  331. });
  332. expect(result.orders.items.map(o => pick(o, ['id', 'total']))).toEqual([
  333. { id: 'T_2', total: 799600 },
  334. ]);
  335. });
  336. it('filter by totalWithTax', async () => {
  337. const result = await adminClient.query(getOrderListDocument, {
  338. options: {
  339. filter: {
  340. totalWithTax: { gt: 323760 },
  341. },
  342. take: 10,
  343. },
  344. });
  345. expect(result.orders.items.map(o => pick(o, ['id', 'totalWithTax']))).toEqual([
  346. { id: 'T_2', totalWithTax: 959520 },
  347. ]);
  348. });
  349. it('filter by totalQuantity', async () => {
  350. const result = await adminClient.query(getOrderListDocument, {
  351. options: {
  352. filter: {
  353. totalQuantity: { eq: 4 },
  354. },
  355. },
  356. });
  357. expect(result.orders.items.map(o => pick(o, ['id', 'totalQuantity']))).toEqual([
  358. { id: 'T_2', totalQuantity: 4 },
  359. ]);
  360. });
  361. it('filter by customerLastName', async () => {
  362. const result = await adminClient.query(getOrderListDocument, {
  363. options: {
  364. filter: {
  365. customerLastName: {
  366. eq: customers[1].lastName,
  367. },
  368. },
  369. },
  370. });
  371. expect(result.orders.totalItems).toEqual(1);
  372. expect(result.orders.items[0].customer?.lastName).toEqual(customers[1].lastName);
  373. });
  374. });
  375. describe('payments', () => {
  376. let firstOrderCode: string;
  377. let firstOrderId: string;
  378. it('settlePayment fails', async () => {
  379. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  380. await proceedToArrangingPayment(shopClient);
  381. const order = await addPaymentToOrder(shopClient, failsToSettlePaymentMethod);
  382. shopOrderGuard.assertSuccess(order);
  383. expect(order.state).toBe('PaymentAuthorized');
  384. const payment = 'payments' in order ? order.payments![0] : null;
  385. if (!payment) throw new Error('Expected payment');
  386. const { settlePayment } = await adminClient.query(settlePaymentDocument, {
  387. id: payment.id,
  388. });
  389. paymentGuard.assertErrorResult(settlePayment);
  390. expect(settlePayment.message).toBe('Settling the payment failed');
  391. expect(settlePayment.errorCode).toBe(ErrorCode.SETTLE_PAYMENT_ERROR);
  392. expect((settlePayment as any).paymentErrorMessage).toBe('Something went horribly wrong');
  393. const result = await adminClient.query(getOrderDocument, {
  394. id: order.id,
  395. });
  396. expect(result.order!.state).toBe('PaymentAuthorized');
  397. expect(result.order!.payments![0].state).toBe('Cancelled');
  398. firstOrderCode = 'code' in order ? order.code : '';
  399. firstOrderId = order.id;
  400. });
  401. it('public payment metadata available in Shop API', async () => {
  402. const { orderByCode } = await shopClient.query(getOrderByCodeWithPaymentsDocument, {
  403. code: firstOrderCode,
  404. });
  405. expect(orderByCode?.payments?.[0].metadata).toEqual({
  406. public: {
  407. publicCreatePaymentData: 'public',
  408. publicSettlePaymentData: 'public',
  409. },
  410. });
  411. });
  412. it('public and private payment metadata available in Admin API', async () => {
  413. const { order } = await adminClient.query(getOrderWithPaymentsDocument, { id: firstOrderId });
  414. expect(order?.payments?.[0].metadata).toEqual({
  415. privateCreatePaymentData: 'secret',
  416. privateSettlePaymentData: 'secret',
  417. public: {
  418. publicCreatePaymentData: 'public',
  419. publicSettlePaymentData: 'public',
  420. },
  421. });
  422. });
  423. it('settlePayment succeeds, onStateTransitionStart called', async () => {
  424. onTransitionSpy.mockClear();
  425. await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
  426. await proceedToArrangingPayment(shopClient);
  427. const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
  428. shopOrderGuard.assertSuccess(order);
  429. expect(order.state).toBe('PaymentAuthorized');
  430. expect(onTransitionSpy).toHaveBeenCalledTimes(1);
  431. expect(onTransitionSpy.mock.calls[0][0]).toBe('Created');
  432. expect(onTransitionSpy.mock.calls[0][1]).toBe('Authorized');
  433. const payment = 'payments' in order ? order.payments![0] : null;
  434. if (!payment) throw new Error('Expected payment');
  435. const { settlePayment } = await adminClient.query(settlePaymentDocument, {
  436. id: payment.id,
  437. });
  438. paymentGuard.assertSuccess(settlePayment);
  439. expect(settlePayment.id).toBe(payment.id);
  440. expect(settlePayment.state).toBe('Settled');
  441. // further metadata is combined into existing object
  442. expect(settlePayment.metadata).toEqual({
  443. moreData: 42,
  444. public: {
  445. baz: 'quux',
  446. },
  447. });
  448. expect(onTransitionSpy).toHaveBeenCalledTimes(2);
  449. expect(onTransitionSpy.mock.calls[1][0]).toBe('Authorized');
  450. expect(onTransitionSpy.mock.calls[1][1]).toBe('Settled');
  451. const result = await adminClient.query(getOrderDocument, {
  452. id: order.id,
  453. });
  454. expect(result.order!.state).toBe('PaymentSettled');
  455. expect(result.order!.payments![0].state).toBe('Settled');
  456. });
  457. it('order history contains expected entries', async () => {
  458. const { order } = await adminClient.query(getOrderHistoryDocument, {
  459. id: 'T_2',
  460. options: { sort: { id: SortOrder.ASC } },
  461. });
  462. expect(order?.history.items.map(pick(['type', 'data']))).toEqual([
  463. {
  464. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  465. data: {
  466. from: 'Created',
  467. to: 'AddingItems',
  468. },
  469. },
  470. {
  471. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  472. data: {
  473. from: 'AddingItems',
  474. to: 'ArrangingPayment',
  475. },
  476. },
  477. {
  478. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  479. data: {
  480. paymentId: 'T_2',
  481. from: 'Created',
  482. to: 'Authorized',
  483. },
  484. },
  485. {
  486. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  487. data: {
  488. from: 'ArrangingPayment',
  489. to: 'PaymentAuthorized',
  490. },
  491. },
  492. {
  493. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  494. data: {
  495. paymentId: 'T_2',
  496. from: 'Authorized',
  497. to: 'Settled',
  498. },
  499. },
  500. {
  501. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  502. data: {
  503. from: 'PaymentAuthorized',
  504. to: 'PaymentSettled',
  505. },
  506. },
  507. ]);
  508. });
  509. it('filter by transactionId', async () => {
  510. const result = await adminClient.query(getOrderListDocument, {
  511. options: {
  512. filter: {
  513. transactionId: {
  514. eq: '12345-' + firstOrderCode,
  515. },
  516. },
  517. },
  518. });
  519. expect(result.orders.totalItems).toEqual(1);
  520. expect(result.orders.items[0].code).toBe(firstOrderCode);
  521. });
  522. });
  523. describe('fulfillment', () => {
  524. const orderId = 'T_2';
  525. let f1Id: string;
  526. let f2Id: string;
  527. let f3Id: string;
  528. it('return error result if lines is empty', async () => {
  529. const { order } = await adminClient.query(getOrderDocument, {
  530. id: orderId,
  531. });
  532. expect(order!.state).toBe('PaymentSettled');
  533. const { addFulfillmentToOrder } = await adminClient.query(createFulfillmentDocument, {
  534. input: {
  535. lines: [],
  536. handler: {
  537. code: manualFulfillmentHandler.code,
  538. arguments: [{ name: 'method', value: 'Test' }],
  539. },
  540. },
  541. });
  542. fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
  543. expect(addFulfillmentToOrder.message).toBe('At least one OrderLine must be specified');
  544. expect(addFulfillmentToOrder.errorCode).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
  545. });
  546. it('returns error result if all quantities are zero', async () => {
  547. const { order } = await adminClient.query(getOrderDocument, {
  548. id: orderId,
  549. });
  550. expect(order!.state).toBe('PaymentSettled');
  551. const { addFulfillmentToOrder } = await adminClient.query(createFulfillmentDocument, {
  552. input: {
  553. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
  554. handler: {
  555. code: manualFulfillmentHandler.code,
  556. arguments: [{ name: 'method', value: 'Test' }],
  557. },
  558. },
  559. });
  560. fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
  561. expect(addFulfillmentToOrder.message).toBe('At least one OrderLine must be specified');
  562. expect(addFulfillmentToOrder.errorCode).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
  563. });
  564. it('creates the first fulfillment', async () => {
  565. const { order } = await adminClient.query(getOrderDocument, {
  566. id: orderId,
  567. });
  568. expect(order!.state).toBe('PaymentSettled');
  569. const lines = order!.lines;
  570. const { addFulfillmentToOrder } = await adminClient.query(createFulfillmentDocument, {
  571. input: {
  572. lines: [{ orderLineId: lines[0].id, quantity: lines[0].quantity }],
  573. handler: {
  574. code: manualFulfillmentHandler.code,
  575. arguments: [
  576. { name: 'method', value: 'Test1' },
  577. { name: 'trackingCode', value: '111' },
  578. ],
  579. },
  580. },
  581. });
  582. fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
  583. expect(addFulfillmentToOrder.id).toBe('T_1');
  584. expect(addFulfillmentToOrder.method).toBe('Test1');
  585. expect(addFulfillmentToOrder.trackingCode).toBe('111');
  586. expect(addFulfillmentToOrder.state).toBe('Pending');
  587. expect(addFulfillmentToOrder.lines).toEqual([
  588. { orderLineId: lines[0].id, quantity: lines[0].quantity },
  589. ]);
  590. f1Id = addFulfillmentToOrder.id;
  591. const result = await adminClient.query(getOrderDocument, {
  592. id: orderId,
  593. });
  594. expect(result.order!.fulfillments?.length).toBe(1);
  595. expect(result.order!.fulfillments![0]!.id).toBe(addFulfillmentToOrder.id);
  596. expect(result.order!.fulfillments![0]!.lines).toEqual([
  597. {
  598. orderLineId: order?.lines[0].id,
  599. quantity: order?.lines[0].quantity,
  600. },
  601. ]);
  602. expect(
  603. result.order!.fulfillments![0]!.lines.filter(l => l.orderLineId === lines[1].id).length,
  604. ).toBe(0);
  605. });
  606. it('creates the second fulfillment', async () => {
  607. const lines = await getUnfulfilledOrderLineInput(adminClient, orderId);
  608. const { addFulfillmentToOrder } = await adminClient.query(createFulfillmentDocument, {
  609. input: {
  610. lines,
  611. handler: {
  612. code: manualFulfillmentHandler.code,
  613. arguments: [
  614. { name: 'method', value: 'Test2' },
  615. { name: 'trackingCode', value: '222' },
  616. ],
  617. },
  618. },
  619. });
  620. fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
  621. expect(addFulfillmentToOrder.id).toBe('T_2');
  622. expect(addFulfillmentToOrder.method).toBe('Test2');
  623. expect(addFulfillmentToOrder.trackingCode).toBe('222');
  624. expect(addFulfillmentToOrder.state).toBe('Pending');
  625. f2Id = addFulfillmentToOrder.id;
  626. });
  627. it('cancels second fulfillment', async () => {
  628. const { transitionFulfillmentToState } = await adminClient.query(transitFulfillmentDocument, {
  629. id: f2Id,
  630. state: 'Cancelled',
  631. });
  632. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  633. expect(transitionFulfillmentToState.id).toBe('T_2');
  634. expect(transitionFulfillmentToState.state).toBe('Cancelled');
  635. });
  636. it('order.fulfillments still lists second (cancelled) fulfillment', async () => {
  637. const { order } = await adminClient.query(getOrderFulfillmentsDocument, {
  638. id: orderId,
  639. });
  640. expect(order?.fulfillments?.sort(sortById).map(pick(['id', 'state']))).toEqual([
  641. { id: f1Id, state: 'Pending' },
  642. { id: f2Id, state: 'Cancelled' },
  643. ]);
  644. });
  645. it('order.fulfillments.summary', async () => {
  646. const { order } = await adminClient.query(getOrderFulfillmentsDocument, {
  647. id: orderId,
  648. });
  649. expect(order?.fulfillments?.sort(sortById).map(pick(['id', 'state', 'summary']))).toEqual([
  650. { id: f1Id, state: 'Pending', summary: [{ orderLine: { id: 'T_3' }, quantity: 1 }] },
  651. { id: f2Id, state: 'Cancelled', summary: [{ orderLine: { id: 'T_4' }, quantity: 3 }] },
  652. ]);
  653. });
  654. it('lines.fulfillments', async () => {
  655. const { order } = await adminClient.query(getOrderLineFulfillmentsDocument, {
  656. id: orderId,
  657. });
  658. expect(order?.lines.find(l => l.id === 'T_3')!.fulfillmentLines).toEqual([
  659. { fulfillment: { id: f1Id, state: 'Pending' }, orderLineId: 'T_3', quantity: 1 },
  660. ]);
  661. // Cancelled Fulfillments do not appear in the line field
  662. expect(order?.lines.find(l => l.id === 'T_4')!.fulfillmentLines).toEqual([]);
  663. });
  664. it('creates third fulfillment with same items from second fulfillment', async () => {
  665. const lines = await getUnfulfilledOrderLineInput(adminClient, orderId);
  666. const { addFulfillmentToOrder } = await adminClient.query(createFulfillmentDocument, {
  667. input: {
  668. lines,
  669. handler: {
  670. code: manualFulfillmentHandler.code,
  671. arguments: [
  672. { name: 'method', value: 'Test3' },
  673. { name: 'trackingCode', value: '333' },
  674. ],
  675. },
  676. },
  677. });
  678. fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
  679. expect(addFulfillmentToOrder.id).toBe('T_3');
  680. expect(addFulfillmentToOrder.method).toBe('Test3');
  681. expect(addFulfillmentToOrder.trackingCode).toBe('333');
  682. expect(addFulfillmentToOrder.state).toBe('Pending');
  683. f3Id = addFulfillmentToOrder.id;
  684. });
  685. it('returns error result if an OrderItem already part of a Fulfillment', async () => {
  686. const { order } = await adminClient.query(getOrderDocument, {
  687. id: orderId,
  688. });
  689. const { addFulfillmentToOrder } = await adminClient.query(createFulfillmentDocument, {
  690. input: {
  691. lines: [
  692. {
  693. orderLineId: order!.lines[0].id,
  694. quantity: 1,
  695. },
  696. ],
  697. handler: {
  698. code: manualFulfillmentHandler.code,
  699. arguments: [{ name: 'method', value: 'Test' }],
  700. },
  701. },
  702. });
  703. fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
  704. expect(addFulfillmentToOrder.message).toBe(
  705. 'One or more OrderItems are already part of a Fulfillment',
  706. );
  707. expect(addFulfillmentToOrder.errorCode).toBe(ErrorCode.ITEMS_ALREADY_FULFILLED_ERROR);
  708. });
  709. it('transitions the first fulfillment from created to Shipped and automatically change the order state to PartiallyShipped', async () => {
  710. const { transitionFulfillmentToState } = await adminClient.query(transitFulfillmentDocument, {
  711. id: f1Id,
  712. state: 'Shipped',
  713. });
  714. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  715. expect(transitionFulfillmentToState.id).toBe(f1Id);
  716. expect(transitionFulfillmentToState.state).toBe('Shipped');
  717. const { order } = await adminClient.query(getOrderDocument, {
  718. id: orderId,
  719. });
  720. expect(order?.state).toBe('PartiallyShipped');
  721. });
  722. it('transitions the third fulfillment from created to Shipped and automatically change the order state to Shipped', async () => {
  723. const { transitionFulfillmentToState } = await adminClient.query(transitFulfillmentDocument, {
  724. id: f3Id,
  725. state: 'Shipped',
  726. });
  727. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  728. expect(transitionFulfillmentToState.id).toBe(f3Id);
  729. expect(transitionFulfillmentToState.state).toBe('Shipped');
  730. const { order } = await adminClient.query(getOrderDocument, {
  731. id: orderId,
  732. });
  733. expect(order?.state).toBe('Shipped');
  734. });
  735. it('transitions the first fulfillment from Shipped to Delivered and change the order state to PartiallyDelivered', async () => {
  736. const { transitionFulfillmentToState } = await adminClient.query(transitFulfillmentDocument, {
  737. id: f1Id,
  738. state: 'Delivered',
  739. });
  740. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  741. expect(transitionFulfillmentToState.id).toBe(f1Id);
  742. expect(transitionFulfillmentToState.state).toBe('Delivered');
  743. const { order } = await adminClient.query(getOrderDocument, {
  744. id: orderId,
  745. });
  746. expect(order?.state).toBe('PartiallyDelivered');
  747. });
  748. it('transitions the third fulfillment from Shipped to Delivered and change the order state to Delivered', async () => {
  749. const { transitionFulfillmentToState } = await adminClient.query(transitFulfillmentDocument, {
  750. id: f3Id,
  751. state: 'Delivered',
  752. });
  753. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  754. expect(transitionFulfillmentToState.id).toBe(f3Id);
  755. expect(transitionFulfillmentToState.state).toBe('Delivered');
  756. const { order } = await adminClient.query(getOrderDocument, {
  757. id: orderId,
  758. });
  759. expect(order?.state).toBe('Delivered');
  760. });
  761. it('order history contains expected entries', async () => {
  762. const { order } = await adminClient.query(getOrderHistoryDocument, {
  763. id: orderId,
  764. options: {
  765. skip: 6,
  766. },
  767. });
  768. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  769. {
  770. data: {
  771. fulfillmentId: f1Id,
  772. },
  773. type: HistoryEntryType.ORDER_FULFILLMENT,
  774. },
  775. {
  776. data: {
  777. from: 'Created',
  778. fulfillmentId: f1Id,
  779. to: 'Pending',
  780. },
  781. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  782. },
  783. {
  784. data: {
  785. fulfillmentId: f2Id,
  786. },
  787. type: HistoryEntryType.ORDER_FULFILLMENT,
  788. },
  789. {
  790. data: {
  791. from: 'Created',
  792. fulfillmentId: f2Id,
  793. to: 'Pending',
  794. },
  795. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  796. },
  797. {
  798. data: {
  799. from: 'Pending',
  800. fulfillmentId: f2Id,
  801. to: 'Cancelled',
  802. },
  803. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  804. },
  805. {
  806. data: {
  807. fulfillmentId: f3Id,
  808. },
  809. type: HistoryEntryType.ORDER_FULFILLMENT,
  810. },
  811. {
  812. data: {
  813. from: 'Created',
  814. fulfillmentId: f3Id,
  815. to: 'Pending',
  816. },
  817. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  818. },
  819. {
  820. data: {
  821. from: 'Pending',
  822. fulfillmentId: f1Id,
  823. to: 'Shipped',
  824. },
  825. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  826. },
  827. {
  828. data: {
  829. from: 'PaymentSettled',
  830. to: 'PartiallyShipped',
  831. },
  832. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  833. },
  834. {
  835. data: {
  836. from: 'Pending',
  837. fulfillmentId: f3Id,
  838. to: 'Shipped',
  839. },
  840. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  841. },
  842. {
  843. data: {
  844. from: 'PartiallyShipped',
  845. to: 'Shipped',
  846. },
  847. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  848. },
  849. {
  850. data: {
  851. from: 'Shipped',
  852. fulfillmentId: f1Id,
  853. to: 'Delivered',
  854. },
  855. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  856. },
  857. {
  858. data: {
  859. from: 'Shipped',
  860. to: 'PartiallyDelivered',
  861. },
  862. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  863. },
  864. {
  865. data: {
  866. from: 'Shipped',
  867. fulfillmentId: f3Id,
  868. to: 'Delivered',
  869. },
  870. type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
  871. },
  872. {
  873. data: {
  874. from: 'PartiallyDelivered',
  875. to: 'Delivered',
  876. },
  877. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  878. },
  879. ]);
  880. });
  881. it('order.fulfillments resolver for single order', async () => {
  882. const { order } = await adminClient.query(getOrderFulfillmentsDocument, {
  883. id: orderId,
  884. });
  885. expect(
  886. order!.fulfillments?.sort(sortById).map(pick(['id', 'method', 'state', 'nextStates'])),
  887. ).toEqual([
  888. { id: f1Id, method: 'Test1', state: 'Delivered', nextStates: ['Cancelled'] },
  889. { id: f2Id, method: 'Test2', state: 'Cancelled', nextStates: [] },
  890. { id: f3Id, method: 'Test3', state: 'Delivered', nextStates: ['Cancelled'] },
  891. ]);
  892. });
  893. it('order.fulfillments resolver for order list', async () => {
  894. const { orders } = await adminClient.query(getOrderListFulfillmentsDocument);
  895. expect(orders.items[0].fulfillments).toEqual([]);
  896. expect(orders.items[1].fulfillments?.sort(sortById)).toEqual([
  897. { id: f1Id, method: 'Test1', state: 'Delivered', nextStates: ['Cancelled'] },
  898. { id: f2Id, method: 'Test2', state: 'Cancelled', nextStates: [] },
  899. { id: f3Id, method: 'Test3', state: 'Delivered', nextStates: ['Cancelled'] },
  900. ]);
  901. });
  902. });
  903. describe('cancellation by orderId', () => {
  904. it('cancel from AddingItems state', async () => {
  905. const testOrder = await createTestOrder(
  906. adminClient,
  907. shopClient,
  908. customers[0].emailAddress,
  909. password,
  910. );
  911. const { order } = await adminClient.query(getOrderDocument, {
  912. id: testOrder.orderId,
  913. });
  914. expect(order!.state).toBe('AddingItems');
  915. await adminClient.query(cancelOrderDocument, {
  916. input: {
  917. orderId: testOrder.orderId,
  918. },
  919. });
  920. const { order: order2 } = await adminClient.query(getOrderDocument, {
  921. id: testOrder.orderId,
  922. });
  923. expect(order2!.state).toBe('Cancelled');
  924. expect(order2!.active).toBe(false);
  925. await assertNoStockMovementsCreated(testOrder.product!.id);
  926. });
  927. it('cancel from ArrangingPayment state', async () => {
  928. const testOrder = await createTestOrder(
  929. adminClient,
  930. shopClient,
  931. customers[0].emailAddress,
  932. password,
  933. );
  934. await proceedToArrangingPayment(shopClient);
  935. const { order } = await adminClient.query(getOrderDocument, {
  936. id: testOrder.orderId,
  937. });
  938. expect(order!.state).toBe('ArrangingPayment');
  939. await adminClient.query(cancelOrderDocument, {
  940. input: {
  941. orderId: testOrder.orderId,
  942. },
  943. });
  944. const { order: order2 } = await adminClient.query(getOrderDocument, {
  945. id: testOrder.orderId,
  946. });
  947. expect(order2!.state).toBe('Cancelled');
  948. expect(order2!.active).toBe(false);
  949. await assertNoStockMovementsCreated(testOrder.product!.id);
  950. });
  951. it('cancel from PaymentAuthorized state with cancelShipping: true', async () => {
  952. const testOrder = await createTestOrder(
  953. adminClient,
  954. shopClient,
  955. customers[0].emailAddress,
  956. password,
  957. );
  958. await proceedToArrangingPayment(shopClient, 2);
  959. const order = await addPaymentToOrder(shopClient, failsToSettlePaymentMethod);
  960. shopOrderGuard.assertSuccess(order);
  961. expect(order.state).toBe('PaymentAuthorized');
  962. const result1 = await adminClient.query(getStockMovementDocument, {
  963. id: 'T_3',
  964. });
  965. let variant1 = result1.product!.variants[0];
  966. expect(variant1.stockOnHand).toBe(100);
  967. expect(variant1.stockAllocated).toBe(2);
  968. expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  969. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  970. { type: StockMovementType.ALLOCATION, quantity: 2 },
  971. ]);
  972. const { cancelOrder } = await adminClient.query(cancelOrderDocument, {
  973. input: {
  974. orderId: testOrder.orderId,
  975. cancelShipping: true,
  976. },
  977. });
  978. canceledOrderGuard.assertSuccess(cancelOrder);
  979. expect(cancelOrder.lines.sort((a, b) => (a.id > b.id ? 1 : -1))).toEqual([
  980. { id: 'T_7', quantity: 0 },
  981. ]);
  982. const { order: order2 } = await adminClient.query(getOrderDocument, {
  983. id: testOrder.orderId,
  984. });
  985. expect(order2!.active).toBe(false);
  986. expect(order2!.state).toBe('Cancelled');
  987. expect(order2!.totalWithTax).toBe(0);
  988. expect(order2!.shippingWithTax).toBe(0);
  989. const result2 = await adminClient.query(getStockMovementDocument, {
  990. id: 'T_3',
  991. });
  992. variant1 = result2.product!.variants[0];
  993. expect(variant1.stockOnHand).toBe(100);
  994. expect(variant1.stockAllocated).toBe(0);
  995. expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  996. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  997. { type: StockMovementType.ALLOCATION, quantity: 2 },
  998. { type: StockMovementType.RELEASE, quantity: 2 },
  999. ]);
  1000. });
  1001. async function assertNoStockMovementsCreated(productId: string) {
  1002. const result = await adminClient.query(getStockMovementDocument, {
  1003. id: productId,
  1004. });
  1005. const variant2 = result.product!.variants[0];
  1006. expect(variant2.stockOnHand).toBe(100);
  1007. expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  1008. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  1009. ]);
  1010. }
  1011. });
  1012. describe('cancellation by OrderLine', () => {
  1013. let orderId: string;
  1014. let product: ResultOf<typeof getProductWithVariantsDocument>['product'];
  1015. beforeAll(async () => {
  1016. const result = await createTestOrder(
  1017. adminClient,
  1018. shopClient,
  1019. customers[0].emailAddress,
  1020. password,
  1021. );
  1022. orderId = result.orderId;
  1023. product = result.product;
  1024. });
  1025. it('cannot cancel from AddingItems state', async () => {
  1026. const { order } = await adminClient.query(getOrderDocument, {
  1027. id: orderId,
  1028. });
  1029. expect(order!.state).toBe('AddingItems');
  1030. const { cancelOrder } = await adminClient.query(cancelOrderDocument, {
  1031. input: {
  1032. orderId,
  1033. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  1034. },
  1035. });
  1036. canceledOrderGuard.assertErrorResult(cancelOrder);
  1037. expect(cancelOrder.message).toBe(
  1038. 'Cannot cancel OrderLines from an Order in the "AddingItems" state',
  1039. );
  1040. expect(cancelOrder.errorCode).toBe(ErrorCode.CANCEL_ACTIVE_ORDER_ERROR);
  1041. });
  1042. it('cannot cancel from ArrangingPayment state', async () => {
  1043. await proceedToArrangingPayment(shopClient, 2);
  1044. const { order } = await adminClient.query(getOrderDocument, {
  1045. id: orderId,
  1046. });
  1047. expect(order!.state).toBe('ArrangingPayment');
  1048. const { cancelOrder } = await adminClient.query(cancelOrderDocument, {
  1049. input: {
  1050. orderId,
  1051. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  1052. },
  1053. });
  1054. canceledOrderGuard.assertErrorResult(cancelOrder);
  1055. expect(cancelOrder.message).toBe(
  1056. 'Cannot cancel OrderLines from an Order in the "ArrangingPayment" state',
  1057. );
  1058. expect(cancelOrder.errorCode).toBe(ErrorCode.CANCEL_ACTIVE_ORDER_ERROR);
  1059. });
  1060. it('returns error result if lines are empty', async () => {
  1061. const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
  1062. shopOrderGuard.assertSuccess(order);
  1063. expect(order.state).toBe('PaymentAuthorized');
  1064. const { cancelOrder } = await adminClient.query(cancelOrderDocument, {
  1065. input: {
  1066. orderId,
  1067. lines: [],
  1068. },
  1069. });
  1070. canceledOrderGuard.assertErrorResult(cancelOrder);
  1071. expect(cancelOrder.message).toBe('At least one OrderLine must be specified');
  1072. expect(cancelOrder.errorCode).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
  1073. });
  1074. it('returns error result if all quantities zero', async () => {
  1075. const { order } = await adminClient.query(getOrderDocument, {
  1076. id: orderId,
  1077. });
  1078. const { cancelOrder } = await adminClient.query(cancelOrderDocument, {
  1079. input: {
  1080. orderId,
  1081. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
  1082. },
  1083. });
  1084. canceledOrderGuard.assertErrorResult(cancelOrder);
  1085. expect(cancelOrder.message).toBe('At least one OrderLine must be specified');
  1086. expect(cancelOrder.errorCode).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
  1087. });
  1088. it('partial cancellation', async () => {
  1089. const result1 = await adminClient.query(getStockMovementDocument, {
  1090. id: product!.id,
  1091. });
  1092. const variant1 = result1.product!.variants[0];
  1093. expect(variant1.stockOnHand).toBe(100);
  1094. expect(variant1.stockAllocated).toBe(2);
  1095. expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  1096. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  1097. { type: StockMovementType.ALLOCATION, quantity: 2 },
  1098. { type: StockMovementType.RELEASE, quantity: 2 },
  1099. { type: StockMovementType.ALLOCATION, quantity: 2 },
  1100. ]);
  1101. const { order } = await adminClient.query(getOrderDocument, {
  1102. id: orderId,
  1103. });
  1104. const { cancelOrder } = await adminClient.query(cancelOrderDocument, {
  1105. input: {
  1106. orderId,
  1107. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  1108. reason: 'cancel reason 1',
  1109. },
  1110. });
  1111. canceledOrderGuard.assertSuccess(cancelOrder);
  1112. expect(cancelOrder.lines[0].quantity).toBe(1);
  1113. const { order: order2 } = await adminClient.query(getOrderDocument, {
  1114. id: orderId,
  1115. });
  1116. expect(order2!.state).toBe('PaymentAuthorized');
  1117. expect(order2!.lines[0].quantity).toBe(1);
  1118. const result2 = await adminClient.query(getStockMovementDocument, {
  1119. id: product!.id,
  1120. });
  1121. const variant2 = result2.product!.variants[0];
  1122. expect(variant2.stockOnHand).toBe(100);
  1123. expect(variant2.stockAllocated).toBe(1);
  1124. expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  1125. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  1126. { type: StockMovementType.ALLOCATION, quantity: 2 },
  1127. { type: StockMovementType.RELEASE, quantity: 2 },
  1128. { type: StockMovementType.ALLOCATION, quantity: 2 },
  1129. { type: StockMovementType.RELEASE, quantity: 1 },
  1130. ]);
  1131. });
  1132. it('returns error result if attempting to cancel already cancelled item', async () => {
  1133. const { order } = await adminClient.query(getOrderDocument, {
  1134. id: orderId,
  1135. });
  1136. const { cancelOrder } = await adminClient.query(cancelOrderDocument, {
  1137. input: {
  1138. orderId,
  1139. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 2 })),
  1140. },
  1141. });
  1142. canceledOrderGuard.assertErrorResult(cancelOrder);
  1143. expect(cancelOrder.message).toBe(
  1144. 'The specified quantity is greater than the available OrderItems',
  1145. );
  1146. expect(cancelOrder.errorCode).toBe(ErrorCode.QUANTITY_TOO_GREAT_ERROR);
  1147. });
  1148. it('complete cancellation', async () => {
  1149. const { order } = await adminClient.query(getOrderDocument, {
  1150. id: orderId,
  1151. });
  1152. await adminClient.query(cancelOrderDocument, {
  1153. input: {
  1154. orderId,
  1155. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  1156. reason: 'cancel reason 2',
  1157. cancelShipping: true,
  1158. },
  1159. });
  1160. const { order: order2 } = await adminClient.query(getOrderDocument, {
  1161. id: orderId,
  1162. });
  1163. expect(order2!.state).toBe('Cancelled');
  1164. expect(order2!.shippingWithTax).toBe(0);
  1165. expect(order2!.totalWithTax).toBe(0);
  1166. const result = await adminClient.query(getStockMovementDocument, {
  1167. id: product!.id,
  1168. });
  1169. const variant2 = result.product!.variants[0];
  1170. expect(variant2.stockOnHand).toBe(100);
  1171. expect(variant2.stockAllocated).toBe(0);
  1172. expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
  1173. { type: StockMovementType.ADJUSTMENT, quantity: 100 },
  1174. { type: StockMovementType.ALLOCATION, quantity: 2 },
  1175. { type: StockMovementType.RELEASE, quantity: 2 },
  1176. { type: StockMovementType.ALLOCATION, quantity: 2 },
  1177. { type: StockMovementType.RELEASE, quantity: 1 },
  1178. { type: StockMovementType.RELEASE, quantity: 1 },
  1179. ]);
  1180. });
  1181. it('cancelled OrderLine.unitPrice is not zero', async () => {
  1182. const { order } = await adminClient.query(getOrderDocument, {
  1183. id: orderId,
  1184. });
  1185. expect(order?.lines[0].unitPrice).not.toBe(0);
  1186. });
  1187. it('order history contains expected entries', async () => {
  1188. const { order } = await adminClient.query(getOrderHistoryDocument, {
  1189. id: orderId,
  1190. options: {
  1191. skip: 0,
  1192. },
  1193. });
  1194. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  1195. {
  1196. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1197. data: {
  1198. from: 'Created',
  1199. to: 'AddingItems',
  1200. },
  1201. },
  1202. {
  1203. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1204. data: {
  1205. from: 'AddingItems',
  1206. to: 'ArrangingPayment',
  1207. },
  1208. },
  1209. {
  1210. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  1211. data: {
  1212. paymentId: 'T_4',
  1213. from: 'Created',
  1214. to: 'Authorized',
  1215. },
  1216. },
  1217. {
  1218. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1219. data: {
  1220. from: 'ArrangingPayment',
  1221. to: 'PaymentAuthorized',
  1222. },
  1223. },
  1224. {
  1225. type: HistoryEntryType.ORDER_CANCELLATION,
  1226. data: {
  1227. lines: [{ orderLineId: 'T_8', quantity: 1 }],
  1228. reason: 'cancel reason 1',
  1229. shippingCancelled: false,
  1230. },
  1231. },
  1232. {
  1233. type: HistoryEntryType.ORDER_CANCELLATION,
  1234. data: {
  1235. lines: [{ orderLineId: 'T_8', quantity: 1 }],
  1236. reason: 'cancel reason 2',
  1237. shippingCancelled: true,
  1238. },
  1239. },
  1240. {
  1241. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1242. data: {
  1243. from: 'PaymentAuthorized',
  1244. to: 'Cancelled',
  1245. },
  1246. },
  1247. ]);
  1248. });
  1249. });
  1250. describe('refunds', () => {
  1251. let orderId: string;
  1252. let paymentId: string;
  1253. let refundId: string;
  1254. beforeAll(async () => {
  1255. const result = await createTestOrder(
  1256. adminClient,
  1257. shopClient,
  1258. customers[0].emailAddress,
  1259. password,
  1260. );
  1261. orderId = result.orderId;
  1262. });
  1263. it('cannot refund from PaymentAuthorized state', async () => {
  1264. await proceedToArrangingPayment(shopClient);
  1265. const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
  1266. shopOrderGuard.assertSuccess(order);
  1267. expect(order.state).toBe('PaymentAuthorized');
  1268. paymentId = 'payments' in order && order.payments?.[0] ? order.payments[0].id : '';
  1269. const { refundOrder } = await adminClient.query(refundOrderDocument, {
  1270. input: {
  1271. lines: order.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  1272. shipping: 0,
  1273. adjustment: 0,
  1274. paymentId,
  1275. },
  1276. });
  1277. refundGuard.assertErrorResult(refundOrder);
  1278. expect(refundOrder.message).toBe('Cannot refund an Order in the "PaymentAuthorized" state');
  1279. expect(refundOrder.errorCode).toBe(ErrorCode.REFUND_ORDER_STATE_ERROR);
  1280. });
  1281. it('returns error result if no amount and no shipping', async () => {
  1282. const { order } = await adminClient.query(getOrderDocument, {
  1283. id: orderId,
  1284. });
  1285. const { settlePayment } = await adminClient.query(settlePaymentDocument, {
  1286. id: order!.payments![0].id,
  1287. });
  1288. paymentGuard.assertSuccess(settlePayment);
  1289. expect(settlePayment.state).toBe('Settled');
  1290. const { refundOrder } = await adminClient.query(refundOrderDocument, {
  1291. input: {
  1292. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
  1293. shipping: 0,
  1294. adjustment: 0,
  1295. paymentId,
  1296. },
  1297. });
  1298. refundGuard.assertErrorResult(refundOrder);
  1299. expect(refundOrder.message).toBe('Nothing to refund');
  1300. expect(refundOrder.errorCode).toBe(ErrorCode.NOTHING_TO_REFUND_ERROR);
  1301. });
  1302. it(
  1303. 'throws if paymentId not valid',
  1304. assertThrowsWithMessage(async () => {
  1305. await adminClient.query(getOrderDocument, {
  1306. id: orderId,
  1307. });
  1308. await adminClient.query(refundOrderDocument, {
  1309. input: {
  1310. lines: [],
  1311. shipping: 100,
  1312. adjustment: 0,
  1313. paymentId: 'T_999',
  1314. },
  1315. });
  1316. }, 'No Payment with the id "999" could be found'),
  1317. );
  1318. it('returns error result if payment and order lines do not belong to the same Order', async () => {
  1319. const { order } = await adminClient.query(getOrderDocument, {
  1320. id: orderId,
  1321. });
  1322. const { refundOrder } = await adminClient.query(refundOrderDocument, {
  1323. input: {
  1324. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  1325. shipping: 100,
  1326. adjustment: 0,
  1327. paymentId: 'T_1',
  1328. },
  1329. });
  1330. refundGuard.assertErrorResult(refundOrder);
  1331. expect(refundOrder.message).toBe('The Payment and OrderLines do not belong to the same Order');
  1332. expect(refundOrder.errorCode).toBe(ErrorCode.PAYMENT_ORDER_MISMATCH_ERROR);
  1333. });
  1334. it('creates a Refund to be manually settled', async () => {
  1335. const { order } = await adminClient.query(getOrderDocument, {
  1336. id: orderId,
  1337. });
  1338. const { refundOrder } = await adminClient.query(refundOrderDocument, {
  1339. input: {
  1340. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  1341. shipping: order!.shippingWithTax,
  1342. adjustment: 0,
  1343. reason: 'foo',
  1344. paymentId,
  1345. },
  1346. });
  1347. refundGuard.assertSuccess(refundOrder);
  1348. expect(refundOrder.shipping).toBe(order!.shippingWithTax);
  1349. expect(refundOrder.items).toBe(order!.subTotalWithTax);
  1350. expect(refundOrder.total).toBe(order!.totalWithTax);
  1351. expect(refundOrder.transactionId).toBe(null);
  1352. expect(refundOrder.state).toBe('Pending');
  1353. refundId = refundOrder.id;
  1354. });
  1355. it('manually settle a Refund', async () => {
  1356. const { settleRefund } = await adminClient.query(settleRefundDocument, {
  1357. input: {
  1358. id: refundId,
  1359. transactionId: 'aaabbb',
  1360. },
  1361. });
  1362. refundGuard.assertSuccess(settleRefund);
  1363. expect(settleRefund.state).toBe('Settled');
  1364. expect(settleRefund.transactionId).toBe('aaabbb');
  1365. });
  1366. it('order history contains expected entries', async () => {
  1367. const { order } = await adminClient.query(getOrderHistoryDocument, {
  1368. id: orderId,
  1369. options: {
  1370. skip: 0,
  1371. },
  1372. });
  1373. expect(order!.history.items.sort(sortById).map(pick(['type', 'data']))).toEqual([
  1374. {
  1375. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1376. data: {
  1377. from: 'Created',
  1378. to: 'AddingItems',
  1379. },
  1380. },
  1381. {
  1382. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1383. data: {
  1384. from: 'AddingItems',
  1385. to: 'ArrangingPayment',
  1386. },
  1387. },
  1388. {
  1389. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  1390. data: {
  1391. paymentId: 'T_5',
  1392. from: 'Created',
  1393. to: 'Authorized',
  1394. },
  1395. },
  1396. {
  1397. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1398. data: {
  1399. from: 'ArrangingPayment',
  1400. to: 'PaymentAuthorized',
  1401. },
  1402. },
  1403. {
  1404. type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
  1405. data: {
  1406. paymentId: 'T_5',
  1407. from: 'Authorized',
  1408. to: 'Settled',
  1409. },
  1410. },
  1411. {
  1412. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1413. data: {
  1414. from: 'PaymentAuthorized',
  1415. to: 'PaymentSettled',
  1416. },
  1417. },
  1418. {
  1419. type: HistoryEntryType.ORDER_REFUND_TRANSITION,
  1420. data: {
  1421. refundId: 'T_1',
  1422. reason: 'foo',
  1423. from: 'Pending',
  1424. to: 'Settled',
  1425. },
  1426. },
  1427. ]);
  1428. });
  1429. // https://github.com/vendure-ecommerce/vendure/issues/873
  1430. it('can add another refund if the first one fails', async () => {
  1431. await createTestOrder(adminClient, shopClient, customers[0].emailAddress, password);
  1432. await proceedToArrangingPayment(shopClient, 2);
  1433. const order = await addPaymentToOrder(shopClient, singleStageRefundFailingPaymentMethod);
  1434. shopOrderGuard.assertSuccess(order);
  1435. expect(order.state).toBe('PaymentSettled');
  1436. const shippingWithTax = 'shippingWithTax' in order ? order.shippingWithTax : 0;
  1437. const totalWithTax = 'totalWithTax' in order ? order.totalWithTax : 0;
  1438. const paymentIdForRefund = 'payments' in order && order.payments?.[0] ? order.payments[0].id : '';
  1439. const { refundOrder: refund1 } = await adminClient.query(refundOrderDocument, {
  1440. input: {
  1441. lines: order.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  1442. shipping: shippingWithTax,
  1443. adjustment: 0,
  1444. reason: 'foo',
  1445. paymentId: paymentIdForRefund,
  1446. },
  1447. });
  1448. refundGuard.assertSuccess(refund1);
  1449. expect(refund1.state).toBe('Failed');
  1450. expect(refund1.total).toBe(totalWithTax);
  1451. const shippingWithTax2 = 'shippingWithTax' in order ? order.shippingWithTax : 0;
  1452. const totalWithTax2 = 'totalWithTax' in order ? order.totalWithTax : 0;
  1453. const paymentId2 = 'payments' in order && order.payments?.[0] ? order.payments[0].id : '';
  1454. const { refundOrder: refund2 } = await adminClient.query(refundOrderDocument, {
  1455. input: {
  1456. lines: order.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  1457. shipping: shippingWithTax2,
  1458. adjustment: 0,
  1459. reason: 'foo',
  1460. paymentId: paymentId2,
  1461. },
  1462. });
  1463. refundGuard.assertSuccess(refund2);
  1464. expect(refund2.state).toBe('Settled');
  1465. expect(refund2.total).toBe(totalWithTax2);
  1466. });
  1467. // https://github.com/vendure-ecommerce/vendure/issues/2302
  1468. it('passes correct amount to createRefund function after cancellation', async () => {
  1469. await createTestOrder(adminClient, shopClient, customers[0].emailAddress, password);
  1470. await proceedToArrangingPayment(shopClient, 2);
  1471. const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod);
  1472. shopOrderGuard.assertSuccess(order);
  1473. expect(order.state).toBe('PaymentSettled');
  1474. const { cancelOrder } = await adminClient.query(cancelOrderDocument, {
  1475. input: {
  1476. orderId: order.id,
  1477. lines: order.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  1478. reason: 'cancel reason 1',
  1479. },
  1480. });
  1481. canceledOrderGuard.assertSuccess(cancelOrder);
  1482. const shippingWithTax3 = 'shippingWithTax' in order ? order.shippingWithTax : 0;
  1483. const totalWithTax3 = 'totalWithTax' in order ? order.totalWithTax : 0;
  1484. const paymentId3 = 'payments' in order && order.payments?.[0] ? order.payments[0].id : '';
  1485. const { refundOrder } = await adminClient.query(refundOrderDocument, {
  1486. input: {
  1487. lines: order.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
  1488. shipping: shippingWithTax3,
  1489. adjustment: 0,
  1490. reason: 'foo',
  1491. paymentId: paymentId3,
  1492. },
  1493. });
  1494. refundGuard.assertSuccess(refundOrder);
  1495. expect(refundOrder.state).toBe('Settled');
  1496. expect(refundOrder.total).toBe(totalWithTax3);
  1497. expect(refundOrder.metadata.amount).toBe(totalWithTax3);
  1498. });
  1499. });
  1500. describe('payment cancellation', () => {
  1501. it("cancelling payment calls the method's cancelPayment handler", async () => {
  1502. await createTestOrder(adminClient, shopClient, customers[0].emailAddress, password);
  1503. await proceedToArrangingPayment(shopClient);
  1504. const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
  1505. shopOrderGuard.assertSuccess(order);
  1506. expect(order.state).toBe('PaymentAuthorized');
  1507. const paymentId = 'payments' in order && order.payments?.[0] ? order.payments[0].id : '';
  1508. if (!paymentId) throw new Error('Expected payment');
  1509. expect(onCancelPaymentSpy).not.toHaveBeenCalled();
  1510. const { cancelPayment } = await adminClient.query(cancelPaymentDocument, {
  1511. paymentId,
  1512. });
  1513. paymentGuard.assertSuccess(cancelPayment);
  1514. expect(cancelPayment.state).toBe('Cancelled');
  1515. expect(cancelPayment.metadata.cancellationCode).toBe('12345');
  1516. expect(onCancelPaymentSpy).toHaveBeenCalledTimes(1);
  1517. });
  1518. it('cancellation failure', async () => {
  1519. await createTestOrder(adminClient, shopClient, customers[0].emailAddress, password);
  1520. await proceedToArrangingPayment(shopClient);
  1521. const order = await addPaymentToOrder(shopClient, failsToCancelPaymentMethod);
  1522. shopOrderGuard.assertSuccess(order);
  1523. expect(order.state).toBe('PaymentAuthorized');
  1524. const paymentId = 'payments' in order && order.payments?.[0] ? order.payments[0].id : '';
  1525. if (!paymentId) throw new Error('Expected payment');
  1526. const { cancelPayment } = await adminClient.query(cancelPaymentDocument, {
  1527. paymentId,
  1528. });
  1529. paymentGuard.assertErrorResult(cancelPayment);
  1530. expect(cancelPayment.message).toBe('Cancelling the payment failed');
  1531. const { order: checkorder } = await adminClient.query(getOrderDocument, {
  1532. id: order.id,
  1533. });
  1534. expect(checkorder!.payments![0].state).toBe('Authorized');
  1535. expect(checkorder!.payments![0].metadata).toEqual({ cancellationData: 'foo' });
  1536. });
  1537. });
  1538. describe('refund by amount', () => {
  1539. let orderId: string;
  1540. let paymentId: string;
  1541. beforeAll(async () => {
  1542. const result = await createTestOrder(
  1543. adminClient,
  1544. shopClient,
  1545. customers[0].emailAddress,
  1546. password,
  1547. );
  1548. orderId = result.orderId;
  1549. await proceedToArrangingPayment(shopClient);
  1550. const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod);
  1551. shopOrderGuard.assertSuccess(order);
  1552. paymentId = 'payments' in order && order.payments?.[0] ? order.payments[0].id : '';
  1553. if (!paymentId) throw new Error('Expected payment');
  1554. });
  1555. it('return RefundAmountError if amount too large', async () => {
  1556. const { refundOrder } = await adminClient.query(refundOrderDocument, {
  1557. input: {
  1558. lines: [],
  1559. shipping: 0,
  1560. adjustment: 0,
  1561. amount: 999999,
  1562. paymentId,
  1563. },
  1564. });
  1565. refundGuard.assertErrorResult(refundOrder);
  1566. expect(refundOrder.message).toBe(
  1567. 'The amount specified exceeds the refundable amount for this payment',
  1568. );
  1569. expect(refundOrder.errorCode).toBe(ErrorCode.REFUND_AMOUNT_ERROR);
  1570. });
  1571. it('creates a partial refund for the given amount', async () => {
  1572. const { order } = await adminClient.query(getOrderDocument, {
  1573. id: orderId,
  1574. });
  1575. const refundAmount = order!.totalWithTax - 500;
  1576. const { refundOrder } = await adminClient.query(refundOrderDocument, {
  1577. input: {
  1578. lines: [],
  1579. shipping: 0,
  1580. adjustment: 0,
  1581. amount: refundAmount,
  1582. paymentId,
  1583. },
  1584. });
  1585. refundGuard.assertSuccess(refundOrder);
  1586. expect(refundOrder.total).toBe(refundAmount);
  1587. });
  1588. });
  1589. describe('order notes', () => {
  1590. let orderId: string;
  1591. let firstNoteId: string;
  1592. beforeAll(async () => {
  1593. const result = await createTestOrder(
  1594. adminClient,
  1595. shopClient,
  1596. customers[2].emailAddress,
  1597. password,
  1598. );
  1599. orderId = result.orderId;
  1600. });
  1601. it('private note', async () => {
  1602. const { addNoteToOrder } = await adminClient.query(addNoteToOrderDocument, {
  1603. input: {
  1604. id: orderId,
  1605. note: 'A private note',
  1606. isPublic: false,
  1607. },
  1608. });
  1609. expect(addNoteToOrder.id).toBe(orderId);
  1610. const { order } = await adminClient.query(getOrderHistoryDocument, {
  1611. id: orderId,
  1612. options: {
  1613. skip: 1,
  1614. },
  1615. });
  1616. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  1617. {
  1618. type: HistoryEntryType.ORDER_NOTE,
  1619. data: {
  1620. note: 'A private note',
  1621. },
  1622. },
  1623. ]);
  1624. firstNoteId = order!.history.items[0].id;
  1625. const { activeOrder } = await shopClient.query(getActiveOrderDocument);
  1626. expect(activeOrder!.history.items.map(pick(['type']))).toEqual([
  1627. { type: HistoryEntryType.ORDER_STATE_TRANSITION },
  1628. ]);
  1629. });
  1630. it('public note', async () => {
  1631. const { addNoteToOrder } = await adminClient.query(addNoteToOrderDocument, {
  1632. input: {
  1633. id: orderId,
  1634. note: 'A public note',
  1635. isPublic: true,
  1636. },
  1637. });
  1638. expect(addNoteToOrder.id).toBe(orderId);
  1639. const { order } = await adminClient.query(getOrderHistoryDocument, {
  1640. id: orderId,
  1641. options: {
  1642. skip: 2,
  1643. },
  1644. });
  1645. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  1646. {
  1647. type: HistoryEntryType.ORDER_NOTE,
  1648. data: {
  1649. note: 'A public note',
  1650. },
  1651. },
  1652. ]);
  1653. const { activeOrder } = await shopClient.query(getActiveOrderDocument);
  1654. expect(activeOrder!.history.items.map(pick(['type', 'data']))).toEqual([
  1655. {
  1656. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  1657. data: {
  1658. from: 'Created',
  1659. to: 'AddingItems',
  1660. },
  1661. },
  1662. {
  1663. type: HistoryEntryType.ORDER_NOTE,
  1664. data: {
  1665. note: 'A public note',
  1666. },
  1667. },
  1668. ]);
  1669. });
  1670. it('update note', async () => {
  1671. const { updateOrderNote } = await adminClient.query(updateOrderNoteDocument, {
  1672. input: {
  1673. noteId: firstNoteId,
  1674. note: 'An updated note',
  1675. },
  1676. });
  1677. expect(updateOrderNote.data).toEqual({
  1678. note: 'An updated note',
  1679. });
  1680. });
  1681. it('delete note', async () => {
  1682. const { order: before } = await adminClient.query(getOrderHistoryDocument, { id: orderId });
  1683. expect(before?.history.totalItems).toBe(3);
  1684. const { deleteOrderNote } = await adminClient.query(deleteOrderNoteDocument, {
  1685. id: firstNoteId,
  1686. });
  1687. expect(deleteOrderNote.result).toBe(DeletionResult.DELETED);
  1688. const { order: after } = await adminClient.query(getOrderHistoryDocument, { id: orderId });
  1689. expect(after?.history.totalItems).toBe(2);
  1690. });
  1691. });
  1692. describe('multiple payments', () => {
  1693. const PARTIAL_PAYMENT_AMOUNT = 1000;
  1694. let orderId: string;
  1695. let orderTotalWithTax: number;
  1696. let payment1Id: string;
  1697. let productInOrder: ResultOf<typeof getProductWithVariantsDocument>['product'];
  1698. beforeAll(async () => {
  1699. const result = await createTestOrder(
  1700. adminClient,
  1701. shopClient,
  1702. customers[1].emailAddress,
  1703. password,
  1704. );
  1705. orderId = result.orderId;
  1706. productInOrder = result.product;
  1707. });
  1708. it('adds a partial payment', async () => {
  1709. await proceedToArrangingPayment(shopClient, 2);
  1710. const { addPaymentToOrder: order } = await shopClient.query(addPaymentDocument, {
  1711. input: {
  1712. method: partialPaymentMethod.code,
  1713. metadata: {
  1714. amount: PARTIAL_PAYMENT_AMOUNT,
  1715. },
  1716. },
  1717. });
  1718. shopOrderGuard.assertSuccess(order);
  1719. orderTotalWithTax = order.totalWithTax;
  1720. expect(order.state).toBe('ArrangingPayment');
  1721. expect(order.payments?.length).toBe(1);
  1722. expect(omit(order.payments![0], ['id'])).toEqual({
  1723. amount: PARTIAL_PAYMENT_AMOUNT,
  1724. metadata: {
  1725. public: {
  1726. amount: PARTIAL_PAYMENT_AMOUNT,
  1727. },
  1728. },
  1729. method: partialPaymentMethod.code,
  1730. state: 'Settled',
  1731. transactionId: '12345',
  1732. });
  1733. payment1Id = order.payments![0].id;
  1734. });
  1735. it('adds another payment to make up order totalWithTax', async () => {
  1736. const { addPaymentToOrder: order } = await shopClient.query(addPaymentDocument, {
  1737. input: {
  1738. method: singleStageRefundablePaymentMethod.code,
  1739. metadata: {},
  1740. },
  1741. });
  1742. shopOrderGuard.assertSuccess(order);
  1743. expect(order.state).toBe('PaymentSettled');
  1744. expect(order.payments?.length).toBe(2);
  1745. expect(
  1746. omit(order.payments!.find(p => p.method === singleStageRefundablePaymentMethod.code)!, [
  1747. 'id',
  1748. ]),
  1749. ).toEqual({
  1750. amount: orderTotalWithTax - PARTIAL_PAYMENT_AMOUNT,
  1751. metadata: {},
  1752. method: singleStageRefundablePaymentMethod.code,
  1753. state: 'Settled',
  1754. transactionId: '12345',
  1755. });
  1756. });
  1757. it('partial refunding of order with multiple payments', async () => {
  1758. const { order } = await adminClient.query(getOrderDocument, {
  1759. id: orderId,
  1760. });
  1761. const { refundOrder } = await adminClient.query(refundOrderDocument, {
  1762. input: {
  1763. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  1764. shipping: 0,
  1765. adjustment: 0,
  1766. reason: 'first refund',
  1767. paymentId: payment1Id,
  1768. },
  1769. });
  1770. refundGuard.assertSuccess(refundOrder);
  1771. expect(refundOrder.total).toBe(PARTIAL_PAYMENT_AMOUNT);
  1772. const { order: orderWithPayments } = await adminClient.query(getOrderWithPaymentsDocument, {
  1773. id: orderId,
  1774. });
  1775. expect(orderWithPayments?.payments!.sort(sortById)[0].refunds.length).toBe(1);
  1776. expect(orderWithPayments?.payments!.sort(sortById)[0].refunds[0].total).toBe(
  1777. PARTIAL_PAYMENT_AMOUNT,
  1778. );
  1779. expect(orderWithPayments?.payments!.sort(sortById)[1].refunds.length).toBe(1);
  1780. expect(orderWithPayments?.payments!.sort(sortById)[1].refunds[0].total).toBe(
  1781. productInOrder!.variants[0].priceWithTax - PARTIAL_PAYMENT_AMOUNT,
  1782. );
  1783. });
  1784. it('refunding remaining amount of order with multiple payments', async () => {
  1785. const { order } = await adminClient.query(getOrderDocument, {
  1786. id: orderId,
  1787. });
  1788. const { refundOrder } = await adminClient.query(refundOrderDocument, {
  1789. input: {
  1790. lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  1791. shipping: order!.shippingWithTax,
  1792. adjustment: 0,
  1793. reason: 'second refund',
  1794. paymentId: payment1Id,
  1795. },
  1796. });
  1797. refundGuard.assertSuccess(refundOrder);
  1798. expect(refundOrder.total).toBe(order!.totalWithTax - order!.lines[0].unitPriceWithTax);
  1799. const { order: orderWithPayments } = await adminClient.query(getOrderWithPaymentsDocument, {
  1800. id: orderId,
  1801. });
  1802. expect(orderWithPayments?.payments!.sort(sortById)[0].refunds.length).toBe(1);
  1803. expect(orderWithPayments?.payments!.sort(sortById)[0].refunds[0].total).toBe(
  1804. PARTIAL_PAYMENT_AMOUNT,
  1805. );
  1806. expect(orderWithPayments?.payments!.sort(sortById)[1].refunds.length).toBe(2);
  1807. expect(orderWithPayments?.payments!.sort(sortById)[1].refunds[0].total).toBe(
  1808. productInOrder!.variants[0].priceWithTax - PARTIAL_PAYMENT_AMOUNT,
  1809. );
  1810. expect(orderWithPayments?.payments!.sort(sortById)[1].refunds[1].total).toBe(
  1811. productInOrder!.variants[0].priceWithTax + order!.shippingWithTax,
  1812. );
  1813. });
  1814. // TODO: LINK RELATED ISSUE HERE
  1815. it('creates duplicate RefundLines when same lines sent to multiple payment refunds', async () => {
  1816. const result = await createTestOrder(
  1817. adminClient,
  1818. shopClient,
  1819. customers[2].emailAddress,
  1820. password,
  1821. );
  1822. const testOrderId = result.orderId;
  1823. await proceedToArrangingPayment(shopClient, 2);
  1824. const { addPaymentToOrder: orderAfterFirstPayment } = await shopClient.query(addPaymentDocument, {
  1825. input: {
  1826. method: partialPaymentMethod.code,
  1827. metadata: { amount: PARTIAL_PAYMENT_AMOUNT },
  1828. },
  1829. });
  1830. shopOrderGuard.assertSuccess(orderAfterFirstPayment);
  1831. const firstPaymentId = orderAfterFirstPayment.payments![0].id;
  1832. const { addPaymentToOrder: completedOrder } = await shopClient.query(addPaymentDocument, {
  1833. input: {
  1834. method: singleStageRefundablePaymentMethod.code,
  1835. metadata: {},
  1836. },
  1837. });
  1838. shopOrderGuard.assertSuccess(completedOrder);
  1839. const secondPayment = completedOrder.payments!.find(
  1840. p => p.method === singleStageRefundablePaymentMethod.code,
  1841. )!;
  1842. const secondPaymentId = secondPayment.id;
  1843. const secondPaymentAmount = secondPayment.amount;
  1844. const { order } = await adminClient.query(getOrderDocument, { id: testOrderId });
  1845. const refundLines = order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity }));
  1846. const totalQuantityOrdered = order!.lines.reduce((sum, l) => sum + l.quantity, 0);
  1847. expect(totalQuantityOrdered).toBeGreaterThan(0);
  1848. const { refundOrder: refund1 } = await adminClient.query(refundOrderDocument, {
  1849. input: {
  1850. lines: refundLines,
  1851. shipping: 0,
  1852. adjustment: 0,
  1853. amount: PARTIAL_PAYMENT_AMOUNT,
  1854. reason: 'Test refund 1',
  1855. paymentId: firstPaymentId,
  1856. },
  1857. });
  1858. refundGuard.assertSuccess(refund1);
  1859. const { refundOrder: refund2 } = await adminClient.query(refundOrderDocument, {
  1860. input: {
  1861. lines: refundLines,
  1862. shipping: 0,
  1863. adjustment: 0,
  1864. amount: secondPaymentAmount,
  1865. reason: 'Test refund 2',
  1866. paymentId: secondPaymentId,
  1867. },
  1868. });
  1869. refundGuard.assertSuccess(refund2);
  1870. const { order: orderWithRefunds } = await adminClient.query(getOrderWithRefundLinesDocument, {
  1871. id: testOrderId,
  1872. });
  1873. const allRefundLines: Array<{ orderLineId: string; quantity: number }> = [];
  1874. for (const payment of orderWithRefunds!.payments) {
  1875. for (const refund of payment.refunds) {
  1876. allRefundLines.push(...refund.lines);
  1877. }
  1878. }
  1879. const totalRefundedQuantity = allRefundLines.reduce((sum, rl) => sum + rl.quantity, 0);
  1880. expect(totalRefundedQuantity).toBe(totalQuantityOrdered);
  1881. });
  1882. // https://github.com/vendure-ecommerce/vendure/issues/847
  1883. it('manual call to settlePayment works with multiple payments', async () => {
  1884. await createTestOrder(adminClient, shopClient, customers[1].emailAddress, password);
  1885. await proceedToArrangingPayment(shopClient);
  1886. await shopClient.query(addPaymentDocument, {
  1887. input: {
  1888. method: partialPaymentMethod.code,
  1889. metadata: {
  1890. amount: PARTIAL_PAYMENT_AMOUNT,
  1891. authorizeOnly: true,
  1892. },
  1893. },
  1894. });
  1895. const { addPaymentToOrder: order } = await shopClient.query(addPaymentDocument, {
  1896. input: {
  1897. method: singleStageRefundablePaymentMethod.code,
  1898. metadata: {},
  1899. },
  1900. });
  1901. shopOrderGuard.assertSuccess(order);
  1902. expect(order.state).toBe('PaymentAuthorized');
  1903. const { settlePayment } = await adminClient.query(settlePaymentDocument, {
  1904. id: order.payments!.find(p => p.method === partialPaymentMethod.code)!.id,
  1905. });
  1906. paymentGuard.assertSuccess(settlePayment);
  1907. expect(settlePayment.state).toBe('Settled');
  1908. const { order: order2 } = await adminClient.query(getOrderDocument, {
  1909. id: order.id,
  1910. });
  1911. expect(order2?.state).toBe('PaymentSettled');
  1912. });
  1913. });
  1914. // https://github.com/vendure-ecommerce/vendure/issues/2505
  1915. describe('updating order customer', () => {
  1916. let orderId: string;
  1917. let customerId: string;
  1918. it('set up order', async () => {
  1919. const result = await createTestOrder(
  1920. adminClient,
  1921. shopClient,
  1922. customers[1].emailAddress,
  1923. password,
  1924. );
  1925. orderId = result.orderId;
  1926. customerId = customers[1].id;
  1927. await proceedToArrangingPayment(shopClient);
  1928. const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod);
  1929. shopOrderGuard.assertSuccess(order);
  1930. const customerIdCheck = 'customer' in order ? order.customer?.id : undefined;
  1931. expect(customerIdCheck).toBe(customerId);
  1932. });
  1933. it(
  1934. 'throws in invalid orderId',
  1935. assertThrowsWithMessage(async () => {
  1936. await adminClient.query(setOrderCustomerDocument, {
  1937. input: {
  1938. orderId: 'T_9999',
  1939. customerId: customers[2].id,
  1940. note: 'Testing',
  1941. },
  1942. });
  1943. }, 'No Order with the id "9999" could be found'),
  1944. );
  1945. it(
  1946. 'throws in invalid orderId',
  1947. assertThrowsWithMessage(async () => {
  1948. await adminClient.query(setOrderCustomerDocument, {
  1949. input: {
  1950. orderId,
  1951. customerId: 'T_999',
  1952. note: 'Testing',
  1953. },
  1954. });
  1955. }, 'No Customer with the id "999" could be found'),
  1956. );
  1957. it('update order customer', async () => {
  1958. const newCustomerId = customers[2].id;
  1959. const { setOrderCustomer } = await adminClient.query(setOrderCustomerDocument, {
  1960. input: {
  1961. orderId,
  1962. customerId: customers[2].id,
  1963. note: 'Testing',
  1964. },
  1965. });
  1966. expect(setOrderCustomer?.customer?.id).toBe(newCustomerId);
  1967. });
  1968. it('adds a history entry for the customer update', async () => {
  1969. const { order } = await adminClient.query(getOrderHistoryDocument, {
  1970. id: orderId,
  1971. options: {
  1972. skip: 4,
  1973. },
  1974. });
  1975. expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
  1976. {
  1977. data: {
  1978. previousCustomerId: customerId,
  1979. previousCustomerName: 'Trevor Donnelly',
  1980. newCustomerId: customers[2].id,
  1981. newCustomerName: `${customers[2].firstName} ${customers[2].lastName}`,
  1982. note: 'Testing',
  1983. },
  1984. type: HistoryEntryType.ORDER_CUSTOMER_UPDATED,
  1985. },
  1986. ]);
  1987. });
  1988. });
  1989. describe('issues', () => {
  1990. // https://github.com/vendure-ecommerce/vendure/issues/639
  1991. it('returns fulfillments for Order with no lines', async () => {
  1992. await shopClient.asAnonymousUser();
  1993. // Apply a coupon code just to create an active order with no OrderLines
  1994. await shopClient.query(applyCouponCodeDocument, {
  1995. couponCode: 'TEST',
  1996. });
  1997. const { activeOrder } = await shopClient.query(getActiveOrderDocument);
  1998. const { order } = await adminClient.query(getOrderFulfillmentsDocument, {
  1999. id: activeOrder!.id,
  2000. });
  2001. expect(order?.fulfillments).toEqual([]);
  2002. });
  2003. // https://github.com/vendure-ecommerce/vendure/issues/603
  2004. it('orders correctly resolves quantities and OrderItems', async () => {
  2005. await shopClient.asAnonymousUser();
  2006. const { addItemToOrder } = await shopClient.query(addItemToOrderDocument, {
  2007. productVariantId: 'T_1',
  2008. quantity: 2,
  2009. });
  2010. shopOrderGuard.assertSuccess(addItemToOrder);
  2011. const { orders } = await adminClient.query(getOrderListWithQtyDocument, {
  2012. options: {
  2013. filter: {
  2014. code: { eq: addItemToOrder.code },
  2015. },
  2016. },
  2017. });
  2018. expect(orders.items[0].totalQuantity).toBe(2);
  2019. expect(orders.items[0].lines[0].quantity).toBe(2);
  2020. });
  2021. // https://github.com/vendure-ecommerce/vendure/issues/716
  2022. it('get an Order with a deleted ShippingMethod', async () => {
  2023. const { createShippingMethod: shippingMethod } = await adminClient.query(
  2024. createShippingMethodDocument,
  2025. {
  2026. input: {
  2027. code: 'royal-mail',
  2028. translations: [
  2029. { languageCode: LanguageCode.en, name: 'Royal Mail', description: '' },
  2030. ],
  2031. fulfillmentHandler: manualFulfillmentHandler.code,
  2032. checker: {
  2033. code: defaultShippingEligibilityChecker.code,
  2034. arguments: [{ name: 'orderMinimum', value: '0' }],
  2035. },
  2036. calculator: {
  2037. code: defaultShippingCalculator.code,
  2038. arguments: [
  2039. { name: 'rate', value: '500' },
  2040. { name: 'taxRate', value: '0' },
  2041. ],
  2042. },
  2043. },
  2044. },
  2045. );
  2046. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  2047. await shopClient.query(addItemToOrderDocument, {
  2048. productVariantId: 'T_1',
  2049. quantity: 2,
  2050. });
  2051. await shopClient.query(setShippingAddressDocument, {
  2052. input: {
  2053. fullName: 'name',
  2054. streetLine1: '12 the street',
  2055. city: 'foo',
  2056. postalCode: '123456',
  2057. countryCode: 'US',
  2058. },
  2059. });
  2060. const { setOrderShippingMethod: order } = await shopClient.query(setShippingMethodDocument, {
  2061. id: [shippingMethod.id],
  2062. });
  2063. shopOrderGuard.assertSuccess(order);
  2064. await adminClient.query(deleteShippingMethodDocument, {
  2065. id: shippingMethod.id,
  2066. });
  2067. const { order: order2 } = await adminClient.query(getOrderDocument, {
  2068. id: order.id,
  2069. });
  2070. expect(order2?.shippingLines[0]).toEqual({
  2071. priceWithTax: 500,
  2072. shippingMethod: pick(shippingMethod, ['id', 'name', 'code', 'description']),
  2073. });
  2074. });
  2075. // https://github.com/vendure-ecommerce/vendure/issues/868
  2076. it('allows multiple refunds of same OrderLine', async () => {
  2077. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  2078. await shopClient.query(addItemToOrderDocument, {
  2079. productVariantId: 'T_1',
  2080. quantity: 2,
  2081. });
  2082. await proceedToArrangingPayment(shopClient);
  2083. const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod);
  2084. shopOrderGuard.assertSuccess(order);
  2085. const paymentId4 = 'payments' in order && order.payments?.[0] ? order.payments[0].id : '';
  2086. const { refundOrder: refund1 } = await adminClient.query(refundOrderDocument, {
  2087. input: {
  2088. lines: order.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  2089. shipping: 0,
  2090. adjustment: 0,
  2091. reason: 'foo',
  2092. paymentId: paymentId4,
  2093. },
  2094. });
  2095. refundGuard.assertSuccess(refund1);
  2096. const { refundOrder: refund2 } = await adminClient.query(refundOrderDocument, {
  2097. input: {
  2098. lines: order.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
  2099. shipping: 0,
  2100. adjustment: 0,
  2101. reason: 'foo',
  2102. paymentId: paymentId4,
  2103. },
  2104. });
  2105. refundGuard.assertSuccess(refund2);
  2106. });
  2107. // https://github.com/vendure-ecommerce/vendure/issues/1125
  2108. it('resolves deleted Product of OrderLine ProductVariants', async () => {
  2109. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  2110. await shopClient.query(addItemToOrderDocument, {
  2111. productVariantId: 'T_7',
  2112. quantity: 1,
  2113. });
  2114. await proceedToArrangingPayment(shopClient);
  2115. const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod);
  2116. shopOrderGuard.assertSuccess(order);
  2117. await adminClient.query(deleteProductDocument, {
  2118. id: 'T_3',
  2119. });
  2120. const { activeCustomer } = await shopClient.query(
  2121. getActiveCustomerWithOrdersProductSlugDocument,
  2122. {
  2123. options: {
  2124. sort: {
  2125. createdAt: SortOrder.ASC,
  2126. },
  2127. },
  2128. },
  2129. );
  2130. expect(
  2131. activeCustomer!.orders.items[activeCustomer!.orders.items.length - 1].lines[0].productVariant
  2132. .product.slug,
  2133. ).toBe('gaming-pc');
  2134. });
  2135. // https://github.com/vendure-ecommerce/vendure/issues/1508
  2136. it('resolves price of deleted ProductVariant of OrderLine', async () => {
  2137. const { activeCustomer } = await shopClient.query(
  2138. getActiveCustomerWithOrdersProductPriceDocument,
  2139. {
  2140. options: {
  2141. sort: {
  2142. createdAt: SortOrder.ASC,
  2143. },
  2144. },
  2145. },
  2146. );
  2147. expect(
  2148. activeCustomer!.orders.items[activeCustomer!.orders.items.length - 1].lines[0].productVariant
  2149. .price,
  2150. ).toBe(108720);
  2151. });
  2152. // https://github.com/vendure-ecommerce/vendure/issues/2204
  2153. it('creates correct history entries and results in correct state when manually adding payment to order', async () => {
  2154. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  2155. const { addItemToOrder } = await shopClient.query(addItemToOrderDocument, {
  2156. productVariantId: 'T_1',
  2157. quantity: 2,
  2158. });
  2159. await proceedToArrangingPayment(shopClient);
  2160. shopOrderGuard.assertSuccess(addItemToOrder);
  2161. const { addManualPaymentToOrder } = await adminClient.query(addManualPaymentDocument, {
  2162. input: {
  2163. orderId: addItemToOrder.id,
  2164. metadata: {},
  2165. method: twoStagePaymentMethod.code,
  2166. transactionId: '12345',
  2167. },
  2168. });
  2169. canceledOrderGuard.assertSuccess(addManualPaymentToOrder as any);
  2170. if ('id' in addManualPaymentToOrder) {
  2171. const { order: orderWithHistory } = await adminClient.query(getOrderHistoryDocument, {
  2172. id: addManualPaymentToOrder.id,
  2173. });
  2174. const stateTransitionHistory = orderWithHistory!.history.items
  2175. .filter(i => i.type === HistoryEntryType.ORDER_STATE_TRANSITION)
  2176. .map(i => i.data);
  2177. expect(stateTransitionHistory).toEqual([
  2178. { from: 'Created', to: 'AddingItems' },
  2179. { from: 'AddingItems', to: 'ArrangingPayment' },
  2180. { from: 'ArrangingPayment', to: 'PaymentSettled' },
  2181. ]);
  2182. const { order } = await adminClient.query(getOrderDocument, {
  2183. id: addManualPaymentToOrder.id,
  2184. });
  2185. expect(order!.state).toBe('PaymentSettled');
  2186. }
  2187. });
  2188. // https://github.com/vendure-ecommerce/vendure/issues/2191
  2189. it('correctly transitions order & fulfillment on partial fulfillment being shipped', async () => {
  2190. await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
  2191. const { addItemToOrder } = await shopClient.query(addItemToOrderDocument, {
  2192. productVariantId: 'T_6',
  2193. quantity: 3,
  2194. });
  2195. await proceedToArrangingPayment(shopClient);
  2196. shopOrderGuard.assertSuccess(addItemToOrder);
  2197. const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod);
  2198. shopOrderGuard.assertSuccess(order);
  2199. const { addFulfillmentToOrder } = await adminClient.query(createFulfillmentDocument, {
  2200. input: {
  2201. lines: [{ orderLineId: order.lines[0].id, quantity: 2 }],
  2202. handler: {
  2203. code: manualFulfillmentHandler.code,
  2204. arguments: [
  2205. { name: 'method', value: 'Test2' },
  2206. { name: 'trackingCode', value: '222' },
  2207. ],
  2208. },
  2209. },
  2210. });
  2211. fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
  2212. const { transitionFulfillmentToState } = await adminClient.query(transitFulfillmentDocument, {
  2213. id: addFulfillmentToOrder.id,
  2214. state: 'Shipped',
  2215. });
  2216. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  2217. expect(transitionFulfillmentToState.id).toBe(addFulfillmentToOrder.id);
  2218. expect(transitionFulfillmentToState.state).toBe('Shipped');
  2219. const { order: order2 } = await adminClient.query(getOrderDocument, {
  2220. id: order.id,
  2221. });
  2222. expect(order2?.state).toBe('PartiallyShipped');
  2223. });
  2224. });
  2225. describe('multiple items to order', () => {
  2226. it('adds multiple items to a new active order', async () => {
  2227. await shopClient.asAnonymousUser();
  2228. const { addItemsToOrder } = await shopClient.query(addMultipleItemsToOrderDocument, {
  2229. inputs: [
  2230. {
  2231. productVariantId: 'T_1',
  2232. quantity: 5,
  2233. },
  2234. {
  2235. productVariantId: 'T_2',
  2236. quantity: 3,
  2237. },
  2238. ],
  2239. });
  2240. expect(addItemsToOrder.order.lines.length).toBe(2);
  2241. expect(addItemsToOrder.order.lines[0].quantity).toBe(5);
  2242. expect(addItemsToOrder.order.lines[1].quantity).toBe(3);
  2243. });
  2244. it('adds successful items and returns error results for failed items', async () => {
  2245. await shopClient.asAnonymousUser();
  2246. const { addItemsToOrder } = await shopClient.query(addMultipleItemsToOrderDocument, {
  2247. inputs: [
  2248. {
  2249. productVariantId: 'T_1',
  2250. quantity: 1,
  2251. },
  2252. {
  2253. productVariantId: 'T_2',
  2254. quantity: 999999, // Exceeds limit
  2255. },
  2256. ],
  2257. });
  2258. const t1 = addItemsToOrder.order.lines.find(l => l.productVariant.id === 'T_1');
  2259. // Should have added 1 of T_1
  2260. expect(t1?.quantity).toBe(1);
  2261. // Should not have added T_2
  2262. const t2 = addItemsToOrder.order.lines.find(l => l.productVariant.id === 'T_2');
  2263. expect(t2).toBeUndefined();
  2264. // Should have errors
  2265. expect(addItemsToOrder.errorResults.length).toBe(1);
  2266. expect(addItemsToOrder.errorResults[0].errorCode).toBe('ORDER_LIMIT_ERROR');
  2267. expect(addItemsToOrder.errorResults[0].message).toBe('ORDER_LIMIT_ERROR');
  2268. });
  2269. });
  2270. });
  2271. async function createTestOrder(
  2272. adminClient: SimpleGraphQLClient,
  2273. shopClient: SimpleGraphQLClient,
  2274. emailAddress: string,
  2275. password: string,
  2276. ): Promise<{
  2277. orderId: string;
  2278. product: ResultOf<typeof getProductWithVariantsDocument>['product'];
  2279. productVariantId: string;
  2280. }> {
  2281. const result = await adminClient.query(getProductWithVariantsDocument, {
  2282. id: 'T_3',
  2283. });
  2284. const product = result.product!;
  2285. const productVariantId = product.variants[0].id;
  2286. // Set the ProductVariant to trackInventory
  2287. await adminClient.query(updateProductVariantsDocument, {
  2288. input: [
  2289. {
  2290. id: productVariantId,
  2291. trackInventory: GlobalFlag.TRUE,
  2292. },
  2293. ],
  2294. });
  2295. // Add the ProductVariant to the Order
  2296. await shopClient.asUserWithCredentials(emailAddress, password);
  2297. const { addItemToOrder } = await shopClient.query(addItemToOrderDocument, {
  2298. productVariantId,
  2299. quantity: 2,
  2300. });
  2301. if ('errorCode' in addItemToOrder) {
  2302. throw new Error(`Failed to add item to order: ${addItemToOrder.message}`);
  2303. }
  2304. const orderId = addItemToOrder.id;
  2305. return { product, productVariantId, orderId };
  2306. }
  2307. async function getUnfulfilledOrderLineInput(
  2308. client: SimpleGraphQLClient,
  2309. id: string,
  2310. ): Promise<Array<{ quantity: number; orderLineId: string }>> {
  2311. const { order } = await client.query(getOrderDocument, {
  2312. id,
  2313. });
  2314. const allFulfillmentLines =
  2315. order?.fulfillments
  2316. ?.filter(f => f.state !== 'Cancelled')
  2317. .reduce(
  2318. (all, f) => [...all, ...f.lines],
  2319. [] as Array<{ orderLineId: string; quantity: number }>,
  2320. ) || [];
  2321. const unfulfilledItems =
  2322. order?.lines
  2323. .map(l => {
  2324. const fulfilledQuantity = allFulfillmentLines
  2325. .filter(fl => fl.orderLineId === l.id)
  2326. .reduce((sum, fl) => sum + fl.quantity, 0);
  2327. return { orderLineId: l.id, unfulfilled: l.quantity - fulfilledQuantity };
  2328. })
  2329. .filter(l => 0 < l.unfulfilled) || [];
  2330. return unfulfilledItems.map(l => ({
  2331. orderLineId: String(l.orderLineId),
  2332. quantity: l.unfulfilled,
  2333. }));
  2334. }