diff --git a/apps/crm/package.json b/apps/crm/package.json index b5e7506..4d3ea3e 100644 --- a/apps/crm/package.json +++ b/apps/crm/package.json @@ -64,6 +64,7 @@ "eslint-config-next": "13.1.2", "eslint-config-prettier": "^8.6.0", "eslint-config-saleor": "workspace:*", + "node-mocks-http": "^1.12.2", "prettier": "^2.8.2", "typescript": "4.9.4" } diff --git a/apps/crm/src/modules/mailchimp/mailchimp-config-settings-manager.ts b/apps/crm/src/modules/mailchimp/mailchimp-config-settings-manager.ts index 29e3112..c1b85fe 100644 --- a/apps/crm/src/modules/mailchimp/mailchimp-config-settings-manager.ts +++ b/apps/crm/src/modules/mailchimp/mailchimp-config-settings-manager.ts @@ -26,13 +26,17 @@ const MetadataSchemaV1 = z.object({ config: ConfigV1, }); +export interface IMailchimpConfigSettingsManagerV1 { + getConfig(): Promise | null>; +} + /** * V1 config. In case of changing config, create another instance and perform migration * * todo save domain? * todo add test */ -export class MailchimpConfigSettingsManagerV1 { +export class MailchimpConfigSettingsManagerV1 implements IMailchimpConfigSettingsManagerV1 { private settingsManager: SettingsManager; private readonly metadataKey = "mailchimp_config_v1"; private logger = createLogger({ @@ -71,7 +75,7 @@ export class MailchimpConfigSettingsManagerV1 { } async getConfig(): Promise | null> { - this.logger.debug(`Will fetched metadata key: ${this.metadataKey}`); + this.logger.debug(`Will fetch metadata key: ${this.metadataKey}`); const rawMetadata = await this.settingsManager .get(this.metadataKey) .then(this.parseEmptyResponse); @@ -112,3 +116,4 @@ export class MailchimpConfigSettingsManagerV1 { export const MailchimpConfigSettingsManager = MailchimpConfigSettingsManagerV1; export const MailchimpConfig = ConfigV1; +export type MailchimpConfigType = z.infer; diff --git a/apps/crm/src/pages/api/webhooks/customer-created.ts b/apps/crm/src/pages/api/webhooks/customer-created.ts index 339948a..4c946ec 100644 --- a/apps/crm/src/pages/api/webhooks/customer-created.ts +++ b/apps/crm/src/pages/api/webhooks/customer-created.ts @@ -18,7 +18,8 @@ export const customerCreatedWebhook = new SaleorAsyncWebhook = async ( +// todo - fetch metadata with event +export const customerCreatedHandler: NextWebhookApiHandler = async ( req, res, context @@ -62,7 +63,7 @@ const handler: NextWebhookApiHandler = async ( return res.status(200).json({ message: "The event has been handled" }); }; -export default customerCreatedWebhook.createHandler(handler); +export default customerCreatedWebhook.createHandler(customerCreatedHandler); export const config = { api: { diff --git a/apps/crm/src/test/webhooks/customer-created-webhook.test.ts b/apps/crm/src/test/webhooks/customer-created-webhook.test.ts new file mode 100644 index 0000000..c2d1fa7 --- /dev/null +++ b/apps/crm/src/test/webhooks/customer-created-webhook.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it, vi } from "vitest"; +import { createMocks } from "node-mocks-http"; + +import { customerCreatedHandler } from "../../pages/api/webhooks/customer-created"; +import { AuthData } from "@saleor/app-sdk/APL"; + +import mailchimp_marketing from "@mailchimp/mailchimp_marketing"; +import { + IMailchimpConfigSettingsManagerV1, + MailchimpConfigType, +} from "../../modules/mailchimp/mailchimp-config-settings-manager"; + +/** + * Mock settings manager. Consider mocking graphQL api instead + */ +vi.mock("../../modules/mailchimp/mailchimp-config-settings-manager", () => { + class MockManager implements IMailchimpConfigSettingsManagerV1 { + async getConfig(): Promise { + return { + token: "mailchimpToken", + customerCreateEvent: { + enabled: true, + listId: "saleor", + }, + dc: "us41", + }; + } + } + + return { + MailchimpConfigSettingsManager: MockManager, + }; +}); + +/** + * Spy on mailchimp client + */ +mailchimp_marketing.lists.addListMember = vi.fn(); + +const mockAuthData: AuthData = { + saleorApiUrl: "https://demo.saleor.io/graphql/", + domain: "demo.saleor.io", + appId: "XYZ", + token: "token-mocked", +}; + +describe("CUSTOMER_CREATED webhook", () => { + it("Call Mailchimp client to add customer with properly mapped data and tags", async () => { + const { req, res } = createMocks({}); + + await customerCreatedHandler(req, res, { + authData: mockAuthData, + payload: { + user: { + id: "user-id", + email: "someuser@gmail.com", + firstName: "John", + lastName: "Doe", + privateMetadata: [ + { + key: "mailchimp_tags", + value: JSON.stringify(["tag1"]), + }, + ], + }, + }, + event: "CUSTOMER_CREATED", + baseUrl: "localhost:3000", + }); + + return expect(mailchimp_marketing.lists.addListMember).toHaveBeenCalledWith("saleor", { + email_address: "someuser@gmail.com", + merge_fields: { + FNAME: "John", + LNAME: "Doe", + }, + status: "transactional", + tags: ["Saleor Import", "tag1"], + }); + }); + it.todo('Doesnt do anything if configuration "customerCreateEvent" is disabled'); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 679e054..34f3eea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,6 +312,9 @@ importers: eslint-config-saleor: specifier: workspace:* version: link:../../packages/eslint-config-saleor + node-mocks-http: + specifier: ^1.12.2 + version: 1.12.2 prettier: specifier: ^2.8.2 version: 2.8.4 @@ -7507,6 +7510,14 @@ packages: dependencies: event-target-shim: 5.0.1 + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: true + /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: @@ -8610,6 +8621,13 @@ packages: upper-case: 2.0.2 dev: true + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: true + /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -8955,6 +8973,11 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: true + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -10690,6 +10713,11 @@ packages: resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} dev: true + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: true + /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} dev: false @@ -12473,6 +12501,11 @@ packages: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} dev: false + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: true + /memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} dev: false @@ -12497,6 +12530,10 @@ packages: type-fest: 0.13.1 yargs-parser: 18.1.3 + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: true + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -12532,7 +12569,6 @@ packages: /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - dev: false /microinvoice@1.0.6: resolution: {integrity: sha512-mFzikOGHV4tEEuGMZ4ZawcOQvA3NRRxi9RajGuuZKevIWK8Wjayo+DsUq+kERcK6s89sar/8V9TxoAzLELEeHg==} @@ -12744,7 +12780,6 @@ packages: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} hasBin: true - dev: false /mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} @@ -13266,6 +13301,11 @@ packages: /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: true + /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: false @@ -13397,6 +13437,22 @@ packages: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true + /node-mocks-http@1.12.2: + resolution: {integrity: sha512-xhWwC0dh35R9rf0j3bRZXuISXdHxxtMx0ywZQBwjrg3yl7KpRETzogfeCamUIjltpn0Fxvs/ZhGJul1vPLrdJQ==} + engines: {node: '>=0.6'} + dependencies: + accepts: 1.3.8 + content-disposition: 0.5.4 + depd: 1.1.2 + fresh: 0.5.2 + merge-descriptors: 1.0.1 + methods: 1.1.2 + mime: 1.6.0 + parseurl: 1.3.3 + range-parser: 1.2.1 + type-is: 1.6.18 + dev: true + /node-releases@2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} @@ -13813,6 +13869,11 @@ packages: peberminta: 0.8.0 dev: false + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: true + /pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: @@ -14165,6 +14226,11 @@ packages: engines: {node: '>=10'} dev: false + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: true + /raw-body@2.5.1: resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} engines: {node: '>= 0.8'} @@ -15974,6 +16040,14 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: true + /typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} dependencies: