Skip to content

Commit 0fa2b59

Browse files
Merge pull request #99 from mailtrap/create-contact-event
Create contact event
2 parents 626e994 + ada5480 commit 0fa2b59

File tree

9 files changed

+291
-10
lines changed

9 files changed

+291
-10
lines changed

README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,22 @@ Quickly integrate Mailtrap with your Node.js app.
2020

2121
Currently, with this SDK you can:
2222
- Email API/SMTP
23-
- Send an email (Transactional and Bulk streams)
23+
- Send an email
2424
- Send an email with a template
25-
- Send a batch of emails (Transactional and Bulk streams)
25+
- Send a batch of emails
2626
- Email Sandbox (Testing)
2727
- Send an email
2828
- Send an email with a template
2929
- Message management
3030
- Inbox management
3131
- Project management
3232
- Contact management
33-
- Contacts
33+
- Contact Events
3434
- Contact Exports
3535
- Contact Fields
3636
- Contact Imports
3737
- Contact Lists
38+
- Contacts
3839
- General
3940
- Templates
4041
- Suppressions management
@@ -116,25 +117,30 @@ See transport usage below:
116117

117118
Refer to the [`examples`](examples) folder for the source code of this and other advanced examples.
118119

119-
### Contacts API
120+
### Contact Events API
120121

121-
- [Contacts](examples/contacts/everything.ts)
122+
- [Contact Events](examples/contact-events/everything.ts)
122123

123-
### Contact Lists API
124+
### Contact Exports API
124125

125-
- [Contact Lists](examples/contact-lists/everything.ts)
126+
- [Contact Exports](examples/contact-exports/everything.ts)
126127

127128
### Contact Fields API
128129

129130
- [Contact Fields](examples/contact-fields/everything.ts)
130131

131-
### Contact Exports API
132-
133-
- [Contact Exports](examples/contact-exports/everything.ts)
134132
### Contact Imports API
135133

136134
- [Contact Imports](examples/contact-imports/everything.ts)
137135

136+
### Contact Lists API
137+
138+
- [Contact Lists](examples/contact-lists/everything.ts)
139+
140+
### Contacts API
141+
142+
- [Contacts](examples/contacts/everything.ts)
143+
138144
### Sending API
139145

