promotion.e2e-spec.ts 17 KB

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