order-modification.e2e-spec.ts 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547
  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 {
  5. defaultShippingCalculator,
  6. defaultShippingEligibilityChecker,
  7. mergeConfig,
  8. ShippingCalculator,
  9. } from '@vendure/core';
  10. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  11. import gql from 'graphql-tag';
  12. import path from 'path';
  13. import { initialData } from '../../../e2e-common/e2e-initial-data';
  14. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  15. import { manualFulfillmentHandler } from '../src/config/fulfillment/manual-fulfillment-handler';
  16. import { orderFixedDiscount } from '../src/config/promotion/actions/order-fixed-discount-action';
  17. import {
  18. failsToSettlePaymentMethod,
  19. testFailingPaymentMethod,
  20. testSuccessfulPaymentMethod,
  21. } from './fixtures/test-payment-methods';
  22. import {
  23. AddManualPayment,
  24. AdminTransition,
  25. CreatePromotion,
  26. CreateShippingMethod,
  27. ErrorCode,
  28. GetOrder,
  29. GetOrderHistory,
  30. GetOrderWithModifications,
  31. GlobalFlag,
  32. HistoryEntryType,
  33. LanguageCode,
  34. ModifyOrder,
  35. OrderFragment,
  36. OrderWithLinesFragment,
  37. OrderWithModificationsFragment,
  38. SettlePayment,
  39. UpdateProductVariants,
  40. } from './graphql/generated-e2e-admin-types';
  41. import {
  42. AddItemToOrderMutationVariables,
  43. ApplyCouponCode,
  44. SetShippingAddress,
  45. SetShippingMethod,
  46. TestOrderWithPaymentsFragment,
  47. TransitionToState,
  48. UpdatedOrderFragment,
  49. } from './graphql/generated-e2e-shop-types';
  50. import {
  51. ADMIN_TRANSITION_TO_STATE,
  52. CREATE_PROMOTION,
  53. CREATE_SHIPPING_METHOD,
  54. GET_ORDER,
  55. GET_ORDER_HISTORY,
  56. SETTLE_PAYMENT,
  57. UPDATE_PRODUCT_VARIANTS,
  58. } from './graphql/shared-definitions';
  59. import {
  60. APPLY_COUPON_CODE,
  61. SET_SHIPPING_ADDRESS,
  62. SET_SHIPPING_METHOD,
  63. TRANSITION_TO_STATE,
  64. } from './graphql/shop-definitions';
  65. import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
  66. const SHIPPING_GB = 500;
  67. const SHIPPING_US = 1000;
  68. const SHIPPING_OTHER = 750;
  69. const testCalculator = new ShippingCalculator({
  70. code: 'test-calculator',
  71. description: [{ languageCode: LanguageCode.en, value: 'Has metadata' }],
  72. args: {},
  73. calculate: (ctx, order, args) => {
  74. let price;
  75. switch (order.shippingAddress.countryCode) {
  76. case 'GB':
  77. price = SHIPPING_GB;
  78. break;
  79. case 'US':
  80. price = SHIPPING_US;
  81. break;
  82. default:
  83. price = SHIPPING_OTHER;
  84. }
  85. return {
  86. price,
  87. priceIncludesTax: true,
  88. taxRate: 20,
  89. };
  90. },
  91. });
  92. describe('Order modification', () => {
  93. const { server, adminClient, shopClient } = createTestEnvironment(
  94. mergeConfig(testConfig, {
  95. paymentOptions: {
  96. paymentMethodHandlers: [
  97. testSuccessfulPaymentMethod,
  98. failsToSettlePaymentMethod,
  99. testFailingPaymentMethod,
  100. ],
  101. },
  102. promotionOptions: {
  103. promotionConditions: [orderFixedDiscount],
  104. },
  105. shippingOptions: {
  106. shippingCalculators: [defaultShippingCalculator, testCalculator],
  107. },
  108. customFields: {
  109. Order: [{ name: 'points', type: 'int', defaultValue: 0 }],
  110. OrderLine: [{ name: 'color', type: 'string', nullable: true }],
  111. },
  112. }),
  113. );
  114. let orderId: string;
  115. let testShippingMethodId: string;
  116. const orderGuard: ErrorResultGuard<
  117. UpdatedOrderFragment | OrderWithModificationsFragment | OrderFragment
  118. > = createErrorResultGuard(input => !!input.id);
  119. beforeAll(async () => {
  120. await server.init({
  121. initialData: {
  122. ...initialData,
  123. paymentMethods: [
  124. {
  125. name: testSuccessfulPaymentMethod.code,
  126. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  127. },
  128. {
  129. name: failsToSettlePaymentMethod.code,
  130. handler: { code: failsToSettlePaymentMethod.code, arguments: [] },
  131. },
  132. {
  133. name: testFailingPaymentMethod.code,
  134. handler: { code: testFailingPaymentMethod.code, arguments: [] },
  135. },
  136. ],
  137. },
  138. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  139. customerCount: 2,
  140. });
  141. await adminClient.asSuperAdmin();
  142. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  143. UPDATE_PRODUCT_VARIANTS,
  144. {
  145. input: [
  146. {
  147. id: 'T_1',
  148. trackInventory: GlobalFlag.TRUE,
  149. },
  150. {
  151. id: 'T_2',
  152. trackInventory: GlobalFlag.TRUE,
  153. },
  154. {
  155. id: 'T_3',
  156. trackInventory: GlobalFlag.TRUE,
  157. },
  158. ],
  159. },
  160. );
  161. const { createShippingMethod } = await adminClient.query<
  162. CreateShippingMethod.Mutation,
  163. CreateShippingMethod.Variables
  164. >(CREATE_SHIPPING_METHOD, {
  165. input: {
  166. code: 'new-method',
  167. fulfillmentHandler: manualFulfillmentHandler.code,
  168. checker: {
  169. code: defaultShippingEligibilityChecker.code,
  170. arguments: [
  171. {
  172. name: 'orderMinimum',
  173. value: '0',
  174. },
  175. ],
  176. },
  177. calculator: {
  178. code: testCalculator.code,
  179. arguments: [],
  180. },
  181. translations: [{ languageCode: LanguageCode.en, name: 'test method', description: '' }],
  182. },
  183. });
  184. testShippingMethodId = createShippingMethod.id;
  185. // create an order and check out
  186. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  187. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  188. productVariantId: 'T_1',
  189. quantity: 1,
  190. customFields: {
  191. color: 'green',
  192. },
  193. } as any);
  194. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  195. productVariantId: 'T_4',
  196. quantity: 2,
  197. });
  198. await proceedToArrangingPayment(shopClient);
  199. const result = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  200. orderGuard.assertSuccess(result);
  201. orderId = result.id;
  202. }, TEST_SETUP_TIMEOUT_MS);
  203. afterAll(async () => {
  204. await server.destroy();
  205. });
  206. it('modifyOrder returns error result when not in Modifying state', async () => {
  207. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  208. id: orderId,
  209. });
  210. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  211. MODIFY_ORDER,
  212. {
  213. input: {
  214. dryRun: false,
  215. orderId,
  216. adjustOrderLines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 3 })),
  217. },
  218. },
  219. );
  220. orderGuard.assertErrorResult(modifyOrder);
  221. expect(modifyOrder.errorCode).toBe(ErrorCode.ORDER_MODIFICATION_STATE_ERROR);
  222. });
  223. it('transition to Modifying state', async () => {
  224. const { transitionOrderToState } = await adminClient.query<
  225. AdminTransition.Mutation,
  226. AdminTransition.Variables
  227. >(ADMIN_TRANSITION_TO_STATE, {
  228. id: orderId,
  229. state: 'Modifying',
  230. });
  231. orderGuard.assertSuccess(transitionOrderToState);
  232. expect(transitionOrderToState.state).toBe('Modifying');
  233. });
  234. describe('error cases', () => {
  235. it('no changes specified error', async () => {
  236. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  237. MODIFY_ORDER,
  238. {
  239. input: {
  240. dryRun: false,
  241. orderId,
  242. },
  243. },
  244. );
  245. orderGuard.assertErrorResult(modifyOrder);
  246. expect(modifyOrder.errorCode).toBe(ErrorCode.NO_CHANGES_SPECIFIED_ERROR);
  247. });
  248. it('no refund paymentId specified', async () => {
  249. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  250. id: orderId,
  251. });
  252. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  253. MODIFY_ORDER,
  254. {
  255. input: {
  256. dryRun: false,
  257. orderId,
  258. surcharges: [{ price: -500, priceIncludesTax: true, description: 'Discount' }],
  259. },
  260. },
  261. );
  262. orderGuard.assertErrorResult(modifyOrder);
  263. expect(modifyOrder.errorCode).toBe(ErrorCode.REFUND_PAYMENT_ID_MISSING_ERROR);
  264. await assertOrderIsUnchanged(order!);
  265. });
  266. it('addItems negative quantity', async () => {
  267. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  268. id: orderId,
  269. });
  270. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  271. MODIFY_ORDER,
  272. {
  273. input: {
  274. dryRun: false,
  275. orderId,
  276. addItems: [{ productVariantId: 'T_3', quantity: -1 }],
  277. },
  278. },
  279. );
  280. orderGuard.assertErrorResult(modifyOrder);
  281. expect(modifyOrder.errorCode).toBe(ErrorCode.NEGATIVE_QUANTITY_ERROR);
  282. await assertOrderIsUnchanged(order!);
  283. });
  284. it('adjustOrderLines negative quantity', async () => {
  285. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  286. id: orderId,
  287. });
  288. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  289. MODIFY_ORDER,
  290. {
  291. input: {
  292. dryRun: false,
  293. orderId,
  294. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: -1 }],
  295. },
  296. },
  297. );
  298. orderGuard.assertErrorResult(modifyOrder);
  299. expect(modifyOrder.errorCode).toBe(ErrorCode.NEGATIVE_QUANTITY_ERROR);
  300. await assertOrderIsUnchanged(order!);
  301. });
  302. it('addItems insufficient stock', async () => {
  303. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  304. id: orderId,
  305. });
  306. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  307. MODIFY_ORDER,
  308. {
  309. input: {
  310. dryRun: false,
  311. orderId,
  312. addItems: [{ productVariantId: 'T_3', quantity: 500 }],
  313. },
  314. },
  315. );
  316. orderGuard.assertErrorResult(modifyOrder);
  317. expect(modifyOrder.errorCode).toBe(ErrorCode.INSUFFICIENT_STOCK_ERROR);
  318. await assertOrderIsUnchanged(order!);
  319. });
  320. it('adjustOrderLines insufficient stock', async () => {
  321. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  322. id: orderId,
  323. });
  324. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  325. MODIFY_ORDER,
  326. {
  327. input: {
  328. dryRun: false,
  329. orderId,
  330. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 500 }],
  331. },
  332. },
  333. );
  334. orderGuard.assertErrorResult(modifyOrder);
  335. expect(modifyOrder.errorCode).toBe(ErrorCode.INSUFFICIENT_STOCK_ERROR);
  336. await assertOrderIsUnchanged(order!);
  337. });
  338. it('addItems order limit', async () => {
  339. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  340. id: orderId,
  341. });
  342. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  343. MODIFY_ORDER,
  344. {
  345. input: {
  346. dryRun: false,
  347. orderId,
  348. addItems: [{ productVariantId: 'T_4', quantity: 9999 }],
  349. },
  350. },
  351. );
  352. orderGuard.assertErrorResult(modifyOrder);
  353. expect(modifyOrder.errorCode).toBe(ErrorCode.ORDER_LIMIT_ERROR);
  354. await assertOrderIsUnchanged(order!);
  355. });
  356. it('adjustOrderLines order limit', async () => {
  357. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  358. id: orderId,
  359. });
  360. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  361. MODIFY_ORDER,
  362. {
  363. input: {
  364. dryRun: false,
  365. orderId,
  366. adjustOrderLines: [{ orderLineId: order!.lines[1].id, quantity: 9999 }],
  367. },
  368. },
  369. );
  370. orderGuard.assertErrorResult(modifyOrder);
  371. expect(modifyOrder.errorCode).toBe(ErrorCode.ORDER_LIMIT_ERROR);
  372. await assertOrderIsUnchanged(order!);
  373. });
  374. });
  375. describe('dry run', () => {
  376. it('addItems', async () => {
  377. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  378. id: orderId,
  379. });
  380. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  381. MODIFY_ORDER,
  382. {
  383. input: {
  384. dryRun: true,
  385. orderId,
  386. addItems: [{ productVariantId: 'T_5', quantity: 1 }],
  387. },
  388. },
  389. );
  390. orderGuard.assertSuccess(modifyOrder);
  391. const expectedTotal = order!.totalWithTax + Math.round(14374 * 1.2); // price of variant T_5
  392. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  393. expect(modifyOrder.lines.length).toBe(order!.lines.length + 1);
  394. await assertOrderIsUnchanged(order!);
  395. });
  396. it('addItems with existing variant id increments existing OrderLine', async () => {
  397. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  398. id: orderId,
  399. });
  400. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  401. MODIFY_ORDER,
  402. {
  403. input: {
  404. dryRun: true,
  405. orderId,
  406. addItems: [
  407. { productVariantId: 'T_1', quantity: 1, customFields: { color: 'green' } } as any,
  408. ],
  409. },
  410. },
  411. );
  412. orderGuard.assertSuccess(modifyOrder);
  413. const lineT1 = modifyOrder.lines.find(l => l.productVariant.id === 'T_1');
  414. expect(modifyOrder.lines.length).toBe(2);
  415. expect(lineT1?.quantity).toBe(2);
  416. await assertOrderIsUnchanged(order!);
  417. });
  418. it('addItems with existing variant id but different customFields adds new OrderLine', async () => {
  419. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  420. id: orderId,
  421. });
  422. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  423. MODIFY_ORDER,
  424. {
  425. input: {
  426. dryRun: true,
  427. orderId,
  428. addItems: [
  429. { productVariantId: 'T_1', quantity: 1, customFields: { color: 'blue' } } as any,
  430. ],
  431. },
  432. },
  433. );
  434. orderGuard.assertSuccess(modifyOrder);
  435. const lineT1 = modifyOrder.lines.find(l => l.productVariant.id === 'T_1');
  436. expect(modifyOrder.lines.length).toBe(3);
  437. expect(
  438. modifyOrder.lines.map(l => ({ variantId: l.productVariant.id, quantity: l.quantity })),
  439. ).toEqual([
  440. { variantId: 'T_1', quantity: 1 },
  441. { variantId: 'T_4', quantity: 2 },
  442. { variantId: 'T_1', quantity: 1 },
  443. ]);
  444. await assertOrderIsUnchanged(order!);
  445. });
  446. it('adjustOrderLines up', async () => {
  447. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  448. id: orderId,
  449. });
  450. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  451. MODIFY_ORDER,
  452. {
  453. input: {
  454. dryRun: true,
  455. orderId,
  456. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 3 }],
  457. },
  458. },
  459. );
  460. orderGuard.assertSuccess(modifyOrder);
  461. const expectedTotal = order!.totalWithTax + order!.lines[0].unitPriceWithTax * 2;
  462. expect(modifyOrder.lines[0].items.length).toBe(3);
  463. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  464. await assertOrderIsUnchanged(order!);
  465. });
  466. it('adjustOrderLines down', async () => {
  467. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  468. id: orderId,
  469. });
  470. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  471. MODIFY_ORDER,
  472. {
  473. input: {
  474. dryRun: true,
  475. orderId,
  476. adjustOrderLines: [{ orderLineId: order!.lines[1].id, quantity: 1 }],
  477. },
  478. },
  479. );
  480. orderGuard.assertSuccess(modifyOrder);
  481. const expectedTotal = order!.totalWithTax - order!.lines[1].unitPriceWithTax;
  482. expect(modifyOrder.lines[1].items.filter(i => i.cancelled).length).toBe(1);
  483. expect(modifyOrder.lines[1].items.filter(i => !i.cancelled).length).toBe(1);
  484. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  485. await assertOrderIsUnchanged(order!);
  486. });
  487. it('adjustOrderLines to zero', async () => {
  488. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  489. id: orderId,
  490. });
  491. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  492. MODIFY_ORDER,
  493. {
  494. input: {
  495. dryRun: true,
  496. orderId,
  497. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 0 }],
  498. },
  499. },
  500. );
  501. orderGuard.assertSuccess(modifyOrder);
  502. const expectedTotal = order!.totalWithTax - order!.lines[0].linePriceWithTax;
  503. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  504. expect(modifyOrder.lines[0].items.every(i => i.cancelled)).toBe(true);
  505. await assertOrderIsUnchanged(order!);
  506. });
  507. it('surcharge positive', async () => {
  508. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  509. id: orderId,
  510. });
  511. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  512. MODIFY_ORDER,
  513. {
  514. input: {
  515. dryRun: true,
  516. orderId,
  517. surcharges: [
  518. {
  519. description: 'extra fee',
  520. sku: '123',
  521. price: 300,
  522. priceIncludesTax: true,
  523. taxRate: 20,
  524. taxDescription: 'VAT',
  525. },
  526. ],
  527. },
  528. },
  529. );
  530. orderGuard.assertSuccess(modifyOrder);
  531. const expectedTotal = order!.totalWithTax + 300;
  532. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  533. expect(modifyOrder.surcharges.map(s => omit(s, ['id']))).toEqual([
  534. {
  535. description: 'extra fee',
  536. sku: '123',
  537. price: 250,
  538. priceWithTax: 300,
  539. taxRate: 20,
  540. },
  541. ]);
  542. await assertOrderIsUnchanged(order!);
  543. });
  544. it('surcharge negative', async () => {
  545. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  546. id: orderId,
  547. });
  548. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  549. MODIFY_ORDER,
  550. {
  551. input: {
  552. dryRun: true,
  553. orderId,
  554. surcharges: [
  555. {
  556. description: 'special discount',
  557. sku: '123',
  558. price: -300,
  559. priceIncludesTax: true,
  560. taxRate: 20,
  561. taxDescription: 'VAT',
  562. },
  563. ],
  564. },
  565. },
  566. );
  567. orderGuard.assertSuccess(modifyOrder);
  568. const expectedTotal = order!.totalWithTax + -300;
  569. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  570. expect(modifyOrder.surcharges.map(s => omit(s, ['id']))).toEqual([
  571. {
  572. description: 'special discount',
  573. sku: '123',
  574. price: -250,
  575. priceWithTax: -300,
  576. taxRate: 20,
  577. },
  578. ]);
  579. await assertOrderIsUnchanged(order!);
  580. });
  581. it('does not add a history entry', async () => {
  582. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  583. id: orderId,
  584. });
  585. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  586. MODIFY_ORDER,
  587. {
  588. input: {
  589. dryRun: true,
  590. orderId,
  591. addItems: [{ productVariantId: 'T_5', quantity: 1 }],
  592. },
  593. },
  594. );
  595. orderGuard.assertSuccess(modifyOrder);
  596. const { order: history } = await adminClient.query<
  597. GetOrderHistory.Query,
  598. GetOrderHistory.Variables
  599. >(GET_ORDER_HISTORY, {
  600. id: orderId,
  601. options: { filter: { type: { eq: HistoryEntryType.ORDER_MODIFIED } } },
  602. });
  603. orderGuard.assertSuccess(history);
  604. expect(history.history.totalItems).toBe(0);
  605. });
  606. });
  607. describe('wet run', () => {
  608. async function assertModifiedOrderIsPersisted(order: OrderWithModificationsFragment) {
  609. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  610. id: order.id,
  611. });
  612. expect(order2!.totalWithTax).toBe(order!.totalWithTax);
  613. expect(order2!.lines.length).toBe(order!.lines.length);
  614. expect(order2!.surcharges.length).toBe(order!.surcharges.length);
  615. expect(order2!.payments!.length).toBe(order!.payments!.length);
  616. expect(order2!.payments!.map(p => pick(p, ['id', 'amount', 'method']))).toEqual(
  617. order!.payments!.map(p => pick(p, ['id', 'amount', 'method'])),
  618. );
  619. }
  620. it('addItems', async () => {
  621. const order = await createOrderAndTransitionToModifyingState([
  622. {
  623. productVariantId: 'T_1',
  624. quantity: 1,
  625. },
  626. ]);
  627. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  628. MODIFY_ORDER,
  629. {
  630. input: {
  631. dryRun: false,
  632. orderId: order.id,
  633. addItems: [{ productVariantId: 'T_5', quantity: 1 }],
  634. },
  635. },
  636. );
  637. orderGuard.assertSuccess(modifyOrder);
  638. const priceDelta = Math.round(14374 * 1.2); // price of variant T_5
  639. const expectedTotal = order!.totalWithTax + priceDelta;
  640. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  641. expect(modifyOrder.lines.length).toBe(order!.lines.length + 1);
  642. expect(modifyOrder.modifications.length).toBe(1);
  643. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  644. expect(modifyOrder.modifications[0].orderItems?.length).toBe(1);
  645. expect(modifyOrder.modifications[0].orderItems?.map(i => i.id)).toEqual([
  646. modifyOrder.lines[1].items[0].id,
  647. ]);
  648. await assertModifiedOrderIsPersisted(modifyOrder);
  649. });
  650. it('adjustOrderLines up', async () => {
  651. const order = await createOrderAndTransitionToModifyingState([
  652. {
  653. productVariantId: 'T_1',
  654. quantity: 1,
  655. },
  656. ]);
  657. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  658. MODIFY_ORDER,
  659. {
  660. input: {
  661. dryRun: false,
  662. orderId: order.id,
  663. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 2 }],
  664. },
  665. },
  666. );
  667. orderGuard.assertSuccess(modifyOrder);
  668. const priceDelta = order!.lines[0].unitPriceWithTax;
  669. const expectedTotal = order!.totalWithTax + priceDelta;
  670. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  671. expect(modifyOrder.lines[0].quantity).toBe(2);
  672. expect(modifyOrder.modifications.length).toBe(1);
  673. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  674. expect(modifyOrder.modifications[0].orderItems?.length).toBe(1);
  675. expect(
  676. modifyOrder.lines[0].items
  677. .map(i => i.id)
  678. .includes(modifyOrder.modifications?.[0].orderItems?.[0].id as string),
  679. ).toBe(true);
  680. await assertModifiedOrderIsPersisted(modifyOrder);
  681. });
  682. it('adjustOrderLines down', async () => {
  683. const order = await createOrderAndTransitionToModifyingState([
  684. {
  685. productVariantId: 'T_1',
  686. quantity: 2,
  687. },
  688. ]);
  689. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  690. MODIFY_ORDER,
  691. {
  692. input: {
  693. dryRun: false,
  694. orderId: order.id,
  695. adjustOrderLines: [{ orderLineId: order!.lines[0].id, quantity: 1 }],
  696. refund: { paymentId: order!.payments![0].id },
  697. },
  698. },
  699. );
  700. orderGuard.assertSuccess(modifyOrder);
  701. const priceDelta = -order!.lines[0].unitPriceWithTax;
  702. const expectedTotal = order!.totalWithTax + priceDelta;
  703. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  704. expect(modifyOrder.lines[0].quantity).toBe(1);
  705. expect(modifyOrder.payments?.length).toBe(1);
  706. expect(modifyOrder.payments?.[0].refunds.length).toBe(1);
  707. expect(modifyOrder.payments?.[0].refunds[0]).toEqual({
  708. id: 'T_1',
  709. state: 'Pending',
  710. total: -priceDelta,
  711. paymentId: modifyOrder.payments?.[0].id,
  712. });
  713. expect(modifyOrder.modifications.length).toBe(1);
  714. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  715. expect(modifyOrder.modifications[0].surcharges).toEqual(modifyOrder.surcharges.map(pick(['id'])));
  716. expect(modifyOrder.modifications[0].orderItems?.length).toBe(1);
  717. expect(
  718. modifyOrder.lines[0].items
  719. .map(i => i.id)
  720. .includes(modifyOrder.modifications?.[0].orderItems?.[0].id as string),
  721. ).toBe(true);
  722. await assertModifiedOrderIsPersisted(modifyOrder);
  723. });
  724. it('adjustOrderLines with changed customField value', async () => {
  725. const order = await createOrderAndTransitionToModifyingState([
  726. {
  727. productVariantId: 'T_1',
  728. quantity: 1,
  729. customFields: {
  730. color: 'green',
  731. },
  732. },
  733. ]);
  734. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  735. MODIFY_ORDER,
  736. {
  737. input: {
  738. dryRun: false,
  739. orderId: order.id,
  740. adjustOrderLines: [
  741. {
  742. orderLineId: order!.lines[0].id,
  743. quantity: 1,
  744. customFields: { color: 'black' },
  745. } as any,
  746. ],
  747. },
  748. },
  749. );
  750. orderGuard.assertSuccess(modifyOrder);
  751. expect(modifyOrder.lines.length).toBe(1);
  752. const { order: orderWithLines } = await adminClient.query(gql(GET_ORDER_WITH_CUSTOM_FIELDS), {
  753. id: order.id,
  754. });
  755. expect(orderWithLines.lines[0]).toEqual({
  756. id: order!.lines[0].id,
  757. customFields: { color: 'black' },
  758. });
  759. });
  760. it('adjustOrderLines handles quantity correctly', async () => {
  761. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  762. UPDATE_PRODUCT_VARIANTS,
  763. {
  764. input: [
  765. {
  766. id: 'T_6',
  767. stockOnHand: 1,
  768. trackInventory: GlobalFlag.TRUE,
  769. },
  770. ],
  771. },
  772. );
  773. const order = await createOrderAndTransitionToModifyingState([
  774. {
  775. productVariantId: 'T_6',
  776. quantity: 1,
  777. },
  778. ]);
  779. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  780. MODIFY_ORDER,
  781. {
  782. input: {
  783. dryRun: false,
  784. orderId: order.id,
  785. adjustOrderLines: [
  786. {
  787. orderLineId: order.lines[0].id,
  788. quantity: 1,
  789. },
  790. ],
  791. updateShippingAddress: {
  792. fullName: 'Jim',
  793. },
  794. },
  795. },
  796. );
  797. orderGuard.assertSuccess(modifyOrder);
  798. });
  799. it('surcharge positive', async () => {
  800. const order = await createOrderAndTransitionToModifyingState([
  801. {
  802. productVariantId: 'T_1',
  803. quantity: 1,
  804. },
  805. ]);
  806. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  807. MODIFY_ORDER,
  808. {
  809. input: {
  810. dryRun: false,
  811. orderId: order.id,
  812. surcharges: [
  813. {
  814. description: 'extra fee',
  815. sku: '123',
  816. price: 300,
  817. priceIncludesTax: true,
  818. taxRate: 20,
  819. taxDescription: 'VAT',
  820. },
  821. ],
  822. },
  823. },
  824. );
  825. orderGuard.assertSuccess(modifyOrder);
  826. const priceDelta = 300;
  827. const expectedTotal = order!.totalWithTax + priceDelta;
  828. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  829. expect(modifyOrder.surcharges.map(s => omit(s, ['id']))).toEqual([
  830. {
  831. description: 'extra fee',
  832. sku: '123',
  833. price: 250,
  834. priceWithTax: 300,
  835. taxRate: 20,
  836. },
  837. ]);
  838. expect(modifyOrder.modifications.length).toBe(1);
  839. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  840. expect(modifyOrder.modifications[0].surcharges).toEqual(modifyOrder.surcharges.map(pick(['id'])));
  841. await assertModifiedOrderIsPersisted(modifyOrder);
  842. });
  843. it('surcharge negative', async () => {
  844. const order = await createOrderAndTransitionToModifyingState([
  845. {
  846. productVariantId: 'T_1',
  847. quantity: 1,
  848. },
  849. ]);
  850. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  851. MODIFY_ORDER,
  852. {
  853. input: {
  854. dryRun: false,
  855. orderId: order!.id,
  856. surcharges: [
  857. {
  858. description: 'special discount',
  859. sku: '123',
  860. price: -300,
  861. priceIncludesTax: true,
  862. taxRate: 20,
  863. taxDescription: 'VAT',
  864. },
  865. ],
  866. refund: {
  867. paymentId: order!.payments![0].id,
  868. },
  869. },
  870. },
  871. );
  872. orderGuard.assertSuccess(modifyOrder);
  873. const expectedTotal = order!.totalWithTax + -300;
  874. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  875. expect(modifyOrder.surcharges.map(s => omit(s, ['id']))).toEqual([
  876. {
  877. description: 'special discount',
  878. sku: '123',
  879. price: -250,
  880. priceWithTax: -300,
  881. taxRate: 20,
  882. },
  883. ]);
  884. expect(modifyOrder.modifications.length).toBe(1);
  885. expect(modifyOrder.modifications[0].priceChange).toBe(-300);
  886. await assertModifiedOrderIsPersisted(modifyOrder);
  887. });
  888. it('update updateShippingAddress, recalculate shipping', async () => {
  889. const order = await createOrderAndTransitionToModifyingState([
  890. {
  891. productVariantId: 'T_1',
  892. quantity: 1,
  893. },
  894. ]);
  895. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  896. MODIFY_ORDER,
  897. {
  898. input: {
  899. dryRun: false,
  900. orderId: order!.id,
  901. updateShippingAddress: {
  902. countryCode: 'US',
  903. },
  904. options: {
  905. recalculateShipping: true,
  906. },
  907. },
  908. },
  909. );
  910. orderGuard.assertSuccess(modifyOrder);
  911. const priceDelta = SHIPPING_US - SHIPPING_OTHER;
  912. const expectedTotal = order!.totalWithTax + priceDelta;
  913. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  914. expect(modifyOrder.shippingAddress?.countryCode).toBe('US');
  915. expect(modifyOrder.modifications.length).toBe(1);
  916. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  917. await assertModifiedOrderIsPersisted(modifyOrder);
  918. });
  919. it('update updateShippingAddress, do not recalculate shipping', async () => {
  920. const order = await createOrderAndTransitionToModifyingState([
  921. {
  922. productVariantId: 'T_1',
  923. quantity: 1,
  924. },
  925. ]);
  926. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  927. MODIFY_ORDER,
  928. {
  929. input: {
  930. dryRun: false,
  931. orderId: order!.id,
  932. updateShippingAddress: {
  933. countryCode: 'US',
  934. },
  935. options: {
  936. recalculateShipping: false,
  937. },
  938. },
  939. },
  940. );
  941. orderGuard.assertSuccess(modifyOrder);
  942. const priceDelta = 0;
  943. const expectedTotal = order!.totalWithTax + priceDelta;
  944. expect(modifyOrder.totalWithTax).toBe(expectedTotal);
  945. expect(modifyOrder.shippingAddress?.countryCode).toBe('US');
  946. expect(modifyOrder.modifications.length).toBe(1);
  947. expect(modifyOrder.modifications[0].priceChange).toBe(priceDelta);
  948. await assertModifiedOrderIsPersisted(modifyOrder);
  949. });
  950. it('update Order customFields', async () => {
  951. const order = await createOrderAndTransitionToModifyingState([
  952. {
  953. productVariantId: 'T_1',
  954. quantity: 1,
  955. },
  956. ]);
  957. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  958. MODIFY_ORDER,
  959. {
  960. input: {
  961. dryRun: false,
  962. orderId: order.id,
  963. customFields: {
  964. points: 42,
  965. },
  966. } as any,
  967. },
  968. );
  969. orderGuard.assertSuccess(modifyOrder);
  970. const { order: orderWithCustomFields } = await adminClient.query(
  971. gql(GET_ORDER_WITH_CUSTOM_FIELDS),
  972. { id: order.id },
  973. );
  974. expect(orderWithCustomFields.customFields).toEqual({
  975. points: 42,
  976. });
  977. });
  978. it('adds a history entry', async () => {
  979. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  980. id: orderId,
  981. });
  982. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  983. MODIFY_ORDER,
  984. {
  985. input: {
  986. dryRun: false,
  987. orderId: order!.id,
  988. addItems: [{ productVariantId: 'T_5', quantity: 1 }],
  989. },
  990. },
  991. );
  992. orderGuard.assertSuccess(modifyOrder);
  993. const { order: history } = await adminClient.query<
  994. GetOrderHistory.Query,
  995. GetOrderHistory.Variables
  996. >(GET_ORDER_HISTORY, {
  997. id: orderId,
  998. options: { filter: { type: { eq: HistoryEntryType.ORDER_MODIFIED } } },
  999. });
  1000. orderGuard.assertSuccess(history);
  1001. expect(history.history.totalItems).toBe(1);
  1002. expect(history.history.items[0].data).toEqual({
  1003. modificationId: modifyOrder.modifications[0].id,
  1004. });
  1005. });
  1006. });
  1007. describe('additional payment handling', () => {
  1008. let orderId2: string;
  1009. beforeAll(async () => {
  1010. const order = await createOrderAndTransitionToModifyingState([
  1011. {
  1012. productVariantId: 'T_1',
  1013. quantity: 1,
  1014. },
  1015. ]);
  1016. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1017. MODIFY_ORDER,
  1018. {
  1019. input: {
  1020. dryRun: false,
  1021. orderId: order.id,
  1022. surcharges: [
  1023. {
  1024. description: 'extra fee',
  1025. sku: '123',
  1026. price: 300,
  1027. priceIncludesTax: true,
  1028. taxRate: 20,
  1029. taxDescription: 'VAT',
  1030. },
  1031. ],
  1032. },
  1033. },
  1034. );
  1035. orderGuard.assertSuccess(modifyOrder);
  1036. orderId2 = modifyOrder.id;
  1037. });
  1038. it('cannot transition back to original state if no payment is set', async () => {
  1039. const { transitionOrderToState } = await adminClient.query<
  1040. AdminTransition.Mutation,
  1041. AdminTransition.Variables
  1042. >(ADMIN_TRANSITION_TO_STATE, {
  1043. id: orderId2,
  1044. state: 'PaymentSettled',
  1045. });
  1046. orderGuard.assertErrorResult(transitionOrderToState);
  1047. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  1048. expect(transitionOrderToState!.transitionError).toBe(
  1049. `Can only transition to the "ArrangingAdditionalPayment" state`,
  1050. );
  1051. });
  1052. it('can transition to ArrangingAdditionalPayment state', async () => {
  1053. const { transitionOrderToState } = await adminClient.query<
  1054. AdminTransition.Mutation,
  1055. AdminTransition.Variables
  1056. >(ADMIN_TRANSITION_TO_STATE, {
  1057. id: orderId2,
  1058. state: 'ArrangingAdditionalPayment',
  1059. });
  1060. orderGuard.assertSuccess(transitionOrderToState);
  1061. expect(transitionOrderToState!.state).toBe('ArrangingAdditionalPayment');
  1062. });
  1063. it('cannot transition from ArrangingAdditionalPayment when total not covered by Payments', async () => {
  1064. const { transitionOrderToState } = await adminClient.query<
  1065. AdminTransition.Mutation,
  1066. AdminTransition.Variables
  1067. >(ADMIN_TRANSITION_TO_STATE, {
  1068. id: orderId2,
  1069. state: 'PaymentSettled',
  1070. });
  1071. orderGuard.assertErrorResult(transitionOrderToState);
  1072. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  1073. expect(transitionOrderToState!.transitionError).toBe(
  1074. `Cannot transition away from "ArrangingAdditionalPayment" unless Order total is covered by Payments`,
  1075. );
  1076. });
  1077. it('addManualPaymentToOrder', async () => {
  1078. const { addManualPaymentToOrder } = await adminClient.query<
  1079. AddManualPayment.Mutation,
  1080. AddManualPayment.Variables
  1081. >(ADD_MANUAL_PAYMENT, {
  1082. input: {
  1083. orderId: orderId2,
  1084. method: 'test',
  1085. transactionId: 'ABC123',
  1086. metadata: {
  1087. foo: 'bar',
  1088. },
  1089. },
  1090. });
  1091. orderGuard.assertSuccess(addManualPaymentToOrder);
  1092. expect(addManualPaymentToOrder.payments?.length).toBe(2);
  1093. expect(omit(addManualPaymentToOrder.payments![1], ['id'])).toEqual({
  1094. transactionId: 'ABC123',
  1095. state: 'Settled',
  1096. amount: 300,
  1097. method: 'test',
  1098. metadata: {
  1099. foo: 'bar',
  1100. },
  1101. refunds: [],
  1102. });
  1103. expect(addManualPaymentToOrder.modifications[0].isSettled).toBe(true);
  1104. expect(addManualPaymentToOrder.modifications[0].payment?.id).toBe(
  1105. addManualPaymentToOrder.payments![1].id,
  1106. );
  1107. });
  1108. it('transition back to original state', async () => {
  1109. const { transitionOrderToState } = await adminClient.query<
  1110. AdminTransition.Mutation,
  1111. AdminTransition.Variables
  1112. >(ADMIN_TRANSITION_TO_STATE, {
  1113. id: orderId2,
  1114. state: 'PaymentSettled',
  1115. });
  1116. orderGuard.assertSuccess(transitionOrderToState);
  1117. expect(transitionOrderToState.state).toBe('PaymentSettled');
  1118. });
  1119. });
  1120. describe('refund handling', () => {
  1121. let orderId3: string;
  1122. beforeAll(async () => {
  1123. const order = await createOrderAndTransitionToModifyingState([
  1124. {
  1125. productVariantId: 'T_1',
  1126. quantity: 1,
  1127. },
  1128. ]);
  1129. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1130. MODIFY_ORDER,
  1131. {
  1132. input: {
  1133. dryRun: false,
  1134. orderId: order.id,
  1135. surcharges: [
  1136. {
  1137. description: 'discount',
  1138. sku: '123',
  1139. price: -300,
  1140. priceIncludesTax: true,
  1141. taxRate: 20,
  1142. taxDescription: 'VAT',
  1143. },
  1144. ],
  1145. refund: {
  1146. paymentId: order.payments![0].id,
  1147. reason: 'discount',
  1148. },
  1149. },
  1150. },
  1151. );
  1152. orderGuard.assertSuccess(modifyOrder);
  1153. orderId3 = modifyOrder.id;
  1154. });
  1155. it('modification is settled', async () => {
  1156. const { order } = await adminClient.query<
  1157. GetOrderWithModifications.Query,
  1158. GetOrderWithModifications.Variables
  1159. >(GET_ORDER_WITH_MODIFICATIONS, { id: orderId3 });
  1160. expect(order?.modifications.length).toBe(1);
  1161. expect(order?.modifications[0].isSettled).toBe(true);
  1162. });
  1163. it('cannot transition to ArrangingAdditionalPayment state if no payment is needed', async () => {
  1164. const { transitionOrderToState } = await adminClient.query<
  1165. AdminTransition.Mutation,
  1166. AdminTransition.Variables
  1167. >(ADMIN_TRANSITION_TO_STATE, {
  1168. id: orderId3,
  1169. state: 'ArrangingAdditionalPayment',
  1170. });
  1171. orderGuard.assertErrorResult(transitionOrderToState);
  1172. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  1173. expect(transitionOrderToState!.transitionError).toBe(
  1174. `Cannot transition Order to the \"ArrangingAdditionalPayment\" state as no additional payments are needed`,
  1175. );
  1176. });
  1177. it('can transition to original state', async () => {
  1178. const { transitionOrderToState } = await adminClient.query<
  1179. AdminTransition.Mutation,
  1180. AdminTransition.Variables
  1181. >(ADMIN_TRANSITION_TO_STATE, {
  1182. id: orderId3,
  1183. state: 'PaymentSettled',
  1184. });
  1185. orderGuard.assertSuccess(transitionOrderToState);
  1186. expect(transitionOrderToState!.state).toBe('PaymentSettled');
  1187. const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1188. id: orderId3,
  1189. });
  1190. expect(order?.payments![0].refunds.length).toBe(1);
  1191. expect(order?.payments![0].refunds[0].total).toBe(300);
  1192. expect(order?.payments![0].refunds[0].reason).toBe('discount');
  1193. });
  1194. });
  1195. // https://github.com/vendure-ecommerce/vendure/issues/688 - 4th point
  1196. it('correct additional payment when discounts applied', async () => {
  1197. await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
  1198. input: {
  1199. name: '$5 off',
  1200. couponCode: '5OFF',
  1201. enabled: true,
  1202. conditions: [],
  1203. actions: [
  1204. {
  1205. code: orderFixedDiscount.code,
  1206. arguments: [{ name: 'discount', value: '500' }],
  1207. },
  1208. ],
  1209. },
  1210. });
  1211. await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
  1212. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
  1213. productVariantId: 'T_1',
  1214. quantity: 1,
  1215. } as any);
  1216. await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(APPLY_COUPON_CODE, {
  1217. couponCode: '5OFF',
  1218. });
  1219. await proceedToArrangingPayment(shopClient);
  1220. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1221. orderGuard.assertSuccess(order);
  1222. const originalTotalWithTax = order.totalWithTax;
  1223. const surcharge = 300;
  1224. const { transitionOrderToState } = await adminClient.query<
  1225. AdminTransition.Mutation,
  1226. AdminTransition.Variables
  1227. >(ADMIN_TRANSITION_TO_STATE, {
  1228. id: order.id,
  1229. state: 'Modifying',
  1230. });
  1231. orderGuard.assertSuccess(transitionOrderToState);
  1232. expect(transitionOrderToState.state).toBe('Modifying');
  1233. const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
  1234. MODIFY_ORDER,
  1235. {
  1236. input: {
  1237. dryRun: false,
  1238. orderId: order.id,
  1239. surcharges: [
  1240. {
  1241. description: 'extra fee',
  1242. sku: '123',
  1243. price: surcharge,
  1244. priceIncludesTax: true,
  1245. taxRate: 20,
  1246. taxDescription: 'VAT',
  1247. },
  1248. ],
  1249. },
  1250. },
  1251. );
  1252. orderGuard.assertSuccess(modifyOrder);
  1253. expect(modifyOrder.totalWithTax).toBe(originalTotalWithTax + surcharge);
  1254. });
  1255. async function assertOrderIsUnchanged(order: OrderWithLinesFragment) {
  1256. const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  1257. id: order.id,
  1258. });
  1259. expect(order2!.totalWithTax).toBe(order!.totalWithTax);
  1260. expect(order2!.lines.length).toBe(order!.lines.length);
  1261. expect(order2!.surcharges.length).toBe(order!.surcharges.length);
  1262. expect(order2!.totalQuantity).toBe(order!.totalQuantity);
  1263. }
  1264. async function createOrderAndTransitionToModifyingState(
  1265. items: Array<AddItemToOrderMutationVariables & { customFields?: any }>,
  1266. ): Promise<TestOrderWithPaymentsFragment> {
  1267. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  1268. for (const itemInput of items) {
  1269. await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), itemInput);
  1270. }
  1271. await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
  1272. SET_SHIPPING_ADDRESS,
  1273. {
  1274. input: {
  1275. fullName: 'name',
  1276. streetLine1: '12 the street',
  1277. city: 'foo',
  1278. postalCode: '123456',
  1279. countryCode: 'AT',
  1280. },
  1281. },
  1282. );
  1283. await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(SET_SHIPPING_METHOD, {
  1284. id: testShippingMethodId,
  1285. });
  1286. await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(TRANSITION_TO_STATE, {
  1287. state: 'ArrangingPayment',
  1288. });
  1289. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1290. orderGuard.assertSuccess(order);
  1291. const { transitionOrderToState } = await adminClient.query<
  1292. AdminTransition.Mutation,
  1293. AdminTransition.Variables
  1294. >(ADMIN_TRANSITION_TO_STATE, {
  1295. id: order.id,
  1296. state: 'Modifying',
  1297. });
  1298. return order;
  1299. }
  1300. });
  1301. export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
  1302. fragment OrderWithModifications on Order {
  1303. id
  1304. state
  1305. total
  1306. totalWithTax
  1307. lines {
  1308. id
  1309. quantity
  1310. linePrice
  1311. linePriceWithTax
  1312. productVariant {
  1313. id
  1314. name
  1315. }
  1316. items {
  1317. id
  1318. createdAt
  1319. updatedAt
  1320. cancelled
  1321. unitPrice
  1322. }
  1323. }
  1324. surcharges {
  1325. id
  1326. description
  1327. sku
  1328. price
  1329. priceWithTax
  1330. taxRate
  1331. }
  1332. payments {
  1333. id
  1334. transactionId
  1335. state
  1336. amount
  1337. method
  1338. metadata
  1339. refunds {
  1340. id
  1341. state
  1342. total
  1343. paymentId
  1344. }
  1345. }
  1346. modifications {
  1347. id
  1348. note
  1349. priceChange
  1350. isSettled
  1351. orderItems {
  1352. id
  1353. }
  1354. surcharges {
  1355. id
  1356. }
  1357. payment {
  1358. id
  1359. state
  1360. amount
  1361. method
  1362. }
  1363. refund {
  1364. id
  1365. state
  1366. total
  1367. paymentId
  1368. }
  1369. }
  1370. shippingAddress {
  1371. streetLine1
  1372. city
  1373. postalCode
  1374. province
  1375. countryCode
  1376. country
  1377. }
  1378. billingAddress {
  1379. streetLine1
  1380. city
  1381. postalCode
  1382. province
  1383. countryCode
  1384. country
  1385. }
  1386. }
  1387. `;
  1388. export const GET_ORDER_WITH_MODIFICATIONS = gql`
  1389. query GetOrderWithModifications($id: ID!) {
  1390. order(id: $id) {
  1391. ...OrderWithModifications
  1392. }
  1393. }
  1394. ${ORDER_WITH_MODIFICATION_FRAGMENT}
  1395. `;
  1396. export const MODIFY_ORDER = gql`
  1397. mutation ModifyOrder($input: ModifyOrderInput!) {
  1398. modifyOrder(input: $input) {
  1399. ...OrderWithModifications
  1400. ... on ErrorResult {
  1401. errorCode
  1402. message
  1403. }
  1404. }
  1405. }
  1406. ${ORDER_WITH_MODIFICATION_FRAGMENT}
  1407. `;
  1408. export const ADD_MANUAL_PAYMENT = gql`
  1409. mutation AddManualPayment($input: ManualPaymentInput!) {
  1410. addManualPaymentToOrder(input: $input) {
  1411. ...OrderWithModifications
  1412. ... on ErrorResult {
  1413. errorCode
  1414. message
  1415. }
  1416. }
  1417. }
  1418. ${ORDER_WITH_MODIFICATION_FRAGMENT}
  1419. `;
  1420. // Note, we don't use the gql tag around these due to the customFields which
  1421. // would cause a codegen error.
  1422. const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = `
  1423. mutation AddItemToOrder($productVariantId: ID!, $quantity: Int!, $customFields: OrderLineCustomFieldsInput) {
  1424. addItemToOrder(productVariantId: $productVariantId, quantity: $quantity, customFields: $customFields) {
  1425. ...on Order { id }
  1426. }
  1427. }
  1428. `;
  1429. const GET_ORDER_WITH_CUSTOM_FIELDS = `
  1430. query GetOrderCustomFields($id: ID!) {
  1431. order(id: $id) {
  1432. customFields { points }
  1433. lines { id, customFields { color } }
  1434. }
  1435. }
  1436. `;