promotion.e2e-spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. import { pick } from '@vendure/common/lib/pick';
  2. import { PromotionAction, PromotionCondition, PromotionOrderAction } from '@vendure/core';
  3. import {
  4. createErrorResultGuard,
  5. createTestEnvironment,
  6. E2E_DEFAULT_CHANNEL_TOKEN,
  7. ErrorResultGuard,
  8. } from '@vendure/testing';
  9. import gql from 'graphql-tag';
  10. import path from 'path';
  11. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  12. import { initialData } from '../../../e2e-common/e2e-initial-data';
  13. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  14. import { PROMOTION_FRAGMENT } from './graphql/fragments';
  15. import * as Codegen from './graphql/generated-e2e-admin-types';
  16. import { CurrencyCode, DeletionResult, ErrorCode, LanguageCode } from './graphql/generated-e2e-admin-types';
  17. import {
  18. ASSIGN_PROMOTIONS_TO_CHANNEL,
  19. CREATE_CHANNEL,
  20. CREATE_PROMOTION,
  21. DELETE_PROMOTION,
  22. REMOVE_PROMOTIONS_FROM_CHANNEL,
  23. } from './graphql/shared-definitions';
  24. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  25. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  26. describe('Promotion resolver', () => {
  27. const promoCondition = generateTestCondition('promo_condition');
  28. const promoCondition2 = generateTestCondition('promo_condition2');
  29. const promoAction = generateTestAction('promo_action');
  30. const { server, adminClient, shopClient } = createTestEnvironment({
  31. ...testConfig(),
  32. promotionOptions: {
  33. promotionConditions: [promoCondition, promoCondition2],
  34. promotionActions: [promoAction],
  35. },
  36. });
  37. const snapshotProps: Array<keyof Codegen.PromotionFragment> = [
  38. 'name',
  39. 'actions',
  40. 'conditions',
  41. 'enabled',
  42. 'couponCode',
  43. 'startsAt',
  44. 'endsAt',
  45. ];
  46. let promotion: Codegen.PromotionFragment;
  47. const promotionGuard: ErrorResultGuard<Codegen.PromotionFragment> = createErrorResultGuard(
  48. input => !!input.couponCode,
  49. );
  50. beforeAll(async () => {
  51. await server.init({
  52. initialData,
  53. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  54. customerCount: 1,
  55. });
  56. await adminClient.asSuperAdmin();
  57. }, TEST_SETUP_TIMEOUT_MS);
  58. afterAll(async () => {
  59. await server.destroy();
  60. });
  61. it('createPromotion', async () => {
  62. const { createPromotion } = await adminClient.query<
  63. Codegen.CreatePromotionMutation,
  64. Codegen.CreatePromotionMutationVariables
  65. >(CREATE_PROMOTION, {
  66. input: {
  67. enabled: true,
  68. couponCode: 'TEST123',
  69. startsAt: new Date('2019-10-30T00:00:00.000Z'),
  70. endsAt: new Date('2019-12-01T00:00:00.000Z'),
  71. translations: [
  72. {
  73. languageCode: LanguageCode.en,
  74. name: 'test promotion',
  75. description: 'a test promotion',
  76. },
  77. ],
  78. conditions: [
  79. {
  80. code: promoCondition.code,
  81. arguments: [{ name: 'arg', value: '500' }],
  82. },
  83. ],
  84. actions: [
  85. {
  86. code: promoAction.code,
  87. arguments: [
  88. {
  89. name: 'facetValueIds',
  90. value: '["T_1"]',
  91. },
  92. ],
  93. },
  94. ],
  95. },
  96. });
  97. promotionGuard.assertSuccess(createPromotion);
  98. promotion = createPromotion;
  99. expect(pick(promotion, snapshotProps)).toMatchSnapshot();
  100. });
  101. it('createPromotion with no description', async () => {
  102. const { createPromotion } = await adminClient.query<
  103. Codegen.CreatePromotionMutation,
  104. Codegen.CreatePromotionMutationVariables
  105. >(CREATE_PROMOTION, {
  106. input: {
  107. enabled: true,
  108. couponCode: 'TEST567',
  109. translations: [
  110. {
  111. languageCode: LanguageCode.en,
  112. name: 'test promotion no description',
  113. customFields: {},
  114. },
  115. ],
  116. conditions: [],
  117. actions: [
  118. {
  119. code: promoAction.code,
  120. arguments: [
  121. {
  122. name: 'facetValueIds',
  123. value: '["T_1"]',
  124. },
  125. ],
  126. },
  127. ],
  128. },
  129. });
  130. promotionGuard.assertSuccess(createPromotion);
  131. expect(createPromotion.name).toBe('test promotion no description');
  132. expect(createPromotion.description).toBe('');
  133. expect(createPromotion.translations[0].description).toBe('');
  134. });
  135. it('createPromotion return error result with empty conditions and no couponCode', async () => {
  136. const { createPromotion } = await adminClient.query<
  137. Codegen.CreatePromotionMutation,
  138. Codegen.CreatePromotionMutationVariables
  139. >(CREATE_PROMOTION, {
  140. input: {
  141. enabled: true,
  142. translations: [
  143. {
  144. languageCode: LanguageCode.en,
  145. name: 'bad promotion',
  146. },
  147. ],
  148. conditions: [],
  149. actions: [
  150. {
  151. code: promoAction.code,
  152. arguments: [
  153. {
  154. name: 'facetValueIds',
  155. value: '["T_1"]',
  156. },
  157. ],
  158. },
  159. ],
  160. },
  161. });
  162. promotionGuard.assertErrorResult(createPromotion);
  163. expect(createPromotion.message).toBe(
  164. 'A Promotion must have either at least one condition or a coupon code set',
  165. );
  166. expect(createPromotion.errorCode).toBe(ErrorCode.MISSING_CONDITIONS_ERROR);
  167. });
  168. it('updatePromotion', async () => {
  169. const { updatePromotion } = await adminClient.query<
  170. Codegen.UpdatePromotionMutation,
  171. Codegen.UpdatePromotionMutationVariables
  172. >(UPDATE_PROMOTION, {
  173. input: {
  174. id: promotion.id,
  175. couponCode: 'TEST1235',
  176. startsAt: new Date('2019-05-30T22:00:00.000Z'),
  177. endsAt: new Date('2019-06-01T22:00:00.000Z'),
  178. conditions: [
  179. {
  180. code: promoCondition.code,
  181. arguments: [{ name: 'arg', value: '90' }],
  182. },
  183. {
  184. code: promoCondition2.code,
  185. arguments: [{ name: 'arg', value: '10' }],
  186. },
  187. ],
  188. },
  189. });
  190. promotionGuard.assertSuccess(updatePromotion);
  191. expect(pick(updatePromotion, snapshotProps)).toMatchSnapshot();
  192. });
  193. it('updatePromotion return error result with empty conditions and no couponCode', async () => {
  194. const { updatePromotion } = await adminClient.query<
  195. Codegen.UpdatePromotionMutation,
  196. Codegen.UpdatePromotionMutationVariables
  197. >(UPDATE_PROMOTION, {
  198. input: {
  199. id: promotion.id,
  200. couponCode: '',
  201. conditions: [],
  202. },
  203. });
  204. promotionGuard.assertErrorResult(updatePromotion);
  205. expect(updatePromotion.message).toBe(
  206. 'A Promotion must have either at least one condition or a coupon code set',
  207. );
  208. expect(updatePromotion.errorCode).toBe(ErrorCode.MISSING_CONDITIONS_ERROR);
  209. });
  210. it('promotion', async () => {
  211. const result = await adminClient.query<Codegen.GetPromotionQuery, Codegen.GetPromotionQueryVariables>(
  212. GET_PROMOTION,
  213. {
  214. id: promotion.id,
  215. },
  216. );
  217. expect(result.promotion!.name).toBe(promotion.name);
  218. });
  219. it('promotions', async () => {
  220. const result = await adminClient.query<
  221. Codegen.GetPromotionListQuery,
  222. Codegen.GetPromotionListQueryVariables
  223. >(GET_PROMOTION_LIST, {});
  224. expect(result.promotions.totalItems).toBe(2);
  225. expect(result.promotions.items[0].name).toBe('test promotion');
  226. });
  227. it('adjustmentOperations', async () => {
  228. const result = await adminClient.query<
  229. Codegen.GetAdjustmentOperationsQuery,
  230. Codegen.GetAdjustmentOperationsQueryVariables
  231. >(GET_ADJUSTMENT_OPERATIONS);
  232. expect(result.promotionActions).toMatchSnapshot();
  233. expect(result.promotionConditions).toMatchSnapshot();
  234. });
  235. describe('channels', () => {
  236. const SECOND_CHANNEL_TOKEN = 'SECOND_CHANNEL_TOKEN';
  237. let secondChannel: Codegen.ChannelFragment;
  238. beforeAll(async () => {
  239. const { createChannel } = await adminClient.query<
  240. Codegen.CreateChannelMutation,
  241. Codegen.CreateChannelMutationVariables
  242. >(CREATE_CHANNEL, {
  243. input: {
  244. code: 'second-channel',
  245. token: SECOND_CHANNEL_TOKEN,
  246. defaultLanguageCode: LanguageCode.en,
  247. pricesIncludeTax: true,
  248. currencyCode: CurrencyCode.EUR,
  249. defaultTaxZoneId: 'T_1',
  250. defaultShippingZoneId: 'T_2',
  251. },
  252. });
  253. secondChannel = createChannel as any;
  254. });
  255. it('does not list Promotions not in active channel', async () => {
  256. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  257. const { promotions } = await adminClient.query<Codegen.GetPromotionListQuery>(GET_PROMOTION_LIST);
  258. expect(promotions.totalItems).toBe(0);
  259. expect(promotions.items).toEqual([]);
  260. });
  261. it('does not return Promotion not in active channel', async () => {
  262. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  263. const { promotion: result } = await adminClient.query<
  264. Codegen.GetPromotionQuery,
  265. Codegen.GetPromotionQueryVariables
  266. >(GET_PROMOTION, {
  267. id: promotion.id,
  268. });
  269. expect(result).toBeNull();
  270. });
  271. it('assignPromotionsToChannel', async () => {
  272. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  273. const { assignPromotionsToChannel } = await adminClient.query<
  274. Codegen.AssignPromotionToChannelMutation,
  275. Codegen.AssignPromotionToChannelMutationVariables
  276. >(ASSIGN_PROMOTIONS_TO_CHANNEL, {
  277. input: {
  278. channelId: secondChannel.id,
  279. promotionIds: [promotion.id],
  280. },
  281. });
  282. expect(assignPromotionsToChannel).toEqual([{ id: promotion.id, name: promotion.name }]);
  283. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  284. const { promotion: result } = await adminClient.query<
  285. Codegen.GetPromotionQuery,
  286. Codegen.GetPromotionQueryVariables
  287. >(GET_PROMOTION, {
  288. id: promotion.id,
  289. });
  290. expect(result?.id).toBe(promotion.id);
  291. const { promotions } = await adminClient.query<Codegen.GetPromotionListQuery>(GET_PROMOTION_LIST);
  292. expect(promotions.totalItems).toBe(1);
  293. expect(promotions.items.map(pick(['id']))).toEqual([{ id: promotion.id }]);
  294. });
  295. it('removePromotionsFromChannel', async () => {
  296. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  297. const { removePromotionsFromChannel } = await adminClient.query<
  298. Codegen.RemovePromotionFromChannelMutation,
  299. Codegen.RemovePromotionFromChannelMutationVariables
  300. >(REMOVE_PROMOTIONS_FROM_CHANNEL, {
  301. input: {
  302. channelId: secondChannel.id,
  303. promotionIds: [promotion.id],
  304. },
  305. });
  306. expect(removePromotionsFromChannel).toEqual([{ id: promotion.id, name: promotion.name }]);
  307. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  308. const { promotion: result } = await adminClient.query<
  309. Codegen.GetPromotionQuery,
  310. Codegen.GetPromotionQueryVariables
  311. >(GET_PROMOTION, {
  312. id: promotion.id,
  313. });
  314. expect(result).toBeNull();
  315. const { promotions } = await adminClient.query<Codegen.GetPromotionListQuery>(GET_PROMOTION_LIST);
  316. expect(promotions.totalItems).toBe(0);
  317. });
  318. });
  319. describe('deletion', () => {
  320. let allPromotions: Codegen.GetPromotionListQuery['promotions']['items'];
  321. let promotionToDelete: Codegen.GetPromotionListQuery['promotions']['items'][number];
  322. beforeAll(async () => {
  323. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  324. const result = await adminClient.query<Codegen.GetPromotionListQuery>(GET_PROMOTION_LIST);
  325. allPromotions = result.promotions.items;
  326. });
  327. it('deletes a promotion', async () => {
  328. promotionToDelete = allPromotions[0];
  329. const result = await adminClient.query<
  330. Codegen.DeletePromotionMutation,
  331. Codegen.DeletePromotionMutationVariables
  332. >(DELETE_PROMOTION, { id: promotionToDelete.id });
  333. expect(result.deletePromotion).toEqual({ result: DeletionResult.DELETED });
  334. });
  335. it('cannot get a deleted promotion', async () => {
  336. const result = await adminClient.query<
  337. Codegen.GetPromotionQuery,
  338. Codegen.GetPromotionQueryVariables
  339. >(GET_PROMOTION, {
  340. id: promotionToDelete.id,
  341. });
  342. expect(result.promotion).toBe(null);
  343. });
  344. it('deleted promotion omitted from list', async () => {
  345. const result = await adminClient.query<Codegen.GetPromotionListQuery>(GET_PROMOTION_LIST);
  346. expect(result.promotions.items.length).toBe(allPromotions.length - 1);
  347. expect(result.promotions.items.map(c => c.id).includes(promotionToDelete.id)).toBe(false);
  348. });
  349. it(
  350. 'updatePromotion throws for deleted promotion',
  351. assertThrowsWithMessage(
  352. () =>
  353. adminClient.query<
  354. Codegen.UpdatePromotionMutation,
  355. Codegen.UpdatePromotionMutationVariables
  356. >(UPDATE_PROMOTION, {
  357. input: {
  358. id: promotionToDelete.id,
  359. enabled: false,
  360. },
  361. }),
  362. 'No Promotion with the id "1" could be found',
  363. ),
  364. );
  365. });
  366. });
  367. function generateTestCondition(code: string): PromotionCondition<any> {
  368. return new PromotionCondition({
  369. code,
  370. description: [{ languageCode: LanguageCode.en, value: `description for ${code}` }],
  371. args: { arg: { type: 'int' } },
  372. check: (order, args) => true,
  373. });
  374. }
  375. function generateTestAction(code: string): PromotionAction<any> {
  376. return new PromotionOrderAction({
  377. code,
  378. description: [{ languageCode: LanguageCode.en, value: `description for ${code}` }],
  379. args: { facetValueIds: { type: 'ID', list: true } },
  380. execute: (order, args) => {
  381. return 42;
  382. },
  383. });
  384. }
  385. export const GET_PROMOTION_LIST = gql`
  386. query GetPromotionList($options: PromotionListOptions) {
  387. promotions(options: $options) {
  388. items {
  389. ...Promotion
  390. }
  391. totalItems
  392. }
  393. }
  394. ${PROMOTION_FRAGMENT}
  395. `;
  396. export const GET_PROMOTION = gql`
  397. query GetPromotion($id: ID!) {
  398. promotion(id: $id) {
  399. ...Promotion
  400. }
  401. }
  402. ${PROMOTION_FRAGMENT}
  403. `;
  404. export const UPDATE_PROMOTION = gql`
  405. mutation UpdatePromotion($input: UpdatePromotionInput!) {
  406. updatePromotion(input: $input) {
  407. ...Promotion
  408. ... on ErrorResult {
  409. errorCode
  410. message
  411. }
  412. }
  413. }
  414. ${PROMOTION_FRAGMENT}
  415. `;
  416. export const CONFIGURABLE_DEF_FRAGMENT = gql`
  417. fragment ConfigurableOperationDef on ConfigurableOperationDefinition {
  418. args {
  419. name
  420. type
  421. ui
  422. }
  423. code
  424. description
  425. }
  426. `;
  427. export const GET_ADJUSTMENT_OPERATIONS = gql`
  428. query GetAdjustmentOperations {
  429. promotionActions {
  430. ...ConfigurableOperationDef
  431. }
  432. promotionConditions {
  433. ...ConfigurableOperationDef
  434. }
  435. }
  436. ${CONFIGURABLE_DEF_FRAGMENT}
  437. `;