From a377542348504bda4e46bf57dde295eeac9de355 Mon Sep 17 00:00:00 2001 From: Nao Ohira Date: Wed, 10 Jan 2024 10:59:25 +0100 Subject: [PATCH 1/6] feat(provider): add brevo-sms provider --- .../static/images/providers/dark/brevo.svg | 1 + .../providers/dark/square/brevo-sms.svg | 5 + .../static/images/providers/light/brevo.svg | 1 + .../providers/light/square/brevo-sms.svg | 5 + .../src/consts/providers/channels/sms.ts | 9 + .../credentials/provider-credentials.ts | 10 + .../src/consts/providers/provider.enum.ts | 1 + packages/application-generic/package.json | 1 + .../sms/handlers/brevo-sms.handler.ts | 21 ++ .../src/factories/sms/handlers/index.ts | 1 + .../src/factories/sms/sms.factory.ts | 2 + pnpm-lock.yaml | 97 ++++++++-- providers/brevo-sms/.czrc | 3 + providers/brevo-sms/.eslintrc.json | 3 + providers/brevo-sms/.gitignore | 9 + providers/brevo-sms/README.md | 9 + providers/brevo-sms/jest.config.js | 5 + providers/brevo-sms/package.json | 80 ++++++++ providers/brevo-sms/src/index.ts | 1 + .../src/lib/brevo-sms.provider.spec.ts | 180 ++++++++++++++++++ .../brevo-sms/src/lib/brevo-sms.provider.ts | 56 ++++++ providers/brevo-sms/src/lib/dateIsValid.ts | 29 +++ providers/brevo-sms/src/lib/objectToEqual.ts | 68 +++++++ providers/brevo-sms/tsconfig.json | 10 + providers/brevo-sms/tsconfig.module.json | 9 + 25 files changed, 597 insertions(+), 19 deletions(-) create mode 100644 apps/web/public/static/images/providers/dark/brevo.svg create mode 100644 apps/web/public/static/images/providers/dark/square/brevo-sms.svg create mode 100644 apps/web/public/static/images/providers/light/brevo.svg create mode 100644 apps/web/public/static/images/providers/light/square/brevo-sms.svg create mode 100644 packages/application-generic/src/factories/sms/handlers/brevo-sms.handler.ts create mode 100644 providers/brevo-sms/.czrc create mode 100644 providers/brevo-sms/.eslintrc.json create mode 100644 providers/brevo-sms/.gitignore create mode 100644 providers/brevo-sms/README.md create mode 100644 providers/brevo-sms/jest.config.js create mode 100644 providers/brevo-sms/package.json create mode 100644 providers/brevo-sms/src/index.ts create mode 100644 providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts create mode 100644 providers/brevo-sms/src/lib/brevo-sms.provider.ts create mode 100644 providers/brevo-sms/src/lib/dateIsValid.ts create mode 100644 providers/brevo-sms/src/lib/objectToEqual.ts create mode 100644 providers/brevo-sms/tsconfig.json create mode 100644 providers/brevo-sms/tsconfig.module.json diff --git a/apps/web/public/static/images/providers/dark/brevo.svg b/apps/web/public/static/images/providers/dark/brevo.svg new file mode 100644 index 00000000000..64d836f0da2 --- /dev/null +++ b/apps/web/public/static/images/providers/dark/brevo.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/static/images/providers/dark/square/brevo-sms.svg b/apps/web/public/static/images/providers/dark/square/brevo-sms.svg new file mode 100644 index 00000000000..52f635ddb41 --- /dev/null +++ b/apps/web/public/static/images/providers/dark/square/brevo-sms.svg @@ -0,0 +1,5 @@ + + + + diff --git a/apps/web/public/static/images/providers/light/brevo.svg b/apps/web/public/static/images/providers/light/brevo.svg new file mode 100644 index 00000000000..64d836f0da2 --- /dev/null +++ b/apps/web/public/static/images/providers/light/brevo.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/static/images/providers/light/square/brevo-sms.svg b/apps/web/public/static/images/providers/light/square/brevo-sms.svg new file mode 100644 index 00000000000..8fb0f024860 --- /dev/null +++ b/apps/web/public/static/images/providers/light/square/brevo-sms.svg @@ -0,0 +1,5 @@ + + + + diff --git a/libs/shared/src/consts/providers/channels/sms.ts b/libs/shared/src/consts/providers/channels/sms.ts index dee21414b48..3cf62e2f79e 100644 --- a/libs/shared/src/consts/providers/channels/sms.ts +++ b/libs/shared/src/consts/providers/channels/sms.ts @@ -26,6 +26,7 @@ import { azureSmsConfig, bulkSmsConfig, iSendSmsConfig, + brevoSmsConfig, } from '../credentials'; import { SmsProviderIdEnum } from '../provider.enum'; @@ -252,4 +253,12 @@ export const smsProviders: IProviderConfig[] = [ docReference: 'https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/sms/receive-sms', logoFileName: { light: 'azure-sms.png', dark: 'azure-sms.png' }, }, + { + id: SmsProviderIdEnum.BrevoSms, + displayName: `Brevo`, + channel: ChannelTypeEnum.SMS, + credentials: brevoSmsConfig, + docReference: 'https://developers.brevo.com/reference/sendtransacsms', + logoFileName: { light: 'brevo.svg', dark: 'brevo.svg' }, + }, ]; diff --git a/libs/shared/src/consts/providers/credentials/provider-credentials.ts b/libs/shared/src/consts/providers/credentials/provider-credentials.ts index 47f76d81be4..55721597872 100644 --- a/libs/shared/src/consts/providers/credentials/provider-credentials.ts +++ b/libs/shared/src/consts/providers/credentials/provider-credentials.ts @@ -1063,3 +1063,13 @@ export const rocketChatConfig: IConfigCredentials[] = [ required: true, }, ]; + +export const brevoSmsConfig: IConfigCredentials[] = [ + { + key: CredentialsKeyEnum.ApiKey, + displayName: 'API Key', + type: 'string', + required: true, + }, + ...smsConfigBase, +]; diff --git a/libs/shared/src/consts/providers/provider.enum.ts b/libs/shared/src/consts/providers/provider.enum.ts index a83d61911ab..af08a38e07f 100644 --- a/libs/shared/src/consts/providers/provider.enum.ts +++ b/libs/shared/src/consts/providers/provider.enum.ts @@ -98,6 +98,7 @@ export enum SmsProviderIdEnum { MessageBird = 'messagebird', Simpletexting = 'simpletexting', AzureSms = 'azure-sms', + BrevoSms = 'brevo-sms', } export enum ChatProviderIdEnum { diff --git a/packages/application-generic/package.json b/packages/application-generic/package.json index acc3463ba42..7bda703ce0f 100644 --- a/packages/application-generic/package.json +++ b/packages/application-generic/package.json @@ -114,6 +114,7 @@ "@novu/twilio": "^0.22.0", "@novu/zulip": "^0.22.0", "@novu/nexmo": "^0.22.0", + "@novu/brevo-sms": "^0.22.0", "@novu/rocket-chat": "^0.22.0", "@opentelemetry/api": "^1.7.0", "@opentelemetry/auto-instrumentations-node": "^0.40.2", diff --git a/packages/application-generic/src/factories/sms/handlers/brevo-sms.handler.ts b/packages/application-generic/src/factories/sms/handlers/brevo-sms.handler.ts new file mode 100644 index 00000000000..2015b6c3f20 --- /dev/null +++ b/packages/application-generic/src/factories/sms/handlers/brevo-sms.handler.ts @@ -0,0 +1,21 @@ +import { ChannelTypeEnum, ICredentials } from '@novu/shared'; +import { BaseSmsHandler } from './base.handler'; +import { BrevoSmsProvider } from '@novu/brevo-sms'; + +export class BrevoSmsHandler extends BaseSmsHandler { + constructor() { + super('brevo-sms', ChannelTypeEnum.SMS); + } + buildProvider(credentials: ICredentials) { + if (!credentials.apiKey || !credentials.from) { + throw Error('Invalid credentials'); + } + + const config = { + apiKey: credentials.apiKey, + from: credentials.from, + }; + + this.provider = new BrevoSmsProvider(config); + } +} diff --git a/packages/application-generic/src/factories/sms/handlers/index.ts b/packages/application-generic/src/factories/sms/handlers/index.ts index 94cb4a4273f..b217cc1606e 100644 --- a/packages/application-generic/src/factories/sms/handlers/index.ts +++ b/packages/application-generic/src/factories/sms/handlers/index.ts @@ -25,3 +25,4 @@ export * from './azure-sms.handler'; export * from './bulk-sms.handler'; export * from './nexmo.handler'; export * from './isend-sms.handler'; +export * from './brevo-sms.handler'; diff --git a/packages/application-generic/src/factories/sms/sms.factory.ts b/packages/application-generic/src/factories/sms/sms.factory.ts index f0107378b47..b2cd7d8a7b6 100644 --- a/packages/application-generic/src/factories/sms/sms.factory.ts +++ b/packages/application-generic/src/factories/sms/sms.factory.ts @@ -27,6 +27,7 @@ import { NovuSmsHandler, NexmoHandler, ISendSmsHandler, + BrevoSmsHandler, } from './handlers'; export class SmsFactory implements ISmsFactory { @@ -57,6 +58,7 @@ export class SmsFactory implements ISmsFactory { new NovuSmsHandler(), new NexmoHandler(), new ISendSmsHandler(), + new BrevoSmsHandler(), ]; getHandler(integration: IntegrationEntity) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e920a211967..6dfecf5db57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2501,6 +2501,9 @@ importers: '@novu/braze': specifier: ^0.22.0 version: link:../../providers/braze + '@novu/brevo-sms': + specifier: ^0.22.0 + version: link:../../providers/brevo-sms '@novu/bulk-sms': specifier: ^0.22.0 version: link:../../providers/bulk-sms @@ -3739,6 +3742,55 @@ importers: specifier: 4.9.5 version: 4.9.5 + providers/brevo-sms: + dependencies: + '@novu/stateless': + specifier: 0.16.3 + version: 0.16.3 + cross-fetch: + specifier: ^4.0.0 + version: 4.0.0 + proxy-agent: + specifier: ^6.3.0 + version: 6.3.0 + devDependencies: + '@istanbuljs/nyc-config-typescript': + specifier: ~1.0.1 + version: 1.0.2(nyc@15.1.0) + '@types/jest': + specifier: ~27.5.2 + version: 27.5.2 + cspell: + specifier: ~6.19.2 + version: 6.19.2 + jest: + specifier: ~27.5.1 + version: 27.5.1(ts-node@10.9.1) + jest-fetch-mock: + specifier: ^3.0.3 + version: 3.0.3 + npm-run-all: + specifier: ^4.1.5 + version: 4.1.5 + nyc: + specifier: ~15.1.0 + version: 15.1.0 + prettier: + specifier: ~2.8.0 + version: 2.8.7 + rimraf: + specifier: ~3.0.2 + version: 3.0.2 + ts-jest: + specifier: ~27.1.5 + version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + ts-node: + specifier: ~10.9.1 + version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) + typescript: + specifier: 4.9.5 + version: 4.9.5 + providers/bulk-sms: dependencies: '@novu/stateless': @@ -8852,8 +8904,8 @@ packages: '@babel/code-frame': 7.22.13 '@babel/generator': 7.23.0 '@babel/helper-compilation-targets': 7.22.15 - '@babel/helper-module-transforms': 7.22.20(@babel/core@7.22.11) - '@babel/helpers': 7.22.11 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.22.11) + '@babel/helpers': 7.23.2 '@babel/parser': 7.23.0 '@babel/template': 7.22.15 '@babel/traverse': 7.23.2 @@ -9173,20 +9225,6 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true - /@babel/helper-module-transforms@7.22.20(@babel/core@7.22.11): - resolution: {integrity: sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.11 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 - dev: true - /@babel/helper-module-transforms@7.22.20(@babel/core@7.23.2): resolution: {integrity: sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==} engines: {node: '>=6.9.0'} @@ -26268,7 +26306,7 @@ packages: /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: - '@babel/parser': 7.22.16 + '@babel/parser': 7.23.0 '@babel/types': 7.23.0 /@types/babel__traverse@7.18.3: @@ -30017,7 +30055,7 @@ packages: dependencies: '@babel/template': 7.22.15 '@babel/types': 7.23.0 - '@types/babel__core': 7.20.0 + '@types/babel__core': 7.20.3 '@types/babel__traverse': 7.18.3 dev: true @@ -32242,6 +32280,13 @@ packages: node-fetch: 2.6.7 transitivePeerDependencies: - encoding + + /cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding dev: false /cross-spawn@6.0.5: @@ -33674,6 +33719,7 @@ packages: /domexception@2.0.1: resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} engines: {node: '>=8'} + deprecated: Use your platform's native DOMException instead dependencies: webidl-conversions: 5.0.0 dev: true @@ -39396,6 +39442,15 @@ packages: jest-util: 29.5.0 dev: true + /jest-fetch-mock@3.0.3: + resolution: {integrity: sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==} + dependencies: + cross-fetch: 3.1.5 + promise-polyfill: 8.3.0 + transitivePeerDependencies: + - encoding + dev: true + /jest-get-type@24.9.0: resolution: {integrity: sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==} engines: {node: '>= 6'} @@ -40181,7 +40236,7 @@ packages: '@babel/preset-env': ^7.1.6 dependencies: '@babel/core': 7.23.2 - '@babel/parser': 7.22.16 + '@babel/parser': 7.23.0 '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.23.2) '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.23.2) '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.23.2) @@ -46383,6 +46438,10 @@ packages: resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} dev: false + /promise-polyfill@8.3.0: + resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==} + dev: true + /promise-retry@2.0.1: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} diff --git a/providers/brevo-sms/.czrc b/providers/brevo-sms/.czrc new file mode 100644 index 00000000000..d1bcc209ca1 --- /dev/null +++ b/providers/brevo-sms/.czrc @@ -0,0 +1,3 @@ +{ + "path": "cz-conventional-changelog" +} diff --git a/providers/brevo-sms/.eslintrc.json b/providers/brevo-sms/.eslintrc.json new file mode 100644 index 00000000000..ec40100be69 --- /dev/null +++ b/providers/brevo-sms/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.js" +} diff --git a/providers/brevo-sms/.gitignore b/providers/brevo-sms/.gitignore new file mode 100644 index 00000000000..963d5292865 --- /dev/null +++ b/providers/brevo-sms/.gitignore @@ -0,0 +1,9 @@ +.idea/* +.nyc_output +build +node_modules +test +src/**.js +coverage +*.log +package-lock.json diff --git a/providers/brevo-sms/README.md b/providers/brevo-sms/README.md new file mode 100644 index 00000000000..b8eb435f2d8 --- /dev/null +++ b/providers/brevo-sms/README.md @@ -0,0 +1,9 @@ +# Novu BrevoSms Provider + +A BrevoSms sms provider library for [@novu/node](https://github.com/novuhq/novu) + +## Usage + +```javascript + FILL IN THE INITIALIZATION USAGE +``` diff --git a/providers/brevo-sms/jest.config.js b/providers/brevo-sms/jest.config.js new file mode 100644 index 00000000000..e86e13bab91 --- /dev/null +++ b/providers/brevo-sms/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/providers/brevo-sms/package.json b/providers/brevo-sms/package.json new file mode 100644 index 00000000000..c96782c8711 --- /dev/null +++ b/providers/brevo-sms/package.json @@ -0,0 +1,80 @@ +{ + "name": "@novu/brevo-sms", + "version": "0.22.0", + "description": "A brevo-sms wrapper for novu", + "main": "build/main/index.js", + "typings": "build/main/index.d.ts", + "module": "build/module/index.js", + "private": false, + "repository": "https://github.com/novuhq/novu", + "license": "MIT", + "keywords": [], + "scripts": { + "prebuild": "rimraf build", + "build": "run-p build:*", + "build:main": "tsc -p tsconfig.json", + "build:module": "tsc -p tsconfig.module.json", + "fix": "run-s fix:*", + "fix:prettier": "prettier \"src/**/*.ts\" --write", + "fix:lint": "eslint src --ext .ts --fix", + "test": "run-s test:*", + "lint": "eslint src --ext .ts", + "test:unit": "jest src", + "watch:build": "tsc -p tsconfig.json -w", + "watch:test": "jest src --watch", + "reset-hard": "git clean -dfx && git reset --hard && yarn", + "prepare-release": "run-s reset-hard test" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@novu/stateless": "0.16.3", + "cross-fetch": "^4.0.0", + "proxy-agent": "^6.3.1" + }, + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "~1.0.1", + "jest-fetch-mock": "^3.0.3", + "@types/jest": "~27.5.2", + "cspell": "~6.19.2", + "jest": "~27.5.1", + "npm-run-all": "^4.1.5", + "nyc": "~15.1.0", + "prettier": "~2.8.0", + "rimraf": "~3.0.2", + "ts-jest": "~27.1.5", + "ts-node": "~10.9.1", + "typescript": "4.9.5" + }, + "files": [ + "build/main", + "build/module", + "!**/*.spec.*", + "!**/*.json", + "CHANGELOG.md", + "LICENSE", + "README.md" + ], + "ava": { + "failFast": true, + "timeout": "60s", + "typescript": { + "rewritePaths": { + "src/": "build/main/" + } + }, + "files": [ + "!build/module/**" + ] + }, + "prettier": { + "singleQuote": true + }, + "nyc": { + "extends": "@istanbuljs/nyc-config-typescript", + "exclude": [ + "**/*.spec.js" + ] + } +} diff --git a/providers/brevo-sms/src/index.ts b/providers/brevo-sms/src/index.ts new file mode 100644 index 00000000000..8937fd642bf --- /dev/null +++ b/providers/brevo-sms/src/index.ts @@ -0,0 +1 @@ +export * from './lib/brevo-sms.provider'; diff --git a/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts b/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts new file mode 100644 index 00000000000..6479ef8b985 --- /dev/null +++ b/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts @@ -0,0 +1,180 @@ +import fetch from 'jest-fetch-mock'; +import { BrevoSmsProvider } from './brevo-sms.provider'; +import { ISmsOptions, SmsEventStatusEnum } from '@novu/stateless'; +import { objectToEqual } from './objectToEqual'; +import { dateIsValid } from './dateIsValid'; + +const mockConfig = { + apiKey: + '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', + from: 'Valophis', +}; + +fetch.enableMocks(); + +expect.extend({ + objectToEqual, + dateIsValid, +}); + +const mockNovuMessage: ISmsOptions = { + from: 'Valophis', + to: '+33623456789', + content: 'SMS content', +}; + +const mockBrevoResponse = { + reference: 'ab1cde2fgh3i4jklmno', + messageId: 1511882900176220, + smsCount: 2, + usedCredits: 0.7, + remainingCredits: 82.85, +}; + +beforeEach(() => { + fetch.doMock(); +}); + +afterEach(() => { + fetch.resetMocks(); +}); + +describe('sendMessage method', () => { + test('should call brevo API transactional sms endpoint once', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + await provider.sendMessage(mockNovuMessage); + + expect(fetch).toBeCalled(); + }); + + test('should call brevo API transactional sms endpoint with right URL', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + await provider.sendMessage(mockNovuMessage); + + expect(fetch.mock.calls[0][0]).toEqual( + 'https://api.brevo.com/v3/transactionalSMS/sms' + ); + }); + + test('should call brevo API transactional sms endpoint using POST method', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + await provider.sendMessage(mockNovuMessage); + + expect(fetch.mock.calls[0][1]).toMatchObject({ + method: 'POST', + }); + }); + + test('should call brevo API using config apiKey', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + await provider.sendMessage(mockNovuMessage); + + expect(fetch.mock.calls[0][1]).toMatchObject({ + headers: { + 'api-key': mockConfig.apiKey, + }, + }); + }); + + test('should send message with provided config from', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + const { from, ...mockNovuMessageWithoutFrom } = mockNovuMessage; + + await provider.sendMessage(mockNovuMessageWithoutFrom); + + expect(fetch.mock.calls[0][1]).toMatchObject({ + body: expect.objectToEqual('sender', mockConfig.from), + }); + }); + + test('should send message with provided option from overriding config from', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + await provider.sendMessage(mockNovuMessage); + + expect(fetch.mock.calls[0][1]).toMatchObject({ + body: expect.objectToEqual('sender', mockNovuMessage.from), + }); + }); + test('should send message with provided option to', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + await provider.sendMessage(mockNovuMessage); + + expect(fetch.mock.calls[0][1]).toMatchObject({ + body: expect.objectToEqual('recipient', mockNovuMessage.to), + }); + }); + test('should send message with provided option content', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + await provider.sendMessage(mockNovuMessage); + + expect(fetch.mock.calls[0][1]).toMatchObject({ + body: expect.objectToEqual('content', mockNovuMessage.content), + }); + }); + test('should return id returned in request response', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + const result = await provider.sendMessage(mockNovuMessage); + + expect(result).toMatchObject({ + id: mockBrevoResponse.messageId, + }); + }); + test('should return date returned in request response', async () => { + const provider = new BrevoSmsProvider(mockConfig); + + fetch.mockResponseOnce(JSON.stringify(mockBrevoResponse), { + status: 201, + }); + + const result = await provider.sendMessage(mockNovuMessage); + + expect(result).toMatchObject({ + date: expect.dateIsValid(), + }); + }); +}); diff --git a/providers/brevo-sms/src/lib/brevo-sms.provider.ts b/providers/brevo-sms/src/lib/brevo-sms.provider.ts new file mode 100644 index 00000000000..85131ab844b --- /dev/null +++ b/providers/brevo-sms/src/lib/brevo-sms.provider.ts @@ -0,0 +1,56 @@ +import { + ChannelTypeEnum, + ISendMessageSuccessResponse, + ISmsOptions, + ISmsProvider, +} from '@novu/stateless'; +import { ProxyAgent } from 'proxy-agent'; +import 'cross-fetch'; + +declare global { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface RequestInit { + agent: ProxyAgent; + } +} + +export class BrevoSmsProvider implements ISmsProvider { + id = 'brevo-sms'; + channelType = ChannelTypeEnum.SMS as ChannelTypeEnum.SMS; + public readonly BASE_URL = 'https://api.brevo.com/v3'; + + constructor( + private config: { + apiKey: string; + from: string; + } + ) {} + + async sendMessage( + options: ISmsOptions + ): Promise { + const sms = { + sender: options.from || this.config.from, + recipient: options.to, + content: options.content, + }; + + const response = await fetch(`${this.BASE_URL}/transactionalSMS/sms`, { + method: 'POST', + headers: { + 'api-key': this.config.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + agent: new ProxyAgent(), + body: JSON.stringify(sms), + }); + + const body: { messageId: string } = await response.json(); + + return { + id: body.messageId, + date: new Date().toISOString(), + }; + } +} diff --git a/providers/brevo-sms/src/lib/dateIsValid.ts b/providers/brevo-sms/src/lib/dateIsValid.ts new file mode 100644 index 00000000000..347c57162a9 --- /dev/null +++ b/providers/brevo-sms/src/lib/dateIsValid.ts @@ -0,0 +1,29 @@ +interface ICustomMatchers { + dateIsValid(): R; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-empty-interface + interface Expect extends ICustomMatchers {} + + // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-empty-interface + interface Matchers extends ICustomMatchers {} + + // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-empty-interface + interface InverseAsymmetricMatchers extends ICustomMatchers {} + } +} + +export function dateIsValid(actual: string | object) { + if (typeof actual !== 'string') { + throw new TypeError('Actual value must be a date in iso format'); + } + + return { + message: () => + `expected ${this.utils.printReceived(actual)} to be a valid string date`, + pass: !isNaN(+new Date(actual)), + }; +} diff --git a/providers/brevo-sms/src/lib/objectToEqual.ts b/providers/brevo-sms/src/lib/objectToEqual.ts new file mode 100644 index 00000000000..002ff379fff --- /dev/null +++ b/providers/brevo-sms/src/lib/objectToEqual.ts @@ -0,0 +1,68 @@ +interface ICustomMatchers { + objectToEqual(key: string, value: unknown): R; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-empty-interface + interface Expect extends ICustomMatchers {} + + // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-empty-interface + interface Matchers extends ICustomMatchers {} + + // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-empty-interface + interface InverseAsymmetricMatchers extends ICustomMatchers {} + } +} + +export function objectToEqual( + actual: string | object, + key: string, + value: unknown +) { + if (typeof actual !== 'string' && typeof actual !== 'object') { + throw new TypeError('Actual value must be a stringified object or object'); + } else if (typeof key !== 'string') { + throw new TypeError( + 'Key must be a string (optionally with . for sub keys)' + ); + } + + let currentObject = typeof actual === 'string' ? JSON.parse(actual) : actual; + const subKeys = key.split('.'); + const finalKey = subKeys.pop(); + + const invalidKeyMessageFactory = (object: object, subKey: string) => () => + `expected ${this.utils.printReceived( + object + )} to be an object with key ${subKey}`; + + for (const subKey of subKeys) { + if (typeof currentObject[subKey] !== 'object') { + return { + message: invalidKeyMessageFactory(currentObject, subKey), + pass: false, + }; + } + currentObject = currentObject[subKey]; + } + + if (currentObject[finalKey] !== value) { + return { + message: () => + `expected ${this.utils.printReceived( + currentObject + )} to have attribute ${key} with value ${String(value)}`, + pass: false, + }; + } + + return { + message: () => + `expected ${this.utils.printReceived( + currentObject + )} to have attribute ${key} with value ${String(value)}`, + pass: true, + }; +} diff --git a/providers/brevo-sms/tsconfig.json b/providers/brevo-sms/tsconfig.json new file mode 100644 index 00000000000..5b8120fea36 --- /dev/null +++ b/providers/brevo-sms/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build/main", + "rootDir": "src", + "types": ["node", "jest"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules/**"] +} diff --git a/providers/brevo-sms/tsconfig.module.json b/providers/brevo-sms/tsconfig.module.json new file mode 100644 index 00000000000..79be3a5c40b --- /dev/null +++ b/providers/brevo-sms/tsconfig.module.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "target": "esnext", + "outDir": "build/module", + "module": "esnext" + }, + "exclude": ["node_modules/**"] +} From 2a4d5390e20199e0d4e715b7f50e44b32144d438 Mon Sep 17 00:00:00 2001 From: Nao Ohira Date: Wed, 10 Jan 2024 17:12:31 +0100 Subject: [PATCH 2/6] docs(provider): add brevo sms provider usage instruction --- providers/brevo-sms/README.md | 14 ++++++++++++-- .../brevo-sms/src/lib/brevo-sms.provider.spec.ts | 7 +++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/providers/brevo-sms/README.md b/providers/brevo-sms/README.md index b8eb435f2d8..0579c0c983b 100644 --- a/providers/brevo-sms/README.md +++ b/providers/brevo-sms/README.md @@ -1,9 +1,19 @@ # Novu BrevoSms Provider -A BrevoSms sms provider library for [@novu/node](https://github.com/novuhq/novu) +A BrevoSms sms provider library for [@novu/stateless](https://github.com/novuhq/novu) ## Usage ```javascript - FILL IN THE INITIALIZATION USAGE +import { BrevoSmsProvider } from '@novu/brevo-sms'; + +const provider = new BrevoSmsProvider({ + apiKey: process.env.BREVO_API_KEY, + from: process.env.BREVO_FROM, // Sender displayed to the recipient +}); + +await provider.sendMessage({ + to: 'My Company', + content: 'Message to send', +}); ``` diff --git a/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts b/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts index 6479ef8b985..b53149a1dc8 100644 --- a/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts +++ b/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts @@ -5,9 +5,8 @@ import { objectToEqual } from './objectToEqual'; import { dateIsValid } from './dateIsValid'; const mockConfig = { - apiKey: - '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', - from: 'Valophis', + apiKey: 'ABCDE', + from: 'My Company', }; fetch.enableMocks(); @@ -18,7 +17,7 @@ expect.extend({ }); const mockNovuMessage: ISmsOptions = { - from: 'Valophis', + from: 'My Company', to: '+33623456789', content: 'SMS content', }; From 42f04dbdff8ef34130fce23dfb9b498b913a8e5d Mon Sep 17 00:00:00 2001 From: Nao Ohira Date: Tue, 30 Jan 2024 08:28:06 +0100 Subject: [PATCH 3/6] fix(provider): Update dependency version Co-authored-by: Biswajeet Das --- providers/brevo-sms/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/brevo-sms/package.json b/providers/brevo-sms/package.json index c96782c8711..8f10796c9e6 100644 --- a/providers/brevo-sms/package.json +++ b/providers/brevo-sms/package.json @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "0.16.3", + "@novu/stateless": "0.22.0", "cross-fetch": "^4.0.0", "proxy-agent": "^6.3.1" }, From 3f226f8e3bba27a9af76562657443d6eeeceef2b Mon Sep 17 00:00:00 2001 From: Nao Ohira Date: Tue, 30 Jan 2024 08:44:42 +0100 Subject: [PATCH 4/6] fix(provider): add brevo-sms provider to sort-providers file --- .../integrations/components/multi-provider/sort-providers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts b/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts index 0aa914f9839..3466371c686 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts +++ b/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts @@ -73,6 +73,7 @@ const providers: Record = { SmsProviderIdEnum.AzureSms, SmsProviderIdEnum.Bandwidth, SmsProviderIdEnum.Simpletexting, + SmsProviderIdEnum.BrevoSms, ].sort(), ], }; From 55706582ef7b01216e416df6dbcc821da02221aa Mon Sep 17 00:00:00 2001 From: Nao Ohira Date: Tue, 30 Jan 2024 09:27:58 +0100 Subject: [PATCH 5/6] fix(provider): fix spelling in brevo-sms provider test --- providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts b/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts index b53149a1dc8..22af6083142 100644 --- a/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts +++ b/providers/brevo-sms/src/lib/brevo-sms.provider.spec.ts @@ -23,7 +23,7 @@ const mockNovuMessage: ISmsOptions = { }; const mockBrevoResponse = { - reference: 'ab1cde2fgh3i4jklmno', + reference: 'brevo-reference', messageId: 1511882900176220, smsCount: 2, usedCredits: 0.7, From 30e008502c144aa8a9064027ea22390e9812b3ca Mon Sep 17 00:00:00 2001 From: Nao Ohira Date: Tue, 30 Jan 2024 09:41:15 +0100 Subject: [PATCH 6/6] fix(provider): fix lockfile after dependency update --- pnpm-lock.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dfecf5db57..cc5863fbcff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3745,8 +3745,8 @@ importers: providers/brevo-sms: dependencies: '@novu/stateless': - specifier: 0.16.3 - version: 0.16.3 + specifier: 0.22.0 + version: link:../../packages/stateless cross-fetch: specifier: ^4.0.0 version: 4.0.0 @@ -48991,7 +48991,7 @@ packages: jest-worker: 26.6.2 rollup: 2.79.1 serialize-javascript: 4.0.0 - terser: 5.16.9 + terser: 5.22.0 dev: true /rollup-plugin-terser@7.0.2(rollup@3.20.2):