Skip to content

Commit af4f8bd

Browse files
authored
Add Campaign phonebook APIs (#38)
1 parent 9644292 commit af4f8bd

15 files changed

Lines changed: 457 additions & 5 deletions

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,28 @@ app.MapPost("/webhooks/termii", (TermiiWebhookEvent webhookEvent) =>
251251

252252
Webhook payloads can vary by event type and Termii account configuration. Unknown fields are preserved in `TermiiWebhookEvent.AdditionalData`.
253253

254+
## Campaigns
255+
256+
Fetch campaign phonebooks:
257+
258+
```csharp
259+
var phonebooks = await client.Campaigns.GetPhonebooksAsync(new GetPhonebooksRequest
260+
{
261+
Page = 0,
262+
Size = 15
263+
});
264+
```
265+
266+
Create a phonebook:
267+
268+
```csharp
269+
var phonebook = await client.Campaigns.CreatePhonebookAsync(new CreatePhonebookRequest
270+
{
271+
PhonebookName = "Customers",
272+
Description = "Customer contacts"
273+
});
274+
```
275+
254276
## Error Handling
255277

256278
The SDK throws `TermiiApiException` for non-success HTTP responses from Termii:
@@ -285,11 +307,11 @@ Implemented in the current SDK:
285307
- Number API: send message through a dedicated Termii number.
286308
- Tokens: send, verify, generate, voice, email, and WhatsApp OTP flows.
287309
- Insights: balance, DND status, number intelligence, message history, and message analytics.
310+
- Campaigns: list, create, update, and delete phonebooks.
288311

289312
Deferred or not yet implemented:
290313

291314
- WhatsApp template/device message APIs.
292-
- Campaign phonebook APIs.
293315
- Product notification email APIs.
294316

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

docs/API_COVERAGE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ The Termii docs describe a REST/JSON API and state that each account has its own
5858
| Insights | Query number intelligence/status | GET | `/api/insight/number/query` | Query | `TermiiClient.Insights` | Implemented | #9, PR #27 | Unit tests added |
5959
| Insights | Fetch message inbox/history | GET | `/api/sms/inbox` | Query | `TermiiClient.Insights` | Implemented | #9, PR #27 | Unit tests added |
6060
| Insights | Fetch message analytics/history | GET | `/api/sms/history/analytics` | Query | `TermiiClient.Insights` | In progress | #34 | Unit tests added |
61+
| Campaigns | Fetch phonebooks | GET | `/api/phonebooks` | Query | `TermiiClient.Campaigns` | In progress | #33 | Unit tests added |
62+
| Campaigns | Create phonebook | POST | `/api/phonebooks` | JSON body | `TermiiClient.Campaigns` | In progress | #33 | Unit tests added |
63+
| Campaigns | Update phonebook | PATCH | `/api/phonebooks/{phonebook_id}` | JSON body | `TermiiClient.Campaigns` | In progress | #33 | Unit tests added |
64+
| Campaigns | Delete phonebook | DELETE | `/api/phonebooks/{phonebook_id}` | Query | `TermiiClient.Campaigns` | In progress | #33 | Unit tests added |
6165

6266
## Deferred Coverage
6367

@@ -67,10 +71,6 @@ The following documented APIs are useful but should come after the first SDK mil
6771
| --- | --- | --- | --- | --- | --- |
6872
| Messaging | Send WhatsApp template without media | POST | `/api/send/template` | Deferred | Requires WhatsApp template/device setup. |
6973
| Messaging | Send WhatsApp template with media | POST | `/api/send/template/media` | Deferred | Requires WhatsApp template/device setup and media payload modeling. |
70-
| Campaigns | Fetch phonebooks | GET | `/api/phonebooks` | Deferred | Part of campaign/phonebook management, not core messaging. |
71-
| Campaigns | Create phonebook | POST | `/api/phonebooks` | Deferred | Part of campaign/phonebook management. |
72-
| Campaigns | Update phonebook | PATCH | `/api/phonebooks/{phonebook_id}` | Deferred | Part of campaign/phonebook management. |
73-
| Campaigns | Delete phonebook | DELETE | `/api/phonebooks/{phonebook_id}` | Deferred | Part of campaign/phonebook management. |
7474
| Email | Send product notification email | POST | `/api/templates/send-email` | Deferred | Email notifications should be a separate milestone after SMS/token/insights. |
7575
| Insights | Webhook events and reports | N/A | Consumer webhook endpoint | Implemented | Receiver-side model support and README example covered by #32. |
7676

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Termii;
4+
5+
public sealed class CreatePhonebookRequest
6+
{
7+
[JsonPropertyName("phonebook_name")]
8+
public string PhonebookName { get; set; } = string.Empty;
9+
10+
[JsonPropertyName("description")]
11+
public string? Description { get; set; }
12+
13+
internal void Validate()
14+
{
15+
TermiiRequestValidation.Required(PhonebookName, nameof(PhonebookName));
16+
}
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Termii;
2+
3+
public sealed class GetPhonebooksRequest
4+
{
5+
public int? Page { get; set; }
6+
7+
public int? Size { get; set; }
8+
9+
public string? Name { get; set; }
10+
11+
internal string ToPath()
12+
{
13+
var query = new List<string>();
14+
TermiiCampaignQueryString.AddIfPresent(query, "page", Page);
15+
TermiiCampaignQueryString.AddIfPresent(query, "size", Size);
16+
TermiiCampaignQueryString.AddIfPresent(query, "name", Name);
17+
18+
return TermiiCampaignQueryString.Append("/api/phonebooks", query);
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Termii;
2+
3+
public interface ITermiiCampaignClient
4+
{
5+
Task<PhonebookListResponse> GetPhonebooksAsync(
6+
GetPhonebooksRequest? request = null,
7+
CancellationToken cancellationToken = default);
8+
9+
Task<PhonebookResponse> CreatePhonebookAsync(
10+
CreatePhonebookRequest request,
11+
CancellationToken cancellationToken = default);
12+
13+
Task<PhonebookResponse> UpdatePhonebookAsync(
14+
string phonebookId,
15+
UpdatePhonebookRequest request,
16+
CancellationToken cancellationToken = default);
17+
18+
Task<PhonebookOperationResponse> DeletePhonebookAsync(
19+
string phonebookId,
20+
CancellationToken cancellationToken = default);
21+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Termii;
5+
6+
public sealed class PhonebookListResponse
7+
{
8+
[JsonPropertyName("content")]
9+
public IReadOnlyList<PhonebookRecord> Content { get; set; } = Array.Empty<PhonebookRecord>();
10+
11+
[JsonPropertyName("data")]
12+
public IReadOnlyList<PhonebookRecord>? Data { get; set; }
13+
14+
[JsonPropertyName("totalElements")]
15+
public int? TotalElements { get; set; }
16+
17+
[JsonPropertyName("totalPages")]
18+
public int? TotalPages { get; set; }
19+
20+
[JsonPropertyName("size")]
21+
public int? Size { get; set; }
22+
23+
[JsonPropertyName("number")]
24+
public int? Number { get; set; }
25+
26+
[JsonExtensionData]
27+
public Dictionary<string, JsonElement>? AdditionalData { get; set; }
28+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Termii;
5+
6+
public sealed class PhonebookOperationResponse
7+
{
8+
[JsonPropertyName("code")]
9+
public string? Code { get; set; }
10+
11+
[JsonPropertyName("message")]
12+
public string? Message { get; set; }
13+
14+
[JsonPropertyName("status")]
15+
public string? Status { get; set; }
16+
17+
[JsonExtensionData]
18+
public Dictionary<string, JsonElement>? AdditionalData { get; set; }
19+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Termii;
5+
6+
public sealed class PhonebookRecord
7+
{
8+
[JsonPropertyName("id")]
9+
public string? Id { get; set; }
10+
11+
[JsonPropertyName("phonebook_id")]
12+
public string? PhonebookId { get; set; }
13+
14+
[JsonPropertyName("phonebook_name")]
15+
public string? PhonebookName { get; set; }
16+
17+
[JsonPropertyName("name")]
18+
public string? Name { get; set; }
19+
20+
[JsonPropertyName("description")]
21+
public string? Description { get; set; }
22+
23+
[JsonPropertyName("total_contacts")]
24+
public int? TotalContacts { get; set; }
25+
26+
[JsonPropertyName("created_at")]
27+
public string? CreatedAt { get; set; }
28+
29+
[JsonPropertyName("updated_at")]
30+
public string? UpdatedAt { get; set; }
31+
32+
[JsonExtensionData]
33+
public Dictionary<string, JsonElement>? AdditionalData { get; set; }
34+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Termii;
5+
6+
public sealed class PhonebookResponse
7+
{
8+
[JsonPropertyName("code")]
9+
public string? Code { get; set; }
10+
11+
[JsonPropertyName("message")]
12+
public string? Message { get; set; }
13+
14+
[JsonPropertyName("data")]
15+
public PhonebookRecord? Data { get; set; }
16+
17+
[JsonPropertyName("phonebook")]
18+
public PhonebookRecord? Phonebook { get; set; }
19+
20+
[JsonExtensionData]
21+
public Dictionary<string, JsonElement>? AdditionalData { get; set; }
22+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
namespace Termii;
2+
3+
internal sealed class TermiiCampaignClient : ITermiiCampaignClient
4+
{
5+
private static readonly HttpMethod Patch = new("PATCH");
6+
7+
private readonly TermiiJsonHttpPipeline _pipeline;
8+
9+
public TermiiCampaignClient(TermiiJsonHttpPipeline pipeline)
10+
{
11+
_pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
12+
}
13+
14+
public Task<PhonebookListResponse> GetPhonebooksAsync(
15+
GetPhonebooksRequest? request = null,
16+
CancellationToken cancellationToken = default)
17+
{
18+
return _pipeline.SendJsonAsync<PhonebookListResponse>(
19+
HttpMethod.Get,
20+
(request ?? new GetPhonebooksRequest()).ToPath(),
21+
body: null,
22+
TermiiAuthenticationLocation.Query,
23+
cancellationToken);
24+
}
25+
26+
public Task<PhonebookResponse> CreatePhonebookAsync(
27+
CreatePhonebookRequest request,
28+
CancellationToken cancellationToken = default)
29+
{
30+
if (request is null)
31+
{
32+
throw new ArgumentNullException(nameof(request));
33+
}
34+
35+
request.Validate();
36+
37+
return _pipeline.SendJsonAsync<PhonebookResponse>(
38+
HttpMethod.Post,
39+
"/api/phonebooks",
40+
request,
41+
TermiiAuthenticationLocation.Body,
42+
cancellationToken);
43+
}
44+
45+
public Task<PhonebookResponse> UpdatePhonebookAsync(
46+
string phonebookId,
47+
UpdatePhonebookRequest request,
48+
CancellationToken cancellationToken = default)
49+
{
50+
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));
51+
52+
if (request is null)
53+
{
54+
throw new ArgumentNullException(nameof(request));
55+
}
56+
57+
return _pipeline.SendJsonAsync<PhonebookResponse>(
58+
Patch,
59+
$"/api/phonebooks/{Uri.EscapeDataString(phonebookId)}",
60+
request,
61+
TermiiAuthenticationLocation.Body,
62+
cancellationToken);
63+
}
64+
65+
public Task<PhonebookOperationResponse> DeletePhonebookAsync(
66+
string phonebookId,
67+
CancellationToken cancellationToken = default)
68+
{
69+
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));
70+
71+
return _pipeline.SendJsonAsync<PhonebookOperationResponse>(
72+
HttpMethod.Delete,
73+
$"/api/phonebooks/{Uri.EscapeDataString(phonebookId)}",
74+
body: null,
75+
TermiiAuthenticationLocation.Query,
76+
cancellationToken);
77+
}
78+
}

0 commit comments

Comments
 (0)