order-modification.e2e-spec.ts 83 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196
  1. /* tslint:disable:no-non-null-assertion */
  2. import { omit } from '@vendure/common/lib/omit';
  3. import { pick } from '@vendure/common/lib/pick';
  4. import { summate } from '@vendure/common/lib/shared-utils';
  5. import {
  6. defaultShippingCalculator,
  7. defaultShippingEligibilityChecker,
  8. freeShipping,
  9. manualFulfillmentHandler,
  10. mergeConfig,
  11. orderFixedDiscount,
  12. orderPercentageDiscount,
  13. productsPercentageDiscount,
  14. ShippingCalculator,
  15. } from '@vendure/core';
  16. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  17. import gql from 'graphql-tag';
  18. import path from 'path';
  19. import { initialData } from '../../../e2e-common/e2e-initial-data';
  20. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  21. import {
  22. failsToSettlePaymentMethod,
  23. testFailingPaymentMethod,
  24. testSuccessfulPaymentMethod,
  25. } from './fixtures/test-payment-methods';
  26. import {
  27. AddManualPayment,
  28. AdminTransition,
  29. CreateFulfillment,
  30. CreatePromotion,
  31. CreatePromotionMutation,
  32. CreatePromotionMutationVariables,
  33. CreateShippingMethod,
  34. ErrorCode,
  35. GetOrder,
  36. GetOrderHistory,
  37. GetOrderWithModifications,
  38. GetOrderWithModificationsQuery,
  39. GetOrderWithModificationsQueryVariables,
  40. GetStockMovement,
  41. GlobalFlag,
  42. HistoryEntryType,
  43. LanguageCode,
  44. ModifyOrder,
  45. ModifyOrderMutation,
  46. ModifyOrderMutationVariables,
  47. OrderFragment,
  48. OrderWithLinesFragment,
  49. OrderWithModificationsFragment,
  50. UpdateChannel,
  51. UpdateProductVariants,
  52. } from './graphql/generated-e2e-admin-types';
  53. import {
  54. AddItemToOrderMutationVariables,
  55. ApplyCouponCode,
  56. SetShippingAddress,
  57. SetShippingMethod,
  58. TestOrderWithPaymentsFragment,
  59. TransitionToState,
  60. UpdatedOrderFragment,
  61. } from './graphql/generated-e2e-shop-types';
  62. import {
  63. ADMIN_TRANSITION_TO_STATE,
  64. CREATE_FULFILLMENT,
  65. CREATE_PROMOTION,
  66. CREATE_SHIPPING_METHOD,
  67. GET_ORDER,
  68. GET_ORDER_HISTORY,
  69. GET_STOCK_MOVEMENT,
  70. UPDATE_CHANNEL,
  71. UPDATE_PRODUCT_VARIANTS,
  72. } from './graphql/shared-definitions';
  73. import {
  74. APPLY_COUPON_CODE,
  75. SET_SHIPPING_ADDRESS,
  76. SET_SHIPPING_METHOD,
  77. TRANSITION_TO_STATE,
  78. } from './graphql/shop-definitions';
  79. import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
  80. const SHIPPING_GB = 500;
  81. const SHIPPING_US = 1000;
  82. const SHIPPING_OTHER = 750;
  83. const testCalculator = new ShippingCalculator({
  84. code: 'test-calculator',
  85. description: [{ languageCode: LanguageCode.en, value: 'Has metadata' }],
  86. args: {},
  87. calculate: (ctx, order, args) => {
  88. let price;
  89. switch (order.shippingAddress.countryCode) {
  90. case 'GB':
  91. price = SHIPPING_GB;
  92. break;
  93. case 'US':
  94. price = SHIPPING_US;
  95. break;
  96. default:
  97. price = SHIPPING_OTHER;
  98. }
  99. return {
  100. price,
  101. priceIncludesTax: true,
  102. taxRate: 20,
  103. };
  104. },
  105. });
  106. describe('Order modification', () => {
  107. const { server, adminClient, shopClient } = createTestEnvironment(
  108. mergeConfig(testConfig(), {
  109. paymentOptions: {
  110. paymentMethodHandlers: [
  111. testSuccessfulPaymentMethod,
  112. failsToSettlePaymentMethod,
  113. testFailingPaymentMethod,
  114. ],
  115. },
  116. shippingOptions: {
  117. shippingCalculators: [defaultShippingCalculator, testCalculator],
  118. },
  119. customFields: {
  120. Order: [{ name: 'points', type: 'int', defaultValue: 0 }],
  121. OrderLine: [{ name: 'color', type: 'string', nullable: true }],
  122. },
  123. }),
  124. );
  125. let orderId: string;
  126. let testShippingMethodId: string;
  127. const orderGuard: ErrorResultGuard<
  128. UpdatedOrderFragment | OrderWithModificationsFragment | OrderFragment
  129. > = createErrorResultGuard(input => !!input.id);
  130. beforeAll(async () => {
  131. await server.init({
  132. initialData: {
  133. ...initialData,
  134. paymentMethods: [
  135. {
  136. name: testSuccessfulPaymentMethod.code,
  137. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  138. },
  139. {
  140. name: failsToSettlePaymentMethod.code,
  141. handler: { code: failsToSettlePaymentMethod.code, arguments: [] },
  142. },
  143. {
  144. name: testFailingPaymentMethod.code,
  145. handler: { code: testFailingPaymentMethod.code, arguments: [] },
  146. },
  147. ],
  148. },
  149. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  150. customerCount: 2,
  151. });
  152. await adminClient.asSuperAdmin();
  153. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  154. UPDATE_PRODUCT_VARIANTS,
  155. {
  156. input: [
  157. {
  158. id: 'T_1',
  159. trackInventory: GlobalFlag.TRUE,
  160. },
  161. {
  162. id: 'T_2',
  163. trackInventory: GlobalFlag.TRUE,
  164. },
  165. {
  166. id: 'T_3',
  167. trackInventory: GlobalFlag.TRUE,
  168. },
  169. ],
  170. },
  171. );
  172. const { createShippingMethod } = await adminClient.query<
  173. CreateShippingMethod.Mutation,
  174. CreateShippingMethod.Variables
  175. >(CREATE_SHIPPING_METHOD, {
  176. input: {
  177. code: 'new-method',
  178. fulfillmentHandler: manualFulfillmentHandler.code,
  179. checker: {
  180. code: defaultShippingEligibilityChecker.code,
  181. arguments: [
  182. {
  183. name: 'orderMinimum',
  184. value: '0',
  185. },
  186. ],
  187. },
  188. calculator: {
  189. code: testCalculator.code,
  190. arguments: [],
  191. },
  192. translations: [{ languageCode: LanguageCode.en, name: 'test method', description: '' }],
  193. },
  194. });
  195. testShippingMethodId = createShippingMethod.id;
  196. // create an order and check out
  197. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  198. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  199. productVariantId: 'T_1',
  200. quantity: 1,
  201. customFields: {
  202. color: 'green',
  203. },
  204. } as any);
  205. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  206. productVariantId: 'T_4',
  207. quantity: 2,
  208. });
  209. await proceedToArrangingPayment(shopClient);
  210. const result = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  211. orderGuard.assertSuccess(result);
  212. orderId = result.id;
  213. }, TEST_SETUP_TIMEOUT_MS);
  214. afterAll(async () => {
  215. await server.destroy();
  216. });
  217. it('modifyOrder returns error result when not in Modifying state', async () => {
  218. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  219. id: orderId,
  220. });
  221. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  222. MODIFY_ORDER,
  223. {
  224. input: {
  225. dryRun: false,
  226. orderId,
  227. adjustOrderLines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 3 })),
  228. },
  229. },
  230. );
  231. orderGuard.assertErrorResult(modifyOrder);
  232. expect(modifyOrder.errorCode).toBe(ErrorCode.ORDER_MODIFICATION_STATE_ERROR);
  233. });
  234. it('transition to Modifying state', async () => {
  235. const transitionOrderToState = await adminTransitionOrderToState(orderId, 'Modifying');
  236. orderGuard.assertSuccess(transitionOrderToState);
  237. expect(transitionOrderToState.state).toBe('Modifying');
  238. });
  239. describe('error cases', () => {
  240. it('no changes specified error', async () => {
  241. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  242. MODIFY_ORDER,
  243. {
  244. input: {
  245. dryRun: false,
  246. orderId,
  247. },
  248. },
  249. );
  250. orderGuard.assertErrorResult(modifyOrder);
  251. expect(modifyOrder.errorCode).toBe(ErrorCode.NO_CHANGES_SPECIFIED_ERROR);
  252. });
  253. it('no refund paymentId specified', async () => {
  254. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  255. id: orderId,
  256. });
  257. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  258. MODIFY_ORDER,
  259. {
  260. input: {
  261. dryRun: false,
  262. orderId,
  263. surcharges: [{ price: -500, priceIncludesTax: true, description: 'Discount' }],
  264. },
  265. },
  266. );
  267. orderGuard.assertErrorResult(modifyOrder);
  268. expect(modifyOrder.errorCode).toBe(ErrorCode.REFUND_PAYMENT_ID_MISSING_ERROR);
  269. await assertOrderIsUnchanged(order!);
  270. });
  271. it('addItems negative quantity', async () => {
  272. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  273. id: orderId,
  274. });
  275. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  276. MODIFY_ORDER,
  277. {
  278. input: {
  279. dryRun: false,
  280. orderId,
  281. addItems: [{ productVariantId: 'T_3', quantity: -1 }],
  282. },
  283. },
  284. );
  285. orderGuard.assertErrorResult(modifyOrder);
  286. expect(modifyOrder.errorCode).toBe(ErrorCode.NEGATIVE_QUANTITY_ERROR);
  287. await assertOrderIsUnchanged(order!);
  288. });
  289. it('adjustOrderLines negative quantity', async () => {
  290. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  291. id: orderId,
  292. });
  293. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  294. MODIFY_ORDER,
  295. {
  296. input: {
  297. dryRun: false,
  298. orderId,
  299. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: -1 }],
  300. },
  301. },
  302. );
  303. orderGuard.assertErrorResult(modifyOrder);
  304. expect(modifyOrder.errorCode).toBe(ErrorCode.NEGATIVE_QUANTITY_ERROR);
  305. await assertOrderIsUnchanged(order!);
  306. });
  307. it('addItems insufficient stock', async () => {
  308. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  309. id: orderId,
  310. });
  311. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  312. MODIFY_ORDER,
  313. {
  314. input: {
  315. dryRun: false,
  316. orderId,
  317. addItems: [{ productVariantId: 'T_3', quantity: 500 }],
  318. },
  319. },
  320. );
  321. orderGuard.assertErrorResult(modifyOrder);
  322. expect(modifyOrder.errorCode).toBe(ErrorCode.INSUFFICIENT_STOCK_ERROR);
  323. await assertOrderIsUnchanged(order!);
  324. });
  325. it('adjustOrderLines insufficient stock', async () => {
  326. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  327. id: orderId,
  328. });
  329. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  330. MODIFY_ORDER,
  331. {
  332. input: {
  333. dryRun: false,
  334. orderId,
  335. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 500 }],
  336. },
  337. },
  338. );
  339. orderGuard.assertErrorResult(modifyOrder);
  340. expect(modifyOrder.errorCode).toBe(ErrorCode.INSUFFICIENT_STOCK_ERROR);
  341. await assertOrderIsUnchanged(order!);
  342. });
  343. it('addItems order limit', async () => {
  344. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  345. id: orderId,
  346. });
  347. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  348. MODIFY_ORDER,
  349. {
  350. input: {
  351. dryRun: false,
  352. orderId,
  353. addItems: [{ productVariantId: 'T_4', quantity: 9999 }],
  354. },
  355. },
  356. );
  357. orderGuard.assertErrorResult(modifyOrder);
  358. expect(modifyOrder.errorCode).toBe(ErrorCode.ORDER_LIMIT_ERROR);
  359. await assertOrderIsUnchanged(order!);
  360. });
  361. it('adjustOrderLines order limit', async () => {
  362. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  363. id: orderId,
  364. });
  365. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  366. MODIFY_ORDER,
  367. {
  368. input: {
  369. dryRun: false,
  370. orderId,
  371. adjustOrderLines: [{ orderLineId: order!.lines[1].id, quantity: 9999 }],
  372. },
  373. },
  374. );
  375. orderGuard.assertErrorResult(modifyOrder);
  376. expect(modifyOrder.errorCode).toBe(ErrorCode.ORDER_LIMIT_ERROR);
  377. await assertOrderIsUnchanged(order!);
  378. });
  379. });
  380. describe('dry run', () => {
  381. it('addItems', async () => {
  382. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  383. id: orderId,
  384. });
  385. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  386. MODIFY_ORDER,
  387. {
  388. input: {
  389. dryRun: true,
  390. orderId,
  391. addItems: [{ productVariantId: 'T_5', quantity: 1 }],
  392. },
  393. },
  394. );
  395. orderGuard.assertSuccess(modifyOrder);
  396. const expectedTotal = order!.totalWithTax + Math.round(14374 * 1.2); // price of variant T_5
  397. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  398. expect(modifyOrder.lines.length).toBe(order!.lines.length + 1);
  399. await assertOrderIsUnchanged(order!);
  400. });
  401. it('addItems with existing variant id increments existing OrderLine', async () => {
  402. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  403. id: orderId,
  404. });
  405. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  406. MODIFY_ORDER,
  407. {
  408. input: {
  409. dryRun: true,
  410. orderId,
  411. addItems: [
  412. { productVariantId: 'T_1', quantity: 1, customFields: { color: 'green' } } as any,
  413. ],
  414. },
  415. },
  416. );
  417. orderGuard.assertSuccess(modifyOrder);
  418. const lineT1 = modifyOrder.lines.find(l => l.productVariant.id === 'T_1');
  419. expect(modifyOrder.lines.length).toBe(2);
  420. expect(lineT1?.quantity).toBe(2);
  421. await assertOrderIsUnchanged(order!);
  422. });
  423. it('addItems with existing variant id but different customFields adds new OrderLine', async () => {
  424. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  425. id: orderId,
  426. });
  427. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  428. MODIFY_ORDER,
  429. {
  430. input: {
  431. dryRun: true,
  432. orderId,
  433. addItems: [
  434. { productVariantId: 'T_1', quantity: 1, customFields: { color: 'blue' } } as any,
  435. ],
  436. },
  437. },
  438. );
  439. orderGuard.assertSuccess(modifyOrder);
  440. const lineT1 = modifyOrder.lines.find(l => l.productVariant.id === 'T_1');
  441. expect(modifyOrder.lines.length).toBe(3);
  442. expect(
  443. modifyOrder.lines.map(l => ({ variantId: l.productVariant.id, quantity: l.quantity })),
  444. ).toEqual([
  445. { variantId: 'T_1', quantity: 1 },
  446. { variantId: 'T_4', quantity: 2 },
  447. { variantId: 'T_1', quantity: 1 },
  448. ]);
  449. await assertOrderIsUnchanged(order!);
  450. });
  451. it('adjustOrderLines up', async () => {
  452. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  453. id: orderId,
  454. });
  455. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  456. MODIFY_ORDER,
  457. {
  458. input: {
  459. dryRun: true,
  460. orderId,
  461. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 3 }],
  462. },
  463. },
  464. );
  465. orderGuard.assertSuccess(modifyOrder);
  466. const expectedTotal = order!.totalWithTax + order!.lines[0].unitPriceWithTax * 2;
  467. expect(modifyOrder.lines[0].items.length).toBe(3);
  468. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  469. await assertOrderIsUnchanged(order!);
  470. });
  471. it('adjustOrderLines down', async () => {
  472. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  473. id: orderId,
  474. });
  475. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  476. MODIFY_ORDER,
  477. {
  478. input: {
  479. dryRun: true,
  480. orderId,
  481. adjustOrderLines: [{ orderLineId: order!.lines[1].id, quantity: 1 }],
  482. },
  483. },
  484. );
  485. orderGuard.assertSuccess(modifyOrder);
  486. const expectedTotal = order!.totalWithTax - order!.lines[1].unitPriceWithTax;
  487. expect(modifyOrder.lines[1].items.filter(i => i.cancelled).length).toBe(1);
  488. expect(modifyOrder.lines[1].items.filter(i => !i.cancelled).length).toBe(1);
  489. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  490. await assertOrderIsUnchanged(order!);
  491. });
  492. it('adjustOrderLines to zero', async () => {
  493. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  494. id: orderId,
  495. });
  496. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  497. MODIFY_ORDER,
  498. {
  499. input: {
  500. dryRun: true,
  501. orderId,
  502. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 0 }],
  503. },
  504. },
  505. );
  506. orderGuard.assertSuccess(modifyOrder);
  507. const expectedTotal = order!.totalWithTax - order!.lines[0].linePriceWithTax;
  508. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  509. expect(modifyOrder.lines[0].items.every(i => i.cancelled)).toBe(true);
  510. await assertOrderIsUnchanged(order!);
  511. });
  512. it('surcharge positive', async () => {
  513. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  514. id: orderId,
  515. });
  516. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  517. MODIFY_ORDER,
  518. {
  519. input: {
  520. dryRun: true,
  521. orderId,
  522. surcharges: [
  523. {
  524. description: 'extra fee',
  525. sku: '123',
  526. price: 300,
  527. priceIncludesTax: true,
  528. taxRate: 20,
  529. taxDescription: 'VAT',
  530. },
  531. ],
  532. },
  533. },
  534. );
  535. orderGuard.assertSuccess(modifyOrder);
  536. const expectedTotal = order!.totalWithTax + 300;
  537. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  538. expect(modifyOrder.surcharges.map(s => omit(s, ['id']))).toEqual([
  539. {
  540. description: 'extra fee',
  541. sku: '123',
  542. price: 250,
  543. priceWithTax: 300,
  544. taxRate: 20,
  545. },
  546. ]);
  547. await assertOrderIsUnchanged(order!);
  548. });
  549. it('surcharge negative', async () => {
  550. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  551. id: orderId,
  552. });
  553. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  554. MODIFY_ORDER,
  555. {
  556. input: {
  557. dryRun: true,
  558. orderId,
  559. surcharges: [
  560. {
  561. description: 'special discount',
  562. sku: '123',
  563. price: -300,
  564. priceIncludesTax: true,
  565. taxRate: 20,
  566. taxDescription: 'VAT',
  567. },
  568. ],
  569. },
  570. },
  571. );
  572. orderGuard.assertSuccess(modifyOrder);
  573. const expectedTotal = order!.totalWithTax + -300;
  574. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  575. expect(modifyOrder.surcharges.map(s => omit(s, ['id']))).toEqual([
  576. {
  577. description: 'special discount',
  578. sku: '123',
  579. price: -250,
  580. priceWithTax: -300,
  581. taxRate: 20,
  582. },
  583. ]);
  584. await assertOrderIsUnchanged(order!);
  585. });
  586. it('does not add a history entry', async () => {
  587. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  588. id: orderId,
  589. });
  590. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  591. MODIFY_ORDER,
  592. {
  593. input: {
  594. dryRun: true,
  595. orderId,
  596. addItems: [{ productVariantId: 'T_5', quantity: 1 }],
  597. },
  598. },
  599. );
  600. orderGuard.assertSuccess(modifyOrder);
  601. const { order: history } = await adminClient.query<
  602. GetOrderHistory.Query,
  603. GetOrderHistory.Variables
  604. >(GET_ORDER_HISTORY, {
  605. id: orderId,
  606. options: { filter: { type: { eq: HistoryEntryType.ORDER_MODIFIED } } },
  607. });
  608. orderGuard.assertSuccess(history);
  609. expect(history.history.totalItems).toBe(0);
  610. });
  611. });
  612. describe('wet run', () => {
  613. async function assertModifiedOrderIsPersisted(order: OrderWithModificationsFragment) {
  614. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  615. id: order.id,
  616. });
  617. expect(order2!.totalWithTax).toBe(order!.totalWithTax);
  618. expect(order2!.lines.length).toBe(order!.lines.length);
  619. expect(order2!.surcharges.length).toBe(order!.surcharges.length);
  620. expect(order2!.payments!.length).toBe(order!.payments!.length);
  621. expect(order2!.payments!.map(p => pick(p, ['id', 'amount', 'method']))).toEqual(
  622. order!.payments!.map(p => pick(p, ['id', 'amount', 'method'])),
  623. );
  624. }
  625. it('addItems', async () => {
  626. const order = await createOrderAndTransitionToModifyingState([
  627. {
  628. productVariantId: 'T_1',
  629. quantity: 1,
  630. },
  631. ]);
  632. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  633. MODIFY_ORDER,
  634. {
  635. input: {
  636. dryRun: false,
  637. orderId: order.id,
  638. addItems: [{ productVariantId: 'T_5', quantity: 1 }],
  639. },
  640. },
  641. );
  642. orderGuard.assertSuccess(modifyOrder);
  643. const priceDelta = Math.round(14374 * 1.2); // price of variant T_5
  644. const expectedTotal = order!.totalWithTax + priceDelta;
  645. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  646. expect(modifyOrder.lines.length).toBe(order!.lines.length + 1);
  647. expect(modifyOrder.modifications.length).toBe(1);
  648. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  649. expect(modifyOrder.modifications[0].orderItems?.length).toBe(1);
  650. expect(modifyOrder.modifications[0].orderItems?.map(i => i.id)).toEqual([
  651. modifyOrder.lines[1].items[0].id,
  652. ]);
  653. await assertModifiedOrderIsPersisted(modifyOrder);
  654. });
  655. it('adjustOrderLines up', async () => {
  656. const order = await createOrderAndTransitionToModifyingState([
  657. {
  658. productVariantId: 'T_1',
  659. quantity: 1,
  660. },
  661. ]);
  662. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  663. MODIFY_ORDER,
  664. {
  665. input: {
  666. dryRun: false,
  667. orderId: order.id,
  668. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 2 }],
  669. },
  670. },
  671. );
  672. orderGuard.assertSuccess(modifyOrder);
  673. const priceDelta = order!.lines[0].unitPriceWithTax;
  674. const expectedTotal = order!.totalWithTax + priceDelta;
  675. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  676. expect(modifyOrder.lines[0].quantity).toBe(2);
  677. expect(modifyOrder.modifications.length).toBe(1);
  678. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  679. expect(modifyOrder.modifications[0].orderItems?.length).toBe(1);
  680. expect(
  681. modifyOrder.lines[0].items
  682. .map(i => i.id)
  683. .includes(modifyOrder.modifications?.[0].orderItems?.[0].id as string),
  684. ).toBe(true);
  685. await assertModifiedOrderIsPersisted(modifyOrder);
  686. });
  687. it('adjustOrderLines down', async () => {
  688. const order = await createOrderAndTransitionToModifyingState([
  689. {
  690. productVariantId: 'T_1',
  691. quantity: 2,
  692. },
  693. ]);
  694. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  695. MODIFY_ORDER,
  696. {
  697. input: {
  698. dryRun: false,
  699. orderId: order.id,
  700. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 1 }],
  701. refund: { paymentId: order!.payments![0].id },
  702. },
  703. },
  704. );
  705. orderGuard.assertSuccess(modifyOrder);
  706. const priceDelta = -order!.lines[0].unitPriceWithTax;
  707. const expectedTotal = order!.totalWithTax + priceDelta;
  708. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  709. expect(modifyOrder.lines[0].quantity).toBe(1);
  710. expect(modifyOrder.payments?.length).toBe(1);
  711. expect(modifyOrder.payments?.[0].refunds.length).toBe(1);
  712. expect(modifyOrder.payments?.[0].refunds[0]).toEqual({
  713. id: 'T_1',
  714. state: 'Pending',
  715. total: -priceDelta,
  716. paymentId: modifyOrder.payments?.[0].id,
  717. });
  718. expect(modifyOrder.modifications.length).toBe(1);
  719. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  720. expect(modifyOrder.modifications[0].surcharges).toEqual(modifyOrder.surcharges.map(pick(['id'])));
  721. expect(modifyOrder.modifications[0].orderItems?.length).toBe(1);
  722. expect(
  723. modifyOrder.lines[0].items
  724. .map(i => i.id)
  725. .includes(modifyOrder.modifications?.[0].orderItems?.[0].id as string),
  726. ).toBe(true);
  727. await assertModifiedOrderIsPersisted(modifyOrder);
  728. });
  729. it('adjustOrderLines with changed customField value', async () => {
  730. const order = await createOrderAndTransitionToModifyingState([
  731. {
  732. productVariantId: 'T_1',
  733. quantity: 1,
  734. customFields: {
  735. color: 'green',
  736. },
  737. },
  738. ]);
  739. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  740. MODIFY_ORDER,
  741. {
  742. input: {
  743. dryRun: false,
  744. orderId: order.id,
  745. adjustOrderLines: [
  746. {
  747. orderLineId: order!.lines[0].id,
  748. quantity: 1,
  749. customFields: { color: 'black' },
  750. } as any,
  751. ],
  752. },
  753. },
  754. );
  755. orderGuard.assertSuccess(modifyOrder);
  756. expect(modifyOrder.lines.length).toBe(1);
  757. const { order: orderWithLines } = await adminClient.query(gql(GET_ORDER_WITH_CUSTOM_FIELDS), {
  758. id: order.id,
  759. });
  760. expect(orderWithLines.lines[0]).toEqual({
  761. id: order!.lines[0].id,
  762. customFields: { color: 'black' },
  763. });
  764. });
  765. it('adjustOrderLines handles quantity correctly', async () => {
  766. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  767. UPDATE_PRODUCT_VARIANTS,
  768. {
  769. input: [
  770. {
  771. id: 'T_6',
  772. stockOnHand: 1,
  773. trackInventory: GlobalFlag.TRUE,
  774. },
  775. ],
  776. },
  777. );
  778. const order = await createOrderAndTransitionToModifyingState([
  779. {
  780. productVariantId: 'T_6',
  781. quantity: 1,
  782. },
  783. ]);
  784. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  785. MODIFY_ORDER,
  786. {
  787. input: {
  788. dryRun: false,
  789. orderId: order.id,
  790. adjustOrderLines: [
  791. {
  792. orderLineId: order.lines[0].id,
  793. quantity: 1,
  794. },
  795. ],
  796. updateShippingAddress: {
  797. fullName: 'Jim',
  798. },
  799. },
  800. },
  801. );
  802. orderGuard.assertSuccess(modifyOrder);
  803. });
  804. it('surcharge positive', async () => {
  805. const order = await createOrderAndTransitionToModifyingState([
  806. {
  807. productVariantId: 'T_1',
  808. quantity: 1,
  809. },
  810. ]);
  811. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  812. MODIFY_ORDER,
  813. {
  814. input: {
  815. dryRun: false,
  816. orderId: order.id,
  817. surcharges: [
  818. {
  819. description: 'extra fee',
  820. sku: '123',
  821. price: 300,
  822. priceIncludesTax: true,
  823. taxRate: 20,
  824. taxDescription: 'VAT',
  825. },
  826. ],
  827. },
  828. },
  829. );
  830. orderGuard.assertSuccess(modifyOrder);
  831. const priceDelta = 300;
  832. const expectedTotal = order!.totalWithTax + priceDelta;
  833. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  834. expect(modifyOrder.surcharges.map(s => omit(s, ['id']))).toEqual([
  835. {
  836. description: 'extra fee',
  837. sku: '123',
  838. price: 250,
  839. priceWithTax: 300,
  840. taxRate: 20,
  841. },
  842. ]);
  843. expect(modifyOrder.modifications.length).toBe(1);
  844. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  845. expect(modifyOrder.modifications[0].surcharges).toEqual(modifyOrder.surcharges.map(pick(['id'])));
  846. await assertModifiedOrderIsPersisted(modifyOrder);
  847. });
  848. it('surcharge negative', async () => {
  849. const order = await createOrderAndTransitionToModifyingState([
  850. {
  851. productVariantId: 'T_1',
  852. quantity: 1,
  853. },
  854. ]);
  855. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  856. MODIFY_ORDER,
  857. {
  858. input: {
  859. dryRun: false,
  860. orderId: order!.id,
  861. surcharges: [
  862. {
  863. description: 'special discount',
  864. sku: '123',
  865. price: -300,
  866. priceIncludesTax: true,
  867. taxRate: 20,
  868. taxDescription: 'VAT',
  869. },
  870. ],
  871. refund: {
  872. paymentId: order!.payments![0].id,
  873. },
  874. },
  875. },
  876. );
  877. orderGuard.assertSuccess(modifyOrder);
  878. const expectedTotal = order!.totalWithTax + -300;
  879. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  880. expect(modifyOrder.surcharges.map(s => omit(s, ['id']))).toEqual([
  881. {
  882. description: 'special discount',
  883. sku: '123',
  884. price: -250,
  885. priceWithTax: -300,
  886. taxRate: 20,
  887. },
  888. ]);
  889. expect(modifyOrder.modifications.length).toBe(1);
  890. expect(modifyOrder.modifications[0].priceChange).toBe(-300);
  891. await assertModifiedOrderIsPersisted(modifyOrder);
  892. });
  893. it('update updateShippingAddress, recalculate shipping', async () => {
  894. const order = await createOrderAndTransitionToModifyingState([
  895. {
  896. productVariantId: 'T_1',
  897. quantity: 1,
  898. },
  899. ]);
  900. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  901. MODIFY_ORDER,
  902. {
  903. input: {
  904. dryRun: false,
  905. orderId: order!.id,
  906. updateShippingAddress: {
  907. countryCode: 'US',
  908. },
  909. options: {
  910. recalculateShipping: true,
  911. },
  912. },
  913. },
  914. );
  915. orderGuard.assertSuccess(modifyOrder);
  916. const priceDelta = SHIPPING_US - SHIPPING_OTHER;
  917. const expectedTotal = order!.totalWithTax + priceDelta;
  918. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  919. expect(modifyOrder.shippingAddress?.countryCode).toBe('US');
  920. expect(modifyOrder.modifications.length).toBe(1);
  921. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  922. await assertModifiedOrderIsPersisted(modifyOrder);
  923. });
  924. it('update updateShippingAddress, do not recalculate shipping', async () => {
  925. const order = await createOrderAndTransitionToModifyingState([
  926. {
  927. productVariantId: 'T_1',
  928. quantity: 1,
  929. },
  930. ]);
  931. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  932. MODIFY_ORDER,
  933. {
  934. input: {
  935. dryRun: false,
  936. orderId: order!.id,
  937. updateShippingAddress: {
  938. countryCode: 'US',
  939. },
  940. options: {
  941. recalculateShipping: false,
  942. },
  943. },
  944. },
  945. );
  946. orderGuard.assertSuccess(modifyOrder);
  947. const priceDelta = 0;
  948. const expectedTotal = order!.totalWithTax + priceDelta;
  949. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  950. expect(modifyOrder.shippingAddress?.countryCode).toBe('US');
  951. expect(modifyOrder.modifications.length).toBe(1);
  952. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  953. await assertModifiedOrderIsPersisted(modifyOrder);
  954. });
  955. it('update Order customFields', async () => {
  956. const order = await createOrderAndTransitionToModifyingState([
  957. {
  958. productVariantId: 'T_1',
  959. quantity: 1,
  960. },
  961. ]);
  962. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  963. MODIFY_ORDER,
  964. {
  965. input: {
  966. dryRun: false,
  967. orderId: order.id,
  968. customFields: {
  969. points: 42,
  970. },
  971. } as any,
  972. },
  973. );
  974. orderGuard.assertSuccess(modifyOrder);
  975. const { order: orderWithCustomFields } = await adminClient.query(
  976. gql(GET_ORDER_WITH_CUSTOM_FIELDS),
  977. { id: order.id },
  978. );
  979. expect(orderWithCustomFields.customFields).toEqual({
  980. points: 42,
  981. });
  982. });
  983. it('adds a history entry', async () => {
  984. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  985. id: orderId,
  986. });
  987. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  988. MODIFY_ORDER,
  989. {
  990. input: {
  991. dryRun: false,
  992. orderId: order!.id,
  993. addItems: [{ productVariantId: 'T_5', quantity: 1 }],
  994. },
  995. },
  996. );
  997. orderGuard.assertSuccess(modifyOrder);
  998. const { order: history } = await adminClient.query<
  999. GetOrderHistory.Query,
  1000. GetOrderHistory.Variables
  1001. >(GET_ORDER_HISTORY, {
  1002. id: orderId,
  1003. options: { filter: { type: { eq: HistoryEntryType.ORDER_MODIFIED } } },
  1004. });
  1005. orderGuard.assertSuccess(history);
  1006. expect(history.history.totalItems).toBe(1);
  1007. expect(history.history.items[0].data).toEqual({
  1008. modificationId: modifyOrder.modifications[0].id,
  1009. });
  1010. });
  1011. });
  1012. describe('additional payment handling', () => {
  1013. let orderId2: string;
  1014. beforeAll(async () => {
  1015. const order = await createOrderAndTransitionToModifyingState([
  1016. {
  1017. productVariantId: 'T_1',
  1018. quantity: 1,
  1019. },
  1020. ]);
  1021. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1022. MODIFY_ORDER,
  1023. {
  1024. input: {
  1025. dryRun: false,
  1026. orderId: order.id,
  1027. surcharges: [
  1028. {
  1029. description: 'extra fee',
  1030. sku: '123',
  1031. price: 300,
  1032. priceIncludesTax: true,
  1033. taxRate: 20,
  1034. taxDescription: 'VAT',
  1035. },
  1036. ],
  1037. },
  1038. },
  1039. );
  1040. orderGuard.assertSuccess(modifyOrder);
  1041. orderId2 = modifyOrder.id;
  1042. });
  1043. it('cannot transition back to original state if no payment is set', async () => {
  1044. const transitionOrderToState = await adminTransitionOrderToState(orderId2, 'PaymentSettled');
  1045. orderGuard.assertErrorResult(transitionOrderToState);
  1046. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  1047. expect(transitionOrderToState!.transitionError).toBe(
  1048. `Can only transition to the "ArrangingAdditionalPayment" state`,
  1049. );
  1050. });
  1051. it('can transition to ArrangingAdditionalPayment state', async () => {
  1052. const transitionOrderToState = await adminTransitionOrderToState(
  1053. orderId2,
  1054. 'ArrangingAdditionalPayment',
  1055. );
  1056. orderGuard.assertSuccess(transitionOrderToState);
  1057. expect(transitionOrderToState!.state).toBe('ArrangingAdditionalPayment');
  1058. });
  1059. it('cannot transition from ArrangingAdditionalPayment when total not covered by Payments', async () => {
  1060. const transitionOrderToState = await adminTransitionOrderToState(orderId2, 'PaymentSettled');
  1061. orderGuard.assertErrorResult(transitionOrderToState);
  1062. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  1063. expect(transitionOrderToState!.transitionError).toBe(
  1064. `Cannot transition away from "ArrangingAdditionalPayment" unless Order total is covered by Payments`,
  1065. );
  1066. });
  1067. it('addManualPaymentToOrder', async () => {
  1068. const { addManualPaymentToOrder } = await adminClient.query<
  1069. AddManualPayment.Mutation,
  1070. AddManualPayment.Variables
  1071. >(ADD_MANUAL_PAYMENT, {
  1072. input: {
  1073. orderId: orderId2,
  1074. method: 'test',
  1075. transactionId: 'ABC123',
  1076. metadata: {
  1077. foo: 'bar',
  1078. },
  1079. },
  1080. });
  1081. orderGuard.assertSuccess(addManualPaymentToOrder);
  1082. expect(addManualPaymentToOrder.payments?.length).toBe(2);
  1083. expect(omit(addManualPaymentToOrder.payments![1], ['id'])).toEqual({
  1084. transactionId: 'ABC123',
  1085. state: 'Settled',
  1086. amount: 300,
  1087. method: 'test',
  1088. metadata: {
  1089. foo: 'bar',
  1090. },
  1091. refunds: [],
  1092. });
  1093. expect(addManualPaymentToOrder.modifications[0].isSettled).toBe(true);
  1094. expect(addManualPaymentToOrder.modifications[0].payment?.id).toBe(
  1095. addManualPaymentToOrder.payments![1].id,
  1096. );
  1097. });
  1098. it('transition back to original state', async () => {
  1099. const transitionOrderToState = await adminTransitionOrderToState(orderId2, 'PaymentSettled');
  1100. orderGuard.assertSuccess(transitionOrderToState);
  1101. expect(transitionOrderToState.state).toBe('PaymentSettled');
  1102. });
  1103. });
  1104. describe('refund handling', () => {
  1105. let orderId3: string;
  1106. beforeAll(async () => {
  1107. const order = await createOrderAndTransitionToModifyingState([
  1108. {
  1109. productVariantId: 'T_1',
  1110. quantity: 1,
  1111. },
  1112. ]);
  1113. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1114. MODIFY_ORDER,
  1115. {
  1116. input: {
  1117. dryRun: false,
  1118. orderId: order.id,
  1119. surcharges: [
  1120. {
  1121. description: 'discount',
  1122. sku: '123',
  1123. price: -300,
  1124. priceIncludesTax: true,
  1125. taxRate: 20,
  1126. taxDescription: 'VAT',
  1127. },
  1128. ],
  1129. refund: {
  1130. paymentId: order.payments![0].id,
  1131. reason: 'discount',
  1132. },
  1133. },
  1134. },
  1135. );
  1136. orderGuard.assertSuccess(modifyOrder);
  1137. orderId3 = modifyOrder.id;
  1138. });
  1139. it('modification is settled', async () => {
  1140. const { order } = await adminClient.query<
  1141. GetOrderWithModifications.Query,
  1142. GetOrderWithModifications.Variables
  1143. >(GET_ORDER_WITH_MODIFICATIONS, { id: orderId3 });
  1144. expect(order?.modifications.length).toBe(1);
  1145. expect(order?.modifications[0].isSettled).toBe(true);
  1146. });
  1147. it('cannot transition to ArrangingAdditionalPayment state if no payment is needed', async () => {
  1148. const transitionOrderToState = await adminTransitionOrderToState(
  1149. orderId3,
  1150. 'ArrangingAdditionalPayment',
  1151. );
  1152. orderGuard.assertErrorResult(transitionOrderToState);
  1153. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  1154. expect(transitionOrderToState!.transitionError).toBe(
  1155. `Cannot transition Order to the \"ArrangingAdditionalPayment\" state as no additional payments are needed`,
  1156. );
  1157. });
  1158. it('can transition to original state', async () => {
  1159. const transitionOrderToState = await adminTransitionOrderToState(orderId3, 'PaymentSettled');
  1160. orderGuard.assertSuccess(transitionOrderToState);
  1161. expect(transitionOrderToState!.state).toBe('PaymentSettled');
  1162. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1163. id: orderId3,
  1164. });
  1165. expect(order?.payments![0].refunds.length).toBe(1);
  1166. expect(order?.payments![0].refunds[0].total).toBe(300);
  1167. expect(order?.payments![0].refunds[0].reason).toBe('discount');
  1168. });
  1169. });
  1170. // https://github.com/vendure-ecommerce/vendure/issues/688 - 4th point
  1171. it('correct additional payment when discounts applied', async () => {
  1172. await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
  1173. input: {
  1174. name: '$5 off',
  1175. couponCode: '5OFF',
  1176. enabled: true,
  1177. conditions: [],
  1178. actions: [
  1179. {
  1180. code: orderFixedDiscount.code,
  1181. arguments: [{ name: 'discount', value: '500' }],
  1182. },
  1183. ],
  1184. },
  1185. });
  1186. await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
  1187. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  1188. productVariantId: 'T_1',
  1189. quantity: 1,
  1190. } as any);
  1191. await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(APPLY_COUPON_CODE, {
  1192. couponCode: '5OFF',
  1193. });
  1194. await proceedToArrangingPayment(shopClient);
  1195. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1196. orderGuard.assertSuccess(order);
  1197. const originalTotalWithTax = order.totalWithTax;
  1198. const surcharge = 300;
  1199. const transitionOrderToState = await adminTransitionOrderToState(order.id, 'Modifying');
  1200. orderGuard.assertSuccess(transitionOrderToState);
  1201. expect(transitionOrderToState.state).toBe('Modifying');
  1202. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1203. MODIFY_ORDER,
  1204. {
  1205. input: {
  1206. dryRun: false,
  1207. orderId: order.id,
  1208. surcharges: [
  1209. {
  1210. description: 'extra fee',
  1211. sku: '123',
  1212. price: surcharge,
  1213. priceIncludesTax: true,
  1214. taxRate: 20,
  1215. taxDescription: 'VAT',
  1216. },
  1217. ],
  1218. },
  1219. },
  1220. );
  1221. orderGuard.assertSuccess(modifyOrder);
  1222. expect(modifyOrder.totalWithTax).toBe(originalTotalWithTax + surcharge);
  1223. });
  1224. // https://github.com/vendure-ecommerce/vendure/issues/872
  1225. describe('correct price calculations when prices include tax', () => {
  1226. async function modifyOrderLineQuantity(order: TestOrderWithPaymentsFragment) {
  1227. const transitionOrderToState = await adminTransitionOrderToState(order.id, 'Modifying');
  1228. orderGuard.assertSuccess(transitionOrderToState);
  1229. expect(transitionOrderToState.state).toBe('Modifying');
  1230. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1231. MODIFY_ORDER,
  1232. {
  1233. input: {
  1234. dryRun: true,
  1235. orderId: order.id,
  1236. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 2 }],
  1237. },
  1238. },
  1239. );
  1240. orderGuard.assertSuccess(modifyOrder);
  1241. return modifyOrder;
  1242. }
  1243. beforeAll(async () => {
  1244. await adminClient.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
  1245. input: {
  1246. id: 'T_1',
  1247. pricesIncludeTax: true,
  1248. },
  1249. });
  1250. });
  1251. it('without promotion', async () => {
  1252. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  1253. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  1254. productVariantId: 'T_1',
  1255. quantity: 1,
  1256. } as any);
  1257. await proceedToArrangingPayment(shopClient);
  1258. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1259. orderGuard.assertSuccess(order);
  1260. const modifyOrder = await modifyOrderLineQuantity(order);
  1261. expect(modifyOrder.lines[0].linePriceWithTax).toBe(order.lines[0].linePriceWithTax * 2);
  1262. });
  1263. it('with promotion', async () => {
  1264. await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
  1265. input: {
  1266. name: 'half price',
  1267. couponCode: 'HALF',
  1268. enabled: true,
  1269. conditions: [],
  1270. actions: [
  1271. {
  1272. code: productsPercentageDiscount.code,
  1273. arguments: [
  1274. { name: 'discount', value: '50' },
  1275. { name: 'productVariantIds', value: JSON.stringify(['T_1']) },
  1276. ],
  1277. },
  1278. ],
  1279. },
  1280. });
  1281. await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
  1282. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  1283. productVariantId: 'T_1',
  1284. quantity: 1,
  1285. } as any);
  1286. await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(APPLY_COUPON_CODE, {
  1287. couponCode: 'HALF',
  1288. });
  1289. await proceedToArrangingPayment(shopClient);
  1290. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1291. orderGuard.assertSuccess(order);
  1292. const modifyOrder = await modifyOrderLineQuantity(order);
  1293. expect(modifyOrder.lines[0].discountedLinePriceWithTax).toBe(
  1294. modifyOrder.lines[0].linePriceWithTax / 2,
  1295. );
  1296. expect(modifyOrder.lines[0].linePriceWithTax).toBe(order.lines[0].linePriceWithTax * 2);
  1297. });
  1298. });
  1299. // https://github.com/vendure-ecommerce/vendure/issues/890
  1300. describe('refund handling when promotions are active on order', () => {
  1301. it('refunds correct amount when order-level promotion applied', async () => {
  1302. await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
  1303. input: {
  1304. name: '$5 off',
  1305. couponCode: '5OFF2',
  1306. enabled: true,
  1307. conditions: [],
  1308. actions: [
  1309. {
  1310. code: orderFixedDiscount.code,
  1311. arguments: [{ name: 'discount', value: '500' }],
  1312. },
  1313. ],
  1314. },
  1315. });
  1316. await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
  1317. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  1318. productVariantId: 'T_1',
  1319. quantity: 2,
  1320. } as any);
  1321. await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(APPLY_COUPON_CODE, {
  1322. couponCode: '5OFF2',
  1323. });
  1324. await proceedToArrangingPayment(shopClient);
  1325. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1326. orderGuard.assertSuccess(order);
  1327. const originalTotalWithTax = order.totalWithTax;
  1328. const transitionOrderToState = await adminTransitionOrderToState(order.id, 'Modifying');
  1329. orderGuard.assertSuccess(transitionOrderToState);
  1330. expect(transitionOrderToState.state).toBe('Modifying');
  1331. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1332. MODIFY_ORDER,
  1333. {
  1334. input: {
  1335. dryRun: false,
  1336. orderId: order.id,
  1337. adjustOrderLines: [{ orderLineId: order.lines[0].id, quantity: 1 }],
  1338. refund: {
  1339. paymentId: order.payments![0].id,
  1340. reason: 'requested',
  1341. },
  1342. },
  1343. },
  1344. );
  1345. orderGuard.assertSuccess(modifyOrder);
  1346. expect(modifyOrder.totalWithTax).toBe(
  1347. originalTotalWithTax - order.lines[0].proratedUnitPriceWithTax,
  1348. );
  1349. expect(modifyOrder.payments![0].refunds![0].total).toBe(order.lines[0].proratedUnitPriceWithTax);
  1350. expect(modifyOrder.totalWithTax).toBe(getOrderPaymentsTotalWithRefunds(modifyOrder));
  1351. });
  1352. });
  1353. // https://github.com/vendure-ecommerce/vendure/issues/1197
  1354. describe('refund on shipping when change made to shippingAddress', () => {
  1355. let order: OrderWithModificationsFragment;
  1356. beforeAll(async () => {
  1357. const createdOrder = await createOrderAndTransitionToModifyingState([
  1358. {
  1359. productVariantId: 'T_1',
  1360. quantity: 1,
  1361. },
  1362. ]);
  1363. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1364. MODIFY_ORDER,
  1365. {
  1366. input: {
  1367. dryRun: false,
  1368. orderId: createdOrder.id,
  1369. updateShippingAddress: {
  1370. countryCode: 'GB',
  1371. },
  1372. refund: {
  1373. paymentId: createdOrder.payments![0].id,
  1374. reason: 'discount',
  1375. },
  1376. },
  1377. },
  1378. );
  1379. orderGuard.assertSuccess(modifyOrder);
  1380. order = modifyOrder;
  1381. });
  1382. it('creates a Refund with the correct amount', async () => {
  1383. expect(order.payments?.[0].refunds[0].total).toBe(SHIPPING_OTHER - SHIPPING_GB);
  1384. });
  1385. it('allows transition to PaymentSettled', async () => {
  1386. const transitionOrderToState = await adminTransitionOrderToState(order.id, 'PaymentSettled');
  1387. orderGuard.assertSuccess(transitionOrderToState);
  1388. expect(transitionOrderToState.state).toBe('PaymentSettled');
  1389. });
  1390. });
  1391. // https://github.com/vendure-ecommerce/vendure/issues/1210
  1392. describe('updating stock levels', () => {
  1393. async function getVariant(id: 'T_1' | 'T_2' | 'T_3') {
  1394. const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  1395. GET_STOCK_MOVEMENT,
  1396. {
  1397. id: 'T_1',
  1398. },
  1399. );
  1400. return product?.variants.find(v => v.id === id)!;
  1401. }
  1402. let orderId4: string;
  1403. let orderId5: string;
  1404. it('updates stock when increasing quantity before fulfillment', async () => {
  1405. const variant1 = await getVariant('T_2');
  1406. expect(variant1.stockOnHand).toBe(100);
  1407. expect(variant1.stockAllocated).toBe(0);
  1408. const order = await createOrderAndTransitionToModifyingState([
  1409. {
  1410. productVariantId: 'T_2',
  1411. quantity: 1,
  1412. },
  1413. ]);
  1414. orderId4 = order.id;
  1415. const variant2 = await getVariant('T_2');
  1416. expect(variant2.stockOnHand).toBe(100);
  1417. expect(variant2.stockAllocated).toBe(1);
  1418. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1419. MODIFY_ORDER,
  1420. {
  1421. input: {
  1422. dryRun: false,
  1423. orderId: order.id,
  1424. adjustOrderLines: [{ orderLineId: order.lines[0].id, quantity: 2 }],
  1425. },
  1426. },
  1427. );
  1428. orderGuard.assertSuccess(modifyOrder);
  1429. const variant3 = await getVariant('T_2');
  1430. expect(variant3.stockOnHand).toBe(100);
  1431. expect(variant3.stockAllocated).toBe(2);
  1432. });
  1433. it('updates stock when increasing quantity after fulfillment', async () => {
  1434. const result = await adminTransitionOrderToState(orderId4, 'ArrangingAdditionalPayment');
  1435. orderGuard.assertSuccess(result);
  1436. expect(result!.state).toBe('ArrangingAdditionalPayment');
  1437. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1438. id: orderId4,
  1439. });
  1440. const { addManualPaymentToOrder } = await adminClient.query<
  1441. AddManualPayment.Mutation,
  1442. AddManualPayment.Variables
  1443. >(ADD_MANUAL_PAYMENT, {
  1444. input: {
  1445. orderId: orderId4,
  1446. method: 'test',
  1447. transactionId: 'ABC123',
  1448. metadata: {
  1449. foo: 'bar',
  1450. },
  1451. },
  1452. });
  1453. orderGuard.assertSuccess(addManualPaymentToOrder);
  1454. await adminTransitionOrderToState(orderId4, 'PaymentSettled');
  1455. await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
  1456. CREATE_FULFILLMENT,
  1457. {
  1458. input: {
  1459. lines: order?.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })) ?? [],
  1460. handler: {
  1461. code: manualFulfillmentHandler.code,
  1462. arguments: [
  1463. { name: 'method', value: 'test method' },
  1464. { name: 'trackingCode', value: 'ABC123' },
  1465. ],
  1466. },
  1467. },
  1468. },
  1469. );
  1470. const variant1 = await getVariant('T_2');
  1471. expect(variant1.stockOnHand).toBe(98);
  1472. expect(variant1.stockAllocated).toBe(0);
  1473. await adminTransitionOrderToState(orderId4, 'Modifying');
  1474. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1475. MODIFY_ORDER,
  1476. {
  1477. input: {
  1478. dryRun: false,
  1479. orderId: order!.id,
  1480. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 3 }],
  1481. },
  1482. },
  1483. );
  1484. orderGuard.assertSuccess(modifyOrder);
  1485. const variant2 = await getVariant('T_2');
  1486. expect(variant2.stockOnHand).toBe(98);
  1487. expect(variant2.stockAllocated).toBe(1);
  1488. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1489. id: orderId4,
  1490. });
  1491. });
  1492. it('updates stock when adding item before fulfillment', async () => {
  1493. const variant1 = await getVariant('T_3');
  1494. expect(variant1.stockOnHand).toBe(100);
  1495. expect(variant1.stockAllocated).toBe(0);
  1496. const order = await createOrderAndTransitionToModifyingState([
  1497. {
  1498. productVariantId: 'T_2',
  1499. quantity: 1,
  1500. },
  1501. ]);
  1502. orderId5 = order.id;
  1503. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1504. MODIFY_ORDER,
  1505. {
  1506. input: {
  1507. dryRun: false,
  1508. orderId: order!.id,
  1509. addItems: [{ productVariantId: 'T_3', quantity: 1 }],
  1510. },
  1511. },
  1512. );
  1513. orderGuard.assertSuccess(modifyOrder);
  1514. const variant2 = await getVariant('T_3');
  1515. expect(variant2.stockOnHand).toBe(100);
  1516. expect(variant2.stockAllocated).toBe(1);
  1517. });
  1518. it('updates stock when removing item before fulfillment', async () => {
  1519. const variant1 = await getVariant('T_3');
  1520. expect(variant1.stockOnHand).toBe(100);
  1521. expect(variant1.stockAllocated).toBe(1);
  1522. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1523. id: orderId5,
  1524. });
  1525. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1526. MODIFY_ORDER,
  1527. {
  1528. input: {
  1529. dryRun: false,
  1530. orderId: orderId5,
  1531. adjustOrderLines: [
  1532. {
  1533. orderLineId: order!.lines.find(l => l.productVariant.id === 'T_3')!.id,
  1534. quantity: 0,
  1535. },
  1536. ],
  1537. refund: {
  1538. paymentId: order!.payments![0].id,
  1539. },
  1540. },
  1541. },
  1542. );
  1543. orderGuard.assertSuccess(modifyOrder);
  1544. const variant2 = await getVariant('T_3');
  1545. expect(variant2.stockOnHand).toBe(100);
  1546. expect(variant2.stockAllocated).toBe(0);
  1547. });
  1548. it('updates stock when removing item after fulfillment', async () => {
  1549. const variant1 = await getVariant('T_3');
  1550. expect(variant1.stockOnHand).toBe(100);
  1551. expect(variant1.stockAllocated).toBe(0);
  1552. const order = await createOrderAndCheckout([
  1553. {
  1554. productVariantId: 'T_3',
  1555. quantity: 1,
  1556. },
  1557. ]);
  1558. const { addFulfillmentToOrder } = await adminClient.query<
  1559. CreateFulfillment.Mutation,
  1560. CreateFulfillment.Variables
  1561. >(CREATE_FULFILLMENT, {
  1562. input: {
  1563. lines: order?.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })) ?? [],
  1564. handler: {
  1565. code: manualFulfillmentHandler.code,
  1566. arguments: [
  1567. { name: 'method', value: 'test method' },
  1568. { name: 'trackingCode', value: 'ABC123' },
  1569. ],
  1570. },
  1571. },
  1572. });
  1573. orderGuard.assertSuccess(addFulfillmentToOrder);
  1574. const variant2 = await getVariant('T_3');
  1575. expect(variant2.stockOnHand).toBe(99);
  1576. expect(variant2.stockAllocated).toBe(0);
  1577. await adminTransitionOrderToState(order.id, 'Modifying');
  1578. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1579. MODIFY_ORDER,
  1580. {
  1581. input: {
  1582. dryRun: false,
  1583. orderId: order.id,
  1584. adjustOrderLines: [
  1585. {
  1586. orderLineId: order!.lines.find(l => l.productVariant.id === 'T_3')!.id,
  1587. quantity: 0,
  1588. },
  1589. ],
  1590. refund: {
  1591. paymentId: order!.payments![0].id,
  1592. },
  1593. },
  1594. },
  1595. );
  1596. const variant3 = await getVariant('T_3');
  1597. expect(variant3.stockOnHand).toBe(100);
  1598. expect(variant3.stockAllocated).toBe(0);
  1599. });
  1600. });
  1601. describe('couponCode handling', () => {
  1602. const CODE_50PC_OFF = '50PC';
  1603. const CODE_FREE_SHIPPING = 'FREESHIP';
  1604. let order: TestOrderWithPaymentsFragment;
  1605. beforeAll(async () => {
  1606. await adminClient.query<CreatePromotionMutation, CreatePromotionMutationVariables>(
  1607. CREATE_PROMOTION,
  1608. {
  1609. input: {
  1610. name: '50% off',
  1611. couponCode: CODE_50PC_OFF,
  1612. enabled: true,
  1613. conditions: [],
  1614. actions: [
  1615. {
  1616. code: orderPercentageDiscount.code,
  1617. arguments: [{ name: 'discount', value: '50' }],
  1618. },
  1619. ],
  1620. },
  1621. },
  1622. );
  1623. await adminClient.query<CreatePromotionMutation, CreatePromotionMutationVariables>(
  1624. CREATE_PROMOTION,
  1625. {
  1626. input: {
  1627. name: 'Free shipping',
  1628. couponCode: CODE_FREE_SHIPPING,
  1629. enabled: true,
  1630. conditions: [],
  1631. actions: [{ code: freeShipping.code, arguments: [] }],
  1632. },
  1633. },
  1634. );
  1635. // create an order and check out
  1636. await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
  1637. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  1638. productVariantId: 'T_1',
  1639. quantity: 1,
  1640. customFields: {
  1641. color: 'green',
  1642. },
  1643. } as any);
  1644. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  1645. productVariantId: 'T_4',
  1646. quantity: 2,
  1647. });
  1648. await proceedToArrangingPayment(shopClient);
  1649. const result = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1650. orderGuard.assertSuccess(result);
  1651. order = result;
  1652. const result2 = await adminTransitionOrderToState(order.id, 'Modifying');
  1653. orderGuard.assertSuccess(result2);
  1654. expect(result2.state).toBe('Modifying');
  1655. });
  1656. it('invalid coupon code returns ErrorResult', async () => {
  1657. const { modifyOrder } = await adminClient.query<
  1658. ModifyOrderMutation,
  1659. ModifyOrderMutationVariables
  1660. >(MODIFY_ORDER, {
  1661. input: {
  1662. dryRun: false,
  1663. orderId: order.id,
  1664. couponCodes: ['BAD_CODE'],
  1665. },
  1666. });
  1667. orderGuard.assertErrorResult(modifyOrder);
  1668. expect(modifyOrder.message).toBe('Coupon code "BAD_CODE" is not valid');
  1669. });
  1670. it('valid coupon code applies Promotion', async () => {
  1671. const { modifyOrder } = await adminClient.query<
  1672. ModifyOrderMutation,
  1673. ModifyOrderMutationVariables
  1674. >(MODIFY_ORDER, {
  1675. input: {
  1676. dryRun: false,
  1677. orderId: order.id,
  1678. refund: {
  1679. paymentId: order.payments![0].id,
  1680. },
  1681. couponCodes: [CODE_50PC_OFF],
  1682. },
  1683. });
  1684. orderGuard.assertSuccess(modifyOrder);
  1685. expect(modifyOrder.subTotalWithTax).toBe(order.subTotalWithTax * 0.5);
  1686. });
  1687. it('adds order.discounts', async () => {
  1688. const { order: orderWithModifications } = await adminClient.query<
  1689. GetOrderWithModificationsQuery,
  1690. GetOrderWithModificationsQueryVariables
  1691. >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
  1692. expect(orderWithModifications?.discounts.length).toBe(1);
  1693. expect(orderWithModifications?.discounts[0].description).toBe('50% off');
  1694. });
  1695. it('adds order.promotions', async () => {
  1696. const { order: orderWithModifications } = await adminClient.query<
  1697. GetOrderWithModificationsQuery,
  1698. GetOrderWithModificationsQueryVariables
  1699. >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
  1700. expect(orderWithModifications?.promotions.length).toBe(1);
  1701. expect(orderWithModifications?.promotions[0].name).toBe('50% off');
  1702. });
  1703. it('creates correct refund amount', async () => {
  1704. const { order: orderWithModifications } = await adminClient.query<
  1705. GetOrderWithModificationsQuery,
  1706. GetOrderWithModificationsQueryVariables
  1707. >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
  1708. expect(orderWithModifications?.payments![0].refunds.length).toBe(1);
  1709. expect(orderWithModifications!.totalWithTax).toBe(
  1710. getOrderPaymentsTotalWithRefunds(orderWithModifications!),
  1711. );
  1712. expect(orderWithModifications?.payments![0].refunds[0].total).toBe(
  1713. order.totalWithTax - orderWithModifications!.totalWithTax,
  1714. );
  1715. });
  1716. it('creates history entry for applying couponCode', async () => {
  1717. const { order: history } = await adminClient.query<
  1718. GetOrderHistory.Query,
  1719. GetOrderHistory.Variables
  1720. >(GET_ORDER_HISTORY, {
  1721. id: order.id,
  1722. options: { filter: { type: { eq: HistoryEntryType.ORDER_COUPON_APPLIED } } },
  1723. });
  1724. orderGuard.assertSuccess(history);
  1725. expect(history.history.items.length).toBe(1);
  1726. expect(pick(history.history.items[0]!, ['type', 'data'])).toEqual({
  1727. type: HistoryEntryType.ORDER_COUPON_APPLIED,
  1728. data: { couponCode: CODE_50PC_OFF, promotionId: 'T_4' },
  1729. });
  1730. });
  1731. it('removes coupon code', async () => {
  1732. const { modifyOrder } = await adminClient.query<
  1733. ModifyOrderMutation,
  1734. ModifyOrderMutationVariables
  1735. >(MODIFY_ORDER, {
  1736. input: {
  1737. dryRun: false,
  1738. orderId: order.id,
  1739. couponCodes: [],
  1740. },
  1741. });
  1742. orderGuard.assertSuccess(modifyOrder);
  1743. expect(modifyOrder.subTotalWithTax).toBe(order.subTotalWithTax);
  1744. });
  1745. it('removes order.discounts', async () => {
  1746. const { order: orderWithModifications } = await adminClient.query<
  1747. GetOrderWithModificationsQuery,
  1748. GetOrderWithModificationsQueryVariables
  1749. >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
  1750. expect(orderWithModifications?.discounts.length).toBe(0);
  1751. });
  1752. it('removes order.promotions', async () => {
  1753. const { order: orderWithModifications } = await adminClient.query<
  1754. GetOrderWithModificationsQuery,
  1755. GetOrderWithModificationsQueryVariables
  1756. >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
  1757. expect(orderWithModifications?.promotions.length).toBe(0);
  1758. });
  1759. it('creates history entry for removing couponCode', async () => {
  1760. const { order: history } = await adminClient.query<
  1761. GetOrderHistory.Query,
  1762. GetOrderHistory.Variables
  1763. >(GET_ORDER_HISTORY, {
  1764. id: order.id,
  1765. options: { filter: { type: { eq: HistoryEntryType.ORDER_COUPON_REMOVED } } },
  1766. });
  1767. orderGuard.assertSuccess(history);
  1768. expect(history.history.items.length).toBe(1);
  1769. expect(pick(history.history.items[0]!, ['type', 'data'])).toEqual({
  1770. type: HistoryEntryType.ORDER_COUPON_REMOVED,
  1771. data: { couponCode: CODE_50PC_OFF },
  1772. });
  1773. });
  1774. it('correct refund for free shipping couponCode', async () => {
  1775. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  1776. productVariantId: 'T_1',
  1777. quantity: 1,
  1778. } as any);
  1779. await proceedToArrangingPayment(shopClient);
  1780. const result = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1781. orderGuard.assertSuccess(result);
  1782. const order2 = result;
  1783. const shippingWithTax = order2.shippingWithTax;
  1784. const result2 = await adminTransitionOrderToState(order2.id, 'Modifying');
  1785. orderGuard.assertSuccess(result2);
  1786. expect(result2.state).toBe('Modifying');
  1787. const { modifyOrder } = await adminClient.query<
  1788. ModifyOrderMutation,
  1789. ModifyOrderMutationVariables
  1790. >(MODIFY_ORDER, {
  1791. input: {
  1792. dryRun: false,
  1793. orderId: order2.id,
  1794. refund: {
  1795. paymentId: order2.payments![0].id,
  1796. },
  1797. couponCodes: [CODE_FREE_SHIPPING],
  1798. },
  1799. });
  1800. orderGuard.assertSuccess(modifyOrder);
  1801. expect(modifyOrder.shippingWithTax).toBe(0);
  1802. expect(modifyOrder!.totalWithTax).toBe(getOrderPaymentsTotalWithRefunds(modifyOrder!));
  1803. expect(modifyOrder.payments![0].refunds[0].total).toBe(shippingWithTax);
  1804. });
  1805. });
  1806. async function adminTransitionOrderToState(id: string, state: string) {
  1807. const result = await adminClient.query<AdminTransition.Mutation, AdminTransition.Variables>(
  1808. ADMIN_TRANSITION_TO_STATE,
  1809. {
  1810. id,
  1811. state,
  1812. },
  1813. );
  1814. return result.transitionOrderToState;
  1815. }
  1816. async function assertOrderIsUnchanged(order: OrderWithLinesFragment) {
  1817. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1818. id: order.id,
  1819. });
  1820. expect(order2!.totalWithTax).toBe(order!.totalWithTax);
  1821. expect(order2!.lines.length).toBe(order!.lines.length);
  1822. expect(order2!.surcharges.length).toBe(order!.surcharges.length);
  1823. expect(order2!.totalQuantity).toBe(order!.totalQuantity);
  1824. }
  1825. async function createOrderAndCheckout(
  1826. items: Array<AddItemToOrderMutationVariables & { customFields?: any }>,
  1827. ) {
  1828. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  1829. for (const itemInput of items) {
  1830. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), itemInput);
  1831. }
  1832. await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
  1833. SET_SHIPPING_ADDRESS,
  1834. {
  1835. input: {
  1836. fullName: 'name',
  1837. streetLine1: '12 the street',
  1838. city: 'foo',
  1839. postalCode: '123456',
  1840. countryCode: 'AT',
  1841. },
  1842. },
  1843. );
  1844. await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(SET_SHIPPING_METHOD, {
  1845. id: testShippingMethodId,
  1846. });
  1847. await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(TRANSITION_TO_STATE, {
  1848. state: 'ArrangingPayment',
  1849. });
  1850. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1851. orderGuard.assertSuccess(order);
  1852. return order;
  1853. }
  1854. async function createOrderAndTransitionToModifyingState(
  1855. items: Array<AddItemToOrderMutationVariables & { customFields?: any }>,
  1856. ): Promise<TestOrderWithPaymentsFragment> {
  1857. const order = await createOrderAndCheckout(items);
  1858. await adminTransitionOrderToState(order.id, 'Modifying');
  1859. return order;
  1860. }
  1861. function getOrderPaymentsTotalWithRefunds(_order: OrderWithModificationsFragment) {
  1862. return _order.payments?.reduce((sum, p) => sum + p.amount - summate(p?.refunds, 'total'), 0) ?? 0;
  1863. }
  1864. });
  1865. export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
  1866. fragment OrderWithModifications on Order {
  1867. id
  1868. state
  1869. subTotal
  1870. subTotalWithTax
  1871. shipping
  1872. shippingWithTax
  1873. total
  1874. totalWithTax
  1875. lines {
  1876. id
  1877. quantity
  1878. linePrice
  1879. linePriceWithTax
  1880. discountedLinePriceWithTax
  1881. proratedLinePriceWithTax
  1882. discounts {
  1883. description
  1884. amountWithTax
  1885. }
  1886. productVariant {
  1887. id
  1888. name
  1889. }
  1890. items {
  1891. id
  1892. createdAt
  1893. updatedAt
  1894. cancelled
  1895. unitPrice
  1896. }
  1897. }
  1898. surcharges {
  1899. id
  1900. description
  1901. sku
  1902. price
  1903. priceWithTax
  1904. taxRate
  1905. }
  1906. payments {
  1907. id
  1908. transactionId
  1909. state
  1910. amount
  1911. method
  1912. metadata
  1913. refunds {
  1914. id
  1915. state
  1916. total
  1917. paymentId
  1918. }
  1919. }
  1920. modifications {
  1921. id
  1922. note
  1923. priceChange
  1924. isSettled
  1925. orderItems {
  1926. id
  1927. }
  1928. surcharges {
  1929. id
  1930. }
  1931. payment {
  1932. id
  1933. state
  1934. amount
  1935. method
  1936. }
  1937. refund {
  1938. id
  1939. state
  1940. total
  1941. paymentId
  1942. }
  1943. }
  1944. promotions {
  1945. id
  1946. name
  1947. couponCode
  1948. }
  1949. discounts {
  1950. description
  1951. adjustmentSource
  1952. amount
  1953. amountWithTax
  1954. }
  1955. shippingAddress {
  1956. streetLine1
  1957. city
  1958. postalCode
  1959. province
  1960. countryCode
  1961. country
  1962. }
  1963. billingAddress {
  1964. streetLine1
  1965. city
  1966. postalCode
  1967. province
  1968. countryCode
  1969. country
  1970. }
  1971. }
  1972. `;
  1973. export const GET_ORDER_WITH_MODIFICATIONS = gql`
  1974. query GetOrderWithModifications($id: ID!) {
  1975. order(id: $id) {
  1976. ...OrderWithModifications
  1977. }
  1978. }
  1979. ${ORDER_WITH_MODIFICATION_FRAGMENT}
  1980. `;
  1981. export const MODIFY_ORDER = gql`
  1982. mutation ModifyOrder($input: ModifyOrderInput!) {
  1983. modifyOrder(input: $input) {
  1984. ...OrderWithModifications
  1985. ... on ErrorResult {
  1986. errorCode
  1987. message
  1988. }
  1989. }
  1990. }
  1991. ${ORDER_WITH_MODIFICATION_FRAGMENT}
  1992. `;
  1993. export const ADD_MANUAL_PAYMENT = gql`
  1994. mutation AddManualPayment($input: ManualPaymentInput!) {
  1995. addManualPaymentToOrder(input: $input) {
  1996. ...OrderWithModifications
  1997. ... on ErrorResult {
  1998. errorCode
  1999. message
  2000. }
  2001. }
  2002. }
  2003. ${ORDER_WITH_MODIFICATION_FRAGMENT}
  2004. `;
  2005. // Note, we don't use the gql tag around these due to the customFields which
  2006. // would cause a codegen error.
  2007. const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = `
  2008. mutation AddItemToOrder($productVariantId: ID!, $quantity: Int!, $customFields: OrderLineCustomFieldsInput) {
  2009. addItemToOrder(productVariantId: $productVariantId, quantity: $quantity, customFields: $customFields) {
  2010. ...on Order { id }
  2011. }
  2012. }
  2013. `;
  2014. const GET_ORDER_WITH_CUSTOM_FIELDS = `
  2015. query GetOrderCustomFields($id: ID!) {
  2016. order(id: $id) {
  2017. customFields { points }
  2018. lines { id, customFields { color } }
  2019. }
  2020. }
  2021. `;