diff --git a/README.md b/README.md index 0c5d113..d038160 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ Quickly integrate Mailtrap with your Node.js app. Currently, with this SDK you can: - Email API/SMTP - - Send an email (Transactional and Bulk streams) + - Send an email - Send an email with a template - - Send a batch of emails (Transactional and Bulk streams) + - Send a batch of emails - Email Sandbox (Testing) - Send an email - Send an email with a template @@ -30,11 +30,12 @@ Currently, with this SDK you can: - Inbox management - Project management - Contact management - - Contacts + - Contact Events - Contact Exports - Contact Fields - Contact Imports - Contact Lists + - Contacts - General - Templates - Suppressions management @@ -116,25 +117,30 @@ See transport usage below: Refer to the [`examples`](examples) folder for the source code of this and other advanced examples. -### Contacts API +### Contact Events API -- [Contacts](examples/contacts/everything.ts) +- [Contact Events](examples/contact-events/everything.ts) -### Contact Lists API +### Contact Exports API -- [Contact Lists](examples/contact-lists/everything.ts) +- [Contact Exports](examples/contact-exports/everything.ts) ### Contact Fields API - [Contact Fields](examples/contact-fields/everything.ts) -### Contact Exports API - -- [Contact Exports](examples/contact-exports/everything.ts) ### Contact Imports API - [Contact Imports](examples/contact-imports/everything.ts) +### Contact Lists API + +- [Contact Lists](examples/contact-lists/everything.ts) + +### Contacts API + +- [Contacts](examples/contacts/everything.ts) + ### Sending API - [Advanced](examples/sending/everything.ts) diff --git a/examples/contact-events/everything.ts b/examples/contact-events/everything.ts new file mode 100644 index 0000000..12f0aef --- /dev/null +++ b/examples/contact-events/everything.ts @@ -0,0 +1,57 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = ""; + +const client = new MailtrapClient({ + token: TOKEN, + accountId: ACCOUNT_ID, +}); + +async function createContactEvent() { + try { + const email = `john.smith+${Date.now()}@example.com`; + let contactId: string; + // Try to get existing contact first + try { + const existing = await client.contacts.get(email); + contactId = existing.data.id; + } catch (_getErr) { + // Not found, create minimal contact + try { + const created = await client.contacts.create({ email }); + contactId = created.data.id; + } catch (err: any) { + const cause = err?.cause || err; + const status = cause?.response?.status; + if (status === 409) { + const existing = await client.contacts.get(email); + contactId = existing.data.id; + } else { + throw err; + } + } + } + + const payload = { + name: "purchase_completed", + params: { + order_id: 12345, + amount: 49.99, + currency: "USD", + coupon_used: false, + }, + }; + + const event = await client.contactEvents.create(contactId, payload); + console.log("Contact event created:", JSON.stringify(event, null, 2)); + } catch (error) { + console.error( + "Error creating contact event:", + error instanceof Error ? error.message : String(error) + ); + } +} + +createContactEvent(); + diff --git a/src/__tests__/lib/api/ContactEvents.test.ts b/src/__tests__/lib/api/ContactEvents.test.ts new file mode 100644 index 0000000..6ae53c7 --- /dev/null +++ b/src/__tests__/lib/api/ContactEvents.test.ts @@ -0,0 +1,17 @@ +import axios, { AxiosInstance } from "axios"; + +import ContactEventsBaseAPI from "../../../lib/api/ContactEvents"; + +describe("lib/api/ContactEvents: ", () => { + const axiosInstance: AxiosInstance = axios.create(); + const accountId = 100; + const api = new ContactEventsBaseAPI(axiosInstance, accountId); + + describe("class ContactEvents(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(api).toHaveProperty("create"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/ContactEvents.test.ts b/src/__tests__/lib/api/resources/ContactEvents.test.ts new file mode 100644 index 0000000..f89615a --- /dev/null +++ b/src/__tests__/lib/api/resources/ContactEvents.test.ts @@ -0,0 +1,106 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import ContactEventsApi from "../../../../lib/api/resources/ContactEvents"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/ContactEvents: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const contactEventsAPI = new ContactEventsApi(axios, accountId); + + const contactIdentifier = "john.smith@example.com"; + const payload = { + name: "purchase_completed", + params: { + order_id: 12345, + amount: 49.99, + currency: "USD", + coupon_used: false, + }, + }; + + const successResponse = { + contact_id: "019a4a05-5924-7d93-86ce-f79293418083", + contact_email: contactIdentifier, + name: payload.name, + params: payload.params, + }; + + describe("class ContactEventsApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(contactEventsAPI).toHaveProperty("create"); + }); + }); + }); + + beforeAll(() => { + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("create(): ", () => { + it("successfully creates a contact event.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactIdentifier}/events`; + const expectedResponseData = successResponse; + + expect.assertions(2); + + mock.onPost(endpoint, payload).reply(200, expectedResponseData); + const result = await contactEventsAPI.create(contactIdentifier, payload); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when accountId is invalid.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactIdentifier}/events`; + const expectedErrorMessage = "Account not found"; + + expect.assertions(2); + + mock.onPost(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactEventsAPI.create(contactIdentifier, payload); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + + it("fails with error when payload is invalid.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactIdentifier}/events`; + const expectedErrorMessage = "Invalid event name"; + + expect.assertions(2); + + mock.onPost(endpoint).reply(422, { error: expectedErrorMessage }); + + try { + await contactEventsAPI.create(contactIdentifier, payload); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index 8d67c18..e28390f 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -15,6 +15,7 @@ import ContactExportsBaseAPI from "../../lib/api/ContactExports"; import TemplatesBaseAPI from "../../lib/api/Templates"; import SuppressionsBaseAPI from "../../lib/api/Suppressions"; import SendingDomainsBaseAPI from "../../lib/api/SendingDomains"; +import ContactEventsBaseAPI from "../../lib/api/ContactEvents"; const { ERRORS, CLIENT_SETTINGS } = CONFIG; const { TESTING_ENDPOINT, BULK_ENDPOINT, SENDING_ENDPOINT } = CLIENT_SETTINGS; @@ -927,5 +928,31 @@ describe("lib/mailtrap-client: ", () => { expect(sendingDomainsClient).toBeInstanceOf(SendingDomainsBaseAPI); }); }); + + describe("get contactEvents(): ", () => { + it("rejects with Mailtrap error, when `accountId` is missing.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + }); + expect.assertions(1); + + try { + client.contactEvents; + } catch (error) { + expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING)); + } + }); + + it("returns contact events API object when accountId is provided.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + accountId: 10, + }); + expect.assertions(1); + + const contactEventsClient = client.contactEvents; + expect(contactEventsClient).toBeInstanceOf(ContactEventsBaseAPI); + }); + }); }); }); diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 39412f1..08bf646 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -8,6 +8,7 @@ import handleSendingError from "./axios-logger"; import MailtrapError from "./MailtrapError"; import ContactsBaseAPI from "./api/Contacts"; +import ContactEventsBaseAPI from "./api/ContactEvents"; import ContactExportsBaseAPI from "./api/ContactExports"; import ContactFieldsBaseAPI from "./api/ContactFields"; import ContactImportsBaseAPI from "./api/ContactImports"; @@ -132,6 +133,14 @@ export default class MailtrapClient { return new ContactsBaseAPI(this.axios, accountId); } + /** + * Getter for Contact Events API. + */ + get contactEvents() { + const accountId = this.validateAccountIdPresence(); + return new ContactEventsBaseAPI(this.axios, accountId); + } + /** * Getter for Contact Exports API. */ diff --git a/src/lib/api/ContactEvents.ts b/src/lib/api/ContactEvents.ts new file mode 100644 index 0000000..047a0e8 --- /dev/null +++ b/src/lib/api/ContactEvents.ts @@ -0,0 +1,12 @@ +import { AxiosInstance } from "axios"; + +import ContactEventsApi from "./resources/ContactEvents"; + +export default class ContactEventsBaseAPI { + public create: ContactEventsApi["create"]; + + constructor(client: AxiosInstance, accountId: number) { + const contactEvents = new ContactEventsApi(client, accountId); + this.create = contactEvents.create.bind(contactEvents); + } +} diff --git a/src/lib/api/resources/ContactEvents.ts b/src/lib/api/resources/ContactEvents.ts new file mode 100644 index 0000000..dcea29a --- /dev/null +++ b/src/lib/api/resources/ContactEvents.ts @@ -0,0 +1,36 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + ContactEventOptions, + ContactEventResponse, +} from "../../../types/api/contact-events"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ContactEventsApi { + private client: AxiosInstance; + + private contactsURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.contactsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts`; + } + + /** + * Creates a new contact event for given contact identifier and data. + */ + public async create( + contactIdentifier: number | string, + data: ContactEventOptions + ) { + const url = `${this.contactsURL}/${contactIdentifier}/events`; + + return this.client.post( + url, + data + ); + } +} diff --git a/src/types/api/contact-events.ts b/src/types/api/contact-events.ts new file mode 100644 index 0000000..ed5ec02 --- /dev/null +++ b/src/types/api/contact-events.ts @@ -0,0 +1,11 @@ +export interface ContactEventOptions { + name: string; + params: Record; +} + +export interface ContactEventResponse { + contact_id: string; + contact_email: string; + name: string; + params: Record; +}