140146
- [Advanced](examples/sending/everything.ts)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { MailtrapClient } from "mailtrap";
2+
3+
const TOKEN = "<YOUR-TOKEN-HERE>";
4+
const ACCOUNT_ID = "<YOUR-ACCOUNT-ID-HERE>";
5+
6+
const client = new MailtrapClient({
7+
token: TOKEN,
8+
accountId: ACCOUNT_ID,
9+
});
10+
11+
async function createContactEvent() {
12+
try {
13+
const email = `john.smith+${Date.now()}@example.com`;
14+
let contactId: string;
15+
// Try to get existing contact first
16+
try {
17+
const existing = await client.contacts.get(email);
18+
contactId = existing.data.id;
19+
} catch (_getErr) {
20+
// Not found, create minimal contact
21+
try {
22+
const created = await client.contacts.create({ email });
23+
contactId = created.data.id;
24+
} catch (err: any) {
25+
const cause = err?.cause || err;
26+
const status = cause?.response?.status;
27+
if (status === 409) {
28+
const existing = await client.contacts.get(email);
29+
contactId = existing.data.id;
30+
} else {
31+
throw err;
32+
}
33+
}
34+
}
35+
36+
const payload = {
37+
name: "purchase_completed",
38+
params: {
39+
order_id: 12345,
40+
amount: 49.99,
41+
currency: "USD",
42+
coupon_used: false,
43+
},
44+
};
45+
46+
const event = await client.contactEvents.create(contactId, payload);
47+
console.log("Contact event created:", JSON.stringify(event, null, 2));
48+
} catch (error) {
49+
console.error(
50+
"Error creating contact event:",
51+
error instanceof Error ? error.message : String(error)
52+
);
53+
}
54+
}
55+
56+
createContactEvent();
57+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import axios, { AxiosInstance } from "axios";
2+
3+
import ContactEventsBaseAPI from "../../../lib/api/ContactEvents";
4+
5+
describe("lib/api/ContactEvents: ", () => {
6+
const axiosInstance: AxiosInstance = axios.create();
7+
const accountId = 100;
8+
const api = new ContactEventsBaseAPI(axiosInstance, accountId);
9+
10+
describe("class ContactEvents(): ", () => {
11+
describe("init: ", () => {
12+
it("initializes with all necessary params.", () => {
13+
expect(api).toHaveProperty("create");
14+
});
15+
});
16+
});
17+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import axios from "axios";
2+
import AxiosMockAdapter from "axios-mock-adapter";
3+
4+
import ContactEventsApi from "../../../../lib/api/resources/ContactEvents";
5+
import handleSendingError from "../../../../lib/axios-logger";
6+
import MailtrapError from "../../../../lib/MailtrapError";
7+
8+
import CONFIG from "../../../../config";
9+
10+
const { CLIENT_SETTINGS } = CONFIG;
11+
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;
12+
13+
describe("lib/api/resources/ContactEvents: ", () => {
14+
let mock: AxiosMockAdapter;
15+
const accountId = 100;
16+
const contactEventsAPI = new ContactEventsApi(axios, accountId);
17+
18+
const contactIdentifier = "[email protected]";
19+
const payload = {
20+
name: "purchase_completed",
21+
params: {
22+
order_id: 12345,
23+
amount: 49.99,
24+
currency: "USD",
25+
coupon_used: false,
26+
},
27+
};
28+
29+
const successResponse = {
30+
contact_id: "019a4a05-5924-7d93-86ce-f79293418083",
31+
contact_email: contactIdentifier,
32+
name: payload.name,
33+
params: payload.params,
34+
};
35+
36+
describe("class ContactEventsApi(): ", () => {
37+
describe("init: ", () => {
38+
it("initializes with all necessary params.", () => {
39+
expect(contactEventsAPI).toHaveProperty("create");
40+
});
41+
});
42+
});
43+
44+
beforeAll(() => {
45+
axios.interceptors.response.use(
46+
(response) => response.data,
47+
handleSendingError
48+
);
49+
mock = new AxiosMockAdapter(axios);
50+
});
51+
52+
afterEach(() => {
53+
mock.reset();
54+
});
55+
56+
describe("create(): ", () => {
57+
it("successfully creates a contact event.", async () => {
58+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactIdentifier}/events`;
59+
const expectedResponseData = successResponse;
60+
61+
expect.assertions(2);
62+
63+
mock.onPost(endpoint, payload).reply(200, expectedResponseData);
64+
const result = await contactEventsAPI.create(contactIdentifier, payload);
65+
66+
expect(mock.history.post[0].url).toEqual(endpoint);
67+
expect(result).toEqual(expectedResponseData);
68+
});
69+
70+
it("fails with error when accountId is invalid.", async () => {
71+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactIdentifier}/events`;
72+
const expectedErrorMessage = "Account not found";
73+
74+
expect.assertions(2);
75+
76+
mock.onPost(endpoint).reply(404, { error: expectedErrorMessage });
77+
78+
try {
79+
await contactEventsAPI.create(contactIdentifier, payload);
80+
} catch (error) {
81+
expect(error).toBeInstanceOf(MailtrapError);
82+
if (error instanceof MailtrapError) {
83+
expect(error.message).toEqual(expectedErrorMessage);
84+
}
85+
}
86+
});
87+
88+
it("fails with error when payload is invalid.", async () => {
89+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactIdentifier}/events`;
90+
const expectedErrorMessage = "Invalid event name";
91+
92+
expect.assertions(2);
93+
94+
mock.onPost(endpoint).reply(422, { error: expectedErrorMessage });
95+
96+
try {
97+
await contactEventsAPI.create(contactIdentifier, payload);
98+
} catch (error) {
99+
expect(error).toBeInstanceOf(MailtrapError);
100+
if (error instanceof MailtrapError) {
101+
expect(error.message).toEqual(expectedErrorMessage);
102+
}
103+
}
104+
});
105+
});
106+
});

src/__tests__/lib/mailtrap-client.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ContactExportsBaseAPI from "../../lib/api/ContactExports";
1515
import TemplatesBaseAPI from "../../lib/api/Templates";
1616
import SuppressionsBaseAPI from "../../lib/api/Suppressions";
1717
import SendingDomainsBaseAPI from "../../lib/api/SendingDomains";
18+
import ContactEventsBaseAPI from "../../lib/api/ContactEvents";
1819

1920
const { ERRORS, CLIENT_SETTINGS } = CONFIG;
2021
const { TESTING_ENDPOINT, BULK_ENDPOINT, SENDING_ENDPOINT } = CLIENT_SETTINGS;
@@ -927,5 +928,31 @@ describe("lib/mailtrap-client: ", () => {
927928
expect(sendingDomainsClient).toBeInstanceOf(SendingDomainsBaseAPI);
928929
});
929930
});
931+
932+
describe("get contactEvents(): ", () => {
933+
it("rejects with Mailtrap error, when `accountId` is missing.", () => {
934+
const client = new MailtrapClient({
935+
token: "MY_API_TOKEN",
936+
});
937+
expect.assertions(1);
938+
939+
try {
940+
client.contactEvents;
941+
} catch (error) {
942+
expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING));
943+
}
944+
});
945+
946+
it("returns contact events API object when accountId is provided.", () => {
947+
const client = new MailtrapClient({
948+
token: "MY_API_TOKEN",
949+
accountId: 10,
950+
});
951+
expect.assertions(1);
952+
953+
const contactEventsClient = client.contactEvents;
954+
expect(contactEventsClient).toBeInstanceOf(ContactEventsBaseAPI);
955+
});
956+
});
930957
});
931958
});

src/lib/MailtrapClient.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import handleSendingError from "./axios-logger";
88
import MailtrapError from "./MailtrapError";
99

1010
import ContactsBaseAPI from "./api/Contacts";
11+
import ContactEventsBaseAPI from "./api/ContactEvents";
1112
import ContactExportsBaseAPI from "./api/ContactExports";
1213
import ContactFieldsBaseAPI from "./api/ContactFields";
1314
import ContactImportsBaseAPI from "./api/ContactImports";
@@ -132,6 +133,14 @@ export default class MailtrapClient {
132133
return new ContactsBaseAPI(this.axios, accountId);
133134
}
134135

136+
/**
137+
* Getter for Contact Events API.
138+
*/
139+
get contactEvents() {
140+
const accountId = this.validateAccountIdPresence();
141+
return new ContactEventsBaseAPI(this.axios, accountId);
142+
}
143+
135144
/**
136145
* Getter for Contact Exports API.
137146
*/

src/lib/api/ContactEvents.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { AxiosInstance } from "axios";
2+
3+
import ContactEventsApi from "./resources/ContactEvents";
4+
5+
export default class ContactEventsBaseAPI {
6+
public create: ContactEventsApi["create"];
7+
8+
constructor(client: AxiosInstance, accountId: number) {
9+
const contactEvents = new ContactEventsApi(client, accountId);
10+
this.create = contactEvents.create.bind(contactEvents);
11+
}
12+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { AxiosInstance } from "axios";
2+
3+
import CONFIG from "../../../config";
4+
import {
5+
ContactEventOptions,
6+
ContactEventResponse,
7+
} from "../../../types/api/contact-events";
8+
9+
const { CLIENT_SETTINGS } = CONFIG;
10+
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;
11+
12+
export default class ContactEventsApi {
13+
private client: AxiosInstance;
14+
15+
private contactsURL: string;
16+
17+
constructor(client: AxiosInstance, accountId: number) {
18+
this.client = client;
19+
this.contactsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts`;
20+
}
21+
22+
/**
23+
* Creates a new contact event for given contact identifier and data.
24+
*/
25+
public async create(
26+
contactIdentifier: number | string,
27+
data: ContactEventOptions
28+
) {
29+
const url = `${this.contactsURL}/${contactIdentifier}/events`;
30+
31+
return this.client.post<ContactEventResponse, ContactEventResponse>(
32+
url,
33+
data
34+
);
35+
}
36+
}

src/types/api/contact-events.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface ContactEventOptions {
2+
name: string;
3+
params: Record<string, string | number | boolean | null>;
4+
}
5+
6+
export interface ContactEventResponse {
7+
contact_id: string;
8+
contact_email: string;
9+
name: string;
10+
params: Record<string, string | number | boolean | null>;
11+
}

0 commit comments

Comments
 (0)