Skip to content

Commit 055f5a7

Browse files
authored
Add Contacts API support (#47)
1 parent 40c3601 commit 055f5a7

15 files changed

Lines changed: 656 additions & 0 deletions

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,35 @@ var phonebook = await client.Campaigns.CreatePhonebookAsync(new CreatePhonebookR
285285
});
286286
```
287287

288+
## Contacts
289+
290+
Add a single contact to a phonebook:
291+
292+
```csharp
293+
var contact = await client.Contacts.AddAsync("phonebook-123", new AddContactRequest
294+
{
295+
PhoneNumber = "8123696237",
296+
CountryCode = "234",
297+
EmailAddress = "person@example.com",
298+
FirstName = "Ada",
299+
LastName = "Lovelace"
300+
});
301+
```
302+
303+
Upload contacts from a CSV file:
304+
305+
```csharp
306+
await using var file = File.OpenRead("contacts.csv");
307+
308+
var upload = await client.Contacts.UploadAsync(new UploadContactsRequest
309+
{
310+
PhonebookId = "phonebook-123",
311+
CountryCode = "234",
312+
File = file,
313+
FileName = "contacts.csv"
314+
});
315+
```
316+
288317
## Product Emails
289318

290319
Send a product notification email:
@@ -334,6 +363,7 @@ Implemented in the current SDK:
334363
- Tokens: send, verify, generate, voice, email, and WhatsApp OTP flows.
335364
- Insights: balance, DND status, number intelligence, message history, and message analytics.
336365
- Campaigns: list, create, update, and delete phonebooks.
366+
- Contacts: list, add, upload, and delete phonebook contacts.
337367
- Product emails: send template-based notification emails.
338368

339369
See [docs/API_COVERAGE.md](docs/API_COVERAGE.md) for the detailed coverage matrix.

docs/API_COVERAGE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ The Termii docs describe a REST/JSON API and state that each account has its own
6464
| Campaigns | Create phonebook | POST | `/api/phonebooks` | JSON body | `TermiiClient.Campaigns` | Implemented | #33, PR #38 | Unit tests added |
6565
| Campaigns | Update phonebook | PATCH | `/api/phonebooks/{phonebook_id}` | JSON body | `TermiiClient.Campaigns` | Implemented | #33, PR #38 | Unit tests added |
6666
| Campaigns | Delete phonebook | DELETE | `/api/phonebooks/{phonebook_id}` | Query | `TermiiClient.Campaigns` | Implemented | #33, PR #38 | Unit tests added |
67+
| Contacts | Fetch contacts by phonebook | GET | `/api/phonebooks/{phonebook_id}/contacts` | Query | `TermiiClient.Contacts` | Implemented | #43 | Unit tests added |
68+
| Contacts | Add single contact to phonebook | POST | `/api/phonebooks/{phonebook_id}/contacts` | JSON body | `TermiiClient.Contacts` | Implemented | #43 | Unit tests added |
69+
| Contacts | Upload contacts CSV | POST | `/api/phonebooks/contacts/upload` | Multipart body | `TermiiClient.Contacts` | Implemented | #43 | Unit tests added |
70+
| Contacts | Delete contact in phonebook | DELETE | `/api/phonebooks/{phonebook_id}/contacts` | Query | `TermiiClient.Contacts` | Implemented | #43 | Unit tests added |
6771
| Email | Send product notification email | POST | `/api/templates/send-email` | JSON body | `TermiiClient.Emails` | Implemented | #31, PR #39 | Unit tests added |
6872

6973
## Receiver-Side Coverage
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Termii;
4+
5+
public sealed class AddContactRequest
6+
{
7+
[JsonPropertyName("phone_number")]
8+
public string PhoneNumber { get; set; } = string.Empty;
9+
10+
[JsonPropertyName("country_code")]
11+
public string? CountryCode { get; set; }
12+
13+
[JsonPropertyName("email_address")]
14+
public string? EmailAddress { get; set; }
15+
16+
[JsonPropertyName("first_name")]
17+
public string? FirstName { get; set; }
18+
19+
[JsonPropertyName("last_name")]
20+
public string? LastName { get; set; }
21+
22+
[JsonPropertyName("company")]
23+
public string? Company { get; set; }
24+
25+
internal void Validate()
26+
{
27+
TermiiRequestValidation.Required(PhoneNumber, nameof(PhoneNumber));
28+
}
29+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Termii;
4+
5+
public sealed class ContactListResponse
6+
{
7+
public IReadOnlyList<string> Headers { get; set; } = Array.Empty<string>();
8+
9+
public ContactPhonebookRecord? Phonebook { get; set; }
10+
11+
public ContactListData? Data { get; set; }
12+
}
13+
14+
public sealed class ContactListData
15+
{
16+
public IReadOnlyList<ContactRecord> Content { get; set; } = Array.Empty<ContactRecord>();
17+
18+
public int? TotalPages { get; set; }
19+
20+
public int? TotalElements { get; set; }
21+
22+
public int? Size { get; set; }
23+
24+
public int? Number { get; set; }
25+
26+
public bool? First { get; set; }
27+
28+
public bool? Last { get; set; }
29+
30+
public bool? Empty { get; set; }
31+
}
32+
33+
public sealed class ContactPhonebookRecord
34+
{
35+
public string? Id { get; set; }
36+
37+
public string? ApplicationId { get; set; }
38+
39+
public string? Description { get; set; }
40+
41+
[JsonPropertyName("created_at")]
42+
public string? CreatedAt { get; set; }
43+
44+
[JsonPropertyName("phonebook_name")]
45+
public string? PhonebookName { get; set; }
46+
47+
[JsonPropertyName("total_contact")]
48+
public int? TotalContact { get; set; }
49+
50+
[JsonPropertyName("total_campaign")]
51+
public int? TotalCampaign { get; set; }
52+
}
53+
54+
public sealed class ContactRecord
55+
{
56+
public string? Id { get; set; }
57+
58+
public string? Pid { get; set; }
59+
60+
[JsonPropertyName("phone_number")]
61+
public string? PhoneNumber { get; set; }
62+
63+
[JsonPropertyName("email_address")]
64+
public string? EmailAddress { get; set; }
65+
66+
[JsonPropertyName("first_name")]
67+
public string? FirstName { get; set; }
68+
69+
[JsonPropertyName("last_name")]
70+
public string? LastName { get; set; }
71+
72+
[JsonPropertyName("company")]
73+
public string? Company { get; set; }
74+
75+
[JsonPropertyName("contact_list_key_value")]
76+
public IReadOnlyList<ContactKeyValue> ContactListKeyValue { get; set; } = Array.Empty<ContactKeyValue>();
77+
}
78+
79+
public sealed class ContactKeyValue
80+
{
81+
public string? Key { get; set; }
82+
83+
public string? Value { get; set; }
84+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Termii;
2+
3+
public sealed class ContactOperationResponse
4+
{
5+
public int? Code { get; set; }
6+
7+
public ContactOperationData? Data { get; set; }
8+
9+
public string? Message { get; set; }
10+
11+
public string? Status { get; set; }
12+
}
13+
14+
public sealed class ContactOperationData
15+
{
16+
public string? Message { get; set; }
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Termii;
4+
5+
public sealed class ContactResponse
6+
{
7+
[JsonPropertyName("Contact added successfully")]
8+
public ContactRecord? ContactAddedSuccessfully { get; set; }
9+
10+
public string? Message { get; set; }
11+
12+
public ContactRecord? Data { get; set; }
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Termii;
2+
3+
public sealed class ContactUploadResponse
4+
{
5+
public string? Message { get; set; }
6+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Termii;
2+
3+
public sealed class GetContactsRequest
4+
{
5+
public int? Page { get; set; }
6+
7+
public int? Size { get; set; }
8+
9+
internal string ToPath(string phonebookId)
10+
{
11+
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));
12+
13+
var query = new List<string>();
14+
TermiiCampaignQueryString.AddIfPresent(query, "page", Page);
15+
TermiiCampaignQueryString.AddIfPresent(query, "size", Size);
16+
17+
return TermiiCampaignQueryString.Append(
18+
$"/api/phonebooks/{Uri.EscapeDataString(phonebookId)}/contacts",
19+
query);
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Termii;
2+
3+
public interface ITermiiContactClient
4+
{
5+
Task<ContactListResponse> GetAsync(
6+
string phonebookId,
7+
GetContactsRequest? request = null,
8+
CancellationToken cancellationToken = default);
9+
10+
Task<ContactResponse> AddAsync(
11+
string phonebookId,
12+
AddContactRequest request,
13+
CancellationToken cancellationToken = default);
14+
15+
Task<ContactUploadResponse> UploadAsync(
16+
UploadContactsRequest request,
17+
CancellationToken cancellationToken = default);
18+
19+
Task<ContactOperationResponse> DeleteAsync(
20+
string phonebookId,
21+
CancellationToken cancellationToken = default);
22+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Net.Http.Headers;
2+
using System.Text;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
6+
namespace Termii;
7+
8+
internal sealed class TermiiContactClient : ITermiiContactClient
9+
{
10+
private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web)
11+
{
12+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
13+
};
14+
15+
private readonly TermiiJsonHttpPipeline _pipeline;
16+
17+
public TermiiContactClient(TermiiJsonHttpPipeline pipeline)
18+
{
19+
_pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
20+
}
21+
22+
public Task<ContactListResponse> GetAsync(
23+
string phonebookId,
24+
GetContactsRequest? request = null,
25+
CancellationToken cancellationToken = default)
26+
{
27+
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));
28+
29+
return _pipeline.SendJsonAsync<ContactListResponse>(
30+
HttpMethod.Get,
31+
(request ?? new GetContactsRequest()).ToPath(phonebookId),
32+
body: null,
33+
TermiiAuthenticationLocation.Query,
34+
cancellationToken);
35+
}
36+
37+
public Task<ContactResponse> AddAsync(
38+
string phonebookId,
39+
AddContactRequest request,
40+
CancellationToken cancellationToken = default)
41+
{
42+
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));
43+
44+
if (request is null)
45+
{
46+
throw new ArgumentNullException(nameof(request));
47+
}
48+
49+
request.Validate();
50+
51+
return _pipeline.SendJsonAsync<ContactResponse>(
52+
HttpMethod.Post,
53+
$"/api/phonebooks/{Uri.EscapeDataString(phonebookId)}/contacts",
54+
request,
55+
TermiiAuthenticationLocation.Body,
56+
cancellationToken);
57+
}
58+
59+
public Task<ContactUploadResponse> UploadAsync(
60+
UploadContactsRequest request,
61+
CancellationToken cancellationToken = default)
62+
{
63+
if (request is null)
64+
{
65+
throw new ArgumentNullException(nameof(request));
66+
}
67+
68+
request.Validate();
69+
70+
var content = new MultipartFormDataContent();
71+
var fileContent = new StreamContent(request.File);
72+
fileContent.Headers.ContentType = new MediaTypeHeaderValue(request.ContentType);
73+
content.Add(fileContent, "file", request.FileName);
74+
75+
var contact = new
76+
{
77+
pid = request.PhonebookId,
78+
country_code = request.CountryCode,
79+
api_key = _pipeline.ApiKey,
80+
};
81+
content.Add(
82+
new StringContent(JsonSerializer.Serialize(contact, JsonSerializerOptions), Encoding.UTF8, "application/json"),
83+
"contact");
84+
85+
return _pipeline.SendContentJsonAsync<ContactUploadResponse>(
86+
HttpMethod.Post,
87+
"/api/phonebooks/contacts/upload",
88+
content,
89+
cancellationToken);
90+
}
91+
92+
public Task<ContactOperationResponse> DeleteAsync(
93+
string phonebookId,
94+
CancellationToken cancellationToken = default)
95+
{
96+
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));
97+
98+
return _pipeline.SendJsonAsync<ContactOperationResponse>(
99+
HttpMethod.Delete,
100+
$"/api/phonebooks/{Uri.EscapeDataString(phonebookId)}/contacts",
101+
body: null,
102+
TermiiAuthenticationLocation.Query,
103+
cancellationToken);
104+
}
105+
}

0 commit comments

Comments
 (0)