Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,35 @@ var phonebook = await client.Campaigns.CreatePhonebookAsync(new CreatePhonebookR
});
```

## Contacts

Add a single contact to a phonebook:

```csharp
var contact = await client.Contacts.AddAsync("phonebook-123", new AddContactRequest
{
PhoneNumber = "8123696237",
CountryCode = "234",
EmailAddress = "person@example.com",
FirstName = "Ada",
LastName = "Lovelace"
});
```

Upload contacts from a CSV file:

```csharp
await using var file = File.OpenRead("contacts.csv");

var upload = await client.Contacts.UploadAsync(new UploadContactsRequest
{
PhonebookId = "phonebook-123",
CountryCode = "234",
File = file,
FileName = "contacts.csv"
});
```

## Product Emails

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

See [docs/API_COVERAGE.md](docs/API_COVERAGE.md) for the detailed coverage matrix.
Expand Down
4 changes: 4 additions & 0 deletions docs/API_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ The Termii docs describe a REST/JSON API and state that each account has its own
| Campaigns | Create phonebook | POST | `/api/phonebooks` | JSON body | `TermiiClient.Campaigns` | Implemented | #33, PR #38 | Unit tests added |
| Campaigns | Update phonebook | PATCH | `/api/phonebooks/{phonebook_id}` | JSON body | `TermiiClient.Campaigns` | Implemented | #33, PR #38 | Unit tests added |
| Campaigns | Delete phonebook | DELETE | `/api/phonebooks/{phonebook_id}` | Query | `TermiiClient.Campaigns` | Implemented | #33, PR #38 | Unit tests added |
| Contacts | Fetch contacts by phonebook | GET | `/api/phonebooks/{phonebook_id}/contacts` | Query | `TermiiClient.Contacts` | Implemented | #43 | Unit tests added |
| Contacts | Add single contact to phonebook | POST | `/api/phonebooks/{phonebook_id}/contacts` | JSON body | `TermiiClient.Contacts` | Implemented | #43 | Unit tests added |
| Contacts | Upload contacts CSV | POST | `/api/phonebooks/contacts/upload` | Multipart body | `TermiiClient.Contacts` | Implemented | #43 | Unit tests added |
| Contacts | Delete contact in phonebook | DELETE | `/api/phonebooks/{phonebook_id}/contacts` | Query | `TermiiClient.Contacts` | Implemented | #43 | Unit tests added |
| Email | Send product notification email | POST | `/api/templates/send-email` | JSON body | `TermiiClient.Emails` | Implemented | #31, PR #39 | Unit tests added |

## Receiver-Side Coverage
Expand Down
29 changes: 29 additions & 0 deletions src/Termii/Contacts/AddContactRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Text.Json.Serialization;

namespace Termii;

public sealed class AddContactRequest
{
[JsonPropertyName("phone_number")]
public string PhoneNumber { get; set; } = string.Empty;

[JsonPropertyName("country_code")]
public string? CountryCode { get; set; }

[JsonPropertyName("email_address")]
public string? EmailAddress { get; set; }

[JsonPropertyName("first_name")]
public string? FirstName { get; set; }

[JsonPropertyName("last_name")]
public string? LastName { get; set; }

[JsonPropertyName("company")]
public string? Company { get; set; }

internal void Validate()
{
TermiiRequestValidation.Required(PhoneNumber, nameof(PhoneNumber));
}
}
84 changes: 84 additions & 0 deletions src/Termii/Contacts/ContactListResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Text.Json.Serialization;

namespace Termii;

public sealed class ContactListResponse
{
public IReadOnlyList<string> Headers { get; set; } = Array.Empty<string>();

public ContactPhonebookRecord? Phonebook { get; set; }

public ContactListData? Data { get; set; }
}

public sealed class ContactListData
{
public IReadOnlyList<ContactRecord> Content { get; set; } = Array.Empty<ContactRecord>();

public int? TotalPages { get; set; }

public int? TotalElements { get; set; }

public int? Size { get; set; }

public int? Number { get; set; }

public bool? First { get; set; }

public bool? Last { get; set; }

public bool? Empty { get; set; }
}

public sealed class ContactPhonebookRecord
{
public string? Id { get; set; }

public string? ApplicationId { get; set; }

public string? Description { get; set; }

[JsonPropertyName("created_at")]
public string? CreatedAt { get; set; }

[JsonPropertyName("phonebook_name")]
public string? PhonebookName { get; set; }

[JsonPropertyName("total_contact")]
public int? TotalContact { get; set; }

[JsonPropertyName("total_campaign")]
public int? TotalCampaign { get; set; }
}

public sealed class ContactRecord
{
public string? Id { get; set; }

public string? Pid { get; set; }

[JsonPropertyName("phone_number")]
public string? PhoneNumber { get; set; }

[JsonPropertyName("email_address")]
public string? EmailAddress { get; set; }

[JsonPropertyName("first_name")]
public string? FirstName { get; set; }

[JsonPropertyName("last_name")]
public string? LastName { get; set; }

[JsonPropertyName("company")]
public string? Company { get; set; }

[JsonPropertyName("contact_list_key_value")]
public IReadOnlyList<ContactKeyValue> ContactListKeyValue { get; set; } = Array.Empty<ContactKeyValue>();
}

public sealed class ContactKeyValue
{
public string? Key { get; set; }

public string? Value { get; set; }
}
17 changes: 17 additions & 0 deletions src/Termii/Contacts/ContactOperationResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Termii;

public sealed class ContactOperationResponse
{
public int? Code { get; set; }

public ContactOperationData? Data { get; set; }

public string? Message { get; set; }

public string? Status { get; set; }
}

public sealed class ContactOperationData
{
public string? Message { get; set; }
}
13 changes: 13 additions & 0 deletions src/Termii/Contacts/ContactResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;

namespace Termii;

public sealed class ContactResponse
{
[JsonPropertyName("Contact added successfully")]
public ContactRecord? ContactAddedSuccessfully { get; set; }

public string? Message { get; set; }

public ContactRecord? Data { get; set; }
}
6 changes: 6 additions & 0 deletions src/Termii/Contacts/ContactUploadResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Termii;

public sealed class ContactUploadResponse
{
public string? Message { get; set; }
}
21 changes: 21 additions & 0 deletions src/Termii/Contacts/GetContactsRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Termii;

public sealed class GetContactsRequest
{
public int? Page { get; set; }

public int? Size { get; set; }

internal string ToPath(string phonebookId)
{
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));

var query = new List<string>();
TermiiCampaignQueryString.AddIfPresent(query, "page", Page);
TermiiCampaignQueryString.AddIfPresent(query, "size", Size);

return TermiiCampaignQueryString.Append(
$"/api/phonebooks/{Uri.EscapeDataString(phonebookId)}/contacts",
query);
}
}
22 changes: 22 additions & 0 deletions src/Termii/Contacts/ITermiiContactClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Termii;

public interface ITermiiContactClient
{
Task<ContactListResponse> GetAsync(
string phonebookId,
GetContactsRequest? request = null,
CancellationToken cancellationToken = default);

Task<ContactResponse> AddAsync(
string phonebookId,
AddContactRequest request,
CancellationToken cancellationToken = default);

Task<ContactUploadResponse> UploadAsync(
UploadContactsRequest request,
CancellationToken cancellationToken = default);

Task<ContactOperationResponse> DeleteAsync(
string phonebookId,
CancellationToken cancellationToken = default);
}
105 changes: 105 additions & 0 deletions src/Termii/Contacts/TermiiContactClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Termii;

internal sealed class TermiiContactClient : ITermiiContactClient
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};

private readonly TermiiJsonHttpPipeline _pipeline;

public TermiiContactClient(TermiiJsonHttpPipeline pipeline)
{
_pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
}

public Task<ContactListResponse> GetAsync(
string phonebookId,
GetContactsRequest? request = null,
CancellationToken cancellationToken = default)
{
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));

return _pipeline.SendJsonAsync<ContactListResponse>(
HttpMethod.Get,
(request ?? new GetContactsRequest()).ToPath(phonebookId),
body: null,
TermiiAuthenticationLocation.Query,
cancellationToken);
}

public Task<ContactResponse> AddAsync(
string phonebookId,
AddContactRequest request,
CancellationToken cancellationToken = default)
{
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));

if (request is null)
{
throw new ArgumentNullException(nameof(request));
}

request.Validate();

return _pipeline.SendJsonAsync<ContactResponse>(
HttpMethod.Post,
$"/api/phonebooks/{Uri.EscapeDataString(phonebookId)}/contacts",
request,
TermiiAuthenticationLocation.Body,
cancellationToken);
}

public Task<ContactUploadResponse> UploadAsync(
UploadContactsRequest request,
CancellationToken cancellationToken = default)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}

request.Validate();

var content = new MultipartFormDataContent();
var fileContent = new StreamContent(request.File);
fileContent.Headers.ContentType = new MediaTypeHeaderValue(request.ContentType);
content.Add(fileContent, "file", request.FileName);

var contact = new
{
pid = request.PhonebookId,
country_code = request.CountryCode,
api_key = _pipeline.ApiKey,
};
content.Add(
new StringContent(JsonSerializer.Serialize(contact, JsonSerializerOptions), Encoding.UTF8, "application/json"),
"contact");

return _pipeline.SendContentJsonAsync<ContactUploadResponse>(
HttpMethod.Post,
"/api/phonebooks/contacts/upload",
content,
cancellationToken);
}

public Task<ContactOperationResponse> DeleteAsync(
string phonebookId,
CancellationToken cancellationToken = default)
{
TermiiRequestValidation.Required(phonebookId, nameof(phonebookId));

return _pipeline.SendJsonAsync<ContactOperationResponse>(
HttpMethod.Delete,
$"/api/phonebooks/{Uri.EscapeDataString(phonebookId)}/contacts",
body: null,
TermiiAuthenticationLocation.Query,
cancellationToken);
}
}
27 changes: 27 additions & 0 deletions src/Termii/Contacts/UploadContactsRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Termii;

public sealed class UploadContactsRequest
{
public string PhonebookId { get; set; } = string.Empty;

public string CountryCode { get; set; } = string.Empty;

public Stream File { get; set; } = Stream.Null;

public string FileName { get; set; } = "contacts.csv";

public string ContentType { get; set; } = "text/csv";

internal void Validate()
{
TermiiRequestValidation.Required(PhonebookId, nameof(PhonebookId));
TermiiRequestValidation.Required(CountryCode, nameof(CountryCode));
TermiiRequestValidation.Required(FileName, nameof(FileName));
TermiiRequestValidation.Required(ContentType, nameof(ContentType));

if (File is null || File == Stream.Null)
{
throw new ArgumentException("A contacts CSV file stream is required.", nameof(File));
}
}
}
Loading
Loading