diff --git a/CHANGELOG.md b/CHANGELOG.md index 10e5690..29cfba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ This project uses [Semantic Versioning 2.0.0](http://semver.org/). ## main +FEATURES: + +- NEW: Added `Dnsimple.Billing.ListCharges` to list billing charges for an account. (dnsimple/dnsimple-csharp#133) + ## 0.15.0 FEATURES: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c5f116..f2963f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,14 +25,7 @@ cd dnsimple-csharp ### 3. Build and test -[Run the test suite](#testing) to check everything is working as expected and to install the project specific -dependencies (the first time you'll run the script it will install all the dependencies for you). - -To run the test suite: - -```shell -dotnet test -``` +[Run the test suite](#testing) to check everything is working as expected and to install the project specific dependencies (the first time you'll run the script it will install all the dependencies for you). ## Releasing @@ -59,6 +52,10 @@ The following instructions uses $VERSION as a placeholder, where $VERSION is a M ## Testing -Submit unit tests for your changes. You can test your changes on your machine by [running the test suite](#testing). +Submit unit tests for your changes. You can test your changes on your machine by running: + +```shell +dotnet test +``` When you submit a PR, tests will also be run on the [continuous integration environment via GitHub Actions](https://github.com/dnsimple/dnsimple-csharp/actions). diff --git a/src/dnsimple-test/MockDnsimpleClient.cs b/src/dnsimple-test/MockDnsimpleClient.cs index bc6d866..f6b88b2 100644 --- a/src/dnsimple-test/MockDnsimpleClient.cs +++ b/src/dnsimple-test/MockDnsimpleClient.cs @@ -17,6 +17,7 @@ public class MockDnsimpleClient : IClient private string Fixture { get; } public AccountsService Accounts { get; } + public BillingService Billing { get; } public CertificatesService Certificates { get; } public ContactsService Contacts { get; } public DomainsService Domains { get; } @@ -38,6 +39,7 @@ public MockDnsimpleClient(string fixture) UserAgent = "Testing user agent"; Accounts = new AccountsService(this); + Billing = new BillingService(this); Certificates = new CertificatesService(this); Contacts = new ContactsService(this); Domains = new DomainsService(this); @@ -165,4 +167,4 @@ public void SetHeaders(List headers) Headers = headers; } } -} \ No newline at end of file +} diff --git a/src/dnsimple-test/Services/BillingTest.cs b/src/dnsimple-test/Services/BillingTest.cs new file mode 100644 index 0000000..92ac20c --- /dev/null +++ b/src/dnsimple-test/Services/BillingTest.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using dnsimple; +using dnsimple.Services; +using dnsimple.Services.ListOptions; +using NUnit.Framework; +using RestSharp; +using Pagination = dnsimple.Services.ListOptions.Pagination; + +namespace dnsimple_test.Services +{ + [TestFixture] + public class BillingTest + { + + private MockResponse _response; + + private const string ListChargesFixture = + "listCharges/success.http"; + + private const string ListChargesBadFilterFixture = + "listCharges/fail-400-bad-filter.http"; + + private const string ListChargesUnauthorizedFixture = + "listCharges/fail-403.http"; + + [SetUp] + public void Initialize() + { + var loader = new FixtureLoader("v2", ListChargesFixture); + _response = new MockResponse(loader); + } + + [Test] + public void ListCharges() + { + var charges = + new PaginatedResponse(_response).Data; + + Assert.Multiple(() => + { + Assert.AreEqual(3, charges.Count); + Assert.AreEqual(14.50m, charges.First().TotalAmount); + Assert.AreEqual(0.00m, charges.First().BalanceAmount); + Assert.AreEqual("1-2", charges.First().Reference); + Assert.AreEqual("collected", charges.First().State); + Assert.AreEqual("Register bubble-registered.com", charges.First().Items.First().Description); + Assert.AreEqual(14.50m, charges.First().Items.First().Amount); + Assert.AreEqual(1, charges.First().Items.First().ProductId); + Assert.AreEqual("domain-registration", charges.First().Items.First().ProductType); + }); + } + + [Test] + [TestCase( + "https://api.sandbox.dnsimple.com/v2/1010/domains?sort=invoiced:asc&start_date=2023-01-01&end_date=2023-08-31")] + public void ListChargesWithOptions(string expectedUrl) + { + var client = new MockDnsimpleClient(ListChargesFixture); + var listOptions = new ListChargesOptions(); + listOptions.FilterByStartDate("2023-01-01"); + listOptions.FilterByEndDate("2023-08-31"); + listOptions.SortByInvoiceDate(Order.asc); + + client.Domains.ListDomains(1010, listOptions); + + Assert.AreEqual(expectedUrl, client.RequestSentTo()); + } + + [Test] + public void ListChargesOptions() + { + var filters = new List> + { + new KeyValuePair("start_date", "2023-01-01"), + new KeyValuePair("end_date", "2023-08-31") + }; + var sorting = new KeyValuePair("sort", + "invoiced:asc"); + var pagination = new List> + { + new KeyValuePair("per_page", "30"), + new KeyValuePair("page", "1") + }; + + var options = new ListChargesOptions + { + Pagination = new Pagination + { + PerPage = 30, + Page = 1 + } + }.FilterByStartDate("2023-01-01") + .FilterByEndDate("2023-08-31") + .SortByInvoiceDate(Order.asc); + + + Assert.Multiple(() => + { + Assert.AreEqual(filters, options.UnpackFilters()); + Assert.AreEqual(pagination, options.UnpackPagination()); + Assert.AreEqual(sorting, options.UnpackSorting()); + }); + } + + [Test] + [TestCase(1010)] + public void ListChargesBadFilter(long accountId) + { + var client = new MockDnsimpleClient(ListChargesBadFilterFixture); + client.StatusCode(HttpStatusCode.BadRequest); + + Assert.Throws( + Is.TypeOf().And.Message + .EqualTo("Invalid date format must be ISO8601 (YYYY-MM-DD)"), + delegate { client.Billing.ListCharges(accountId); }); + } + + [Test] + [TestCase(1010)] + public void ListChargesUnauthorized(long accountId) + { + var client = new MockDnsimpleClient(ListChargesUnauthorizedFixture); + client.StatusCode(HttpStatusCode.BadRequest); + + Assert.Throws( + Is.TypeOf().And.Message + .EqualTo("Permission Denied. Required Scope: billing:*:read"), + delegate { client.Billing.ListCharges(accountId); }); + } + } +} diff --git a/src/dnsimple-test/dnsimple-test.csproj b/src/dnsimple-test/dnsimple-test.csproj index 04bd6da..d30a6fa 100644 --- a/src/dnsimple-test/dnsimple-test.csproj +++ b/src/dnsimple-test/dnsimple-test.csproj @@ -257,6 +257,15 @@ Always + + Always + + + Always + + + Always + Always diff --git a/src/dnsimple-test/fixtures/v2/api/listCharges/fail-400-bad-filter.http b/src/dnsimple-test/fixtures/v2/api/listCharges/fail-400-bad-filter.http new file mode 100644 index 0000000..5e36743 --- /dev/null +++ b/src/dnsimple-test/fixtures/v2/api/listCharges/fail-400-bad-filter.http @@ -0,0 +1,14 @@ +HTTP/1.1 400 Bad Request +Date: Tue, 24 Oct 2023 08:13:01 GMT +Connection: close +X-RateLimit-Limit: 2400 +X-RateLimit-Remaining: 2392 +X-RateLimit-Reset: 1698136677 +Content-Type: application/json; charset=utf-8 +X-WORK-WITH-US: Love automation? So do we! https://dnsimple.com/jobs +Cache-Control: no-cache +X-Request-Id: bdfbf3a7-d9dc-4018-9732-61502be989a3 +X-Runtime: 0.455303 +Transfer-Encoding: chunked + +{"message":"Invalid date format must be ISO8601 (YYYY-MM-DD)"} diff --git a/src/dnsimple-test/fixtures/v2/api/listCharges/fail-403.http b/src/dnsimple-test/fixtures/v2/api/listCharges/fail-403.http new file mode 100644 index 0000000..ddf9f64 --- /dev/null +++ b/src/dnsimple-test/fixtures/v2/api/listCharges/fail-403.http @@ -0,0 +1,14 @@ +HTTP/1.1 403 Forbidden +Date: Tue, 24 Oct 2023 09:49:29 GMT +Connection: close +X-RateLimit-Limit: 2400 +X-RateLimit-Remaining: 2398 +X-RateLimit-Reset: 1698143967 +Content-Type: application/json; charset=utf-8 +X-WORK-WITH-US: Love automation? So do we! https://dnsimple.com/jobs +Cache-Control: no-cache +X-Request-Id: 5554e2d3-2652-4ca7-8c5e-92b4c35f28d6 +X-Runtime: 0.035309 +Transfer-Encoding: chunked + +{"message":"Permission Denied. Required Scope: billing:*:read"} diff --git a/src/dnsimple-test/fixtures/v2/api/listCharges/success.http b/src/dnsimple-test/fixtures/v2/api/listCharges/success.http new file mode 100644 index 0000000..ae726a0 --- /dev/null +++ b/src/dnsimple-test/fixtures/v2/api/listCharges/success.http @@ -0,0 +1,14 @@ +HTTP/1.1 200 OK +Date: Tue, 24 Oct 2023 09:52:55 GMT +Connection: close +X-RateLimit-Limit: 2400 +X-RateLimit-Remaining: 2397 +X-RateLimit-Reset: 1698143967 +Content-Type: application/json; charset=utf-8 +X-WORK-WITH-US: Love automation? So do we! https://dnsimple.com/jobs +Cache-Control: no-store, must-revalidate, private, max-age=0 +X-Request-Id: a57a87c8-626a-4361-9fb8-b55ca9be8e5d +X-Runtime: 0.060526 +Transfer-Encoding: chunked + +{"data":[{"invoiced_at":"2023-08-17T05:53:36Z","total_amount":"14.50","balance_amount":"0.00","reference":"1-2","state":"collected","items":[{"description":"Register bubble-registered.com","amount":"14.50","product_id":1,"product_type":"domain-registration","product_reference":"bubble-registered.com"}]},{"invoiced_at":"2023-08-17T05:57:53Z","total_amount":"14.50","balance_amount":"0.00","reference":"2-2","state":"refunded","items":[{"description":"Register example.com","amount":"14.50","product_id":2,"product_type":"domain-registration","product_reference":"example.com"}]},{"invoiced_at":"2023-10-24T07:49:05Z","total_amount":"1099999.99","balance_amount":"0.00","reference":"4-2","state":"collected","items":[{"description":"Test Line Item 1","amount":"99999.99","product_id":null,"product_type":"manual","product_reference":null},{"description":"Test Line Item 2","amount":"1000000.00","product_id":null,"product_type":"manual","product_reference":null}]}],"pagination":{"current_page":1,"per_page":30,"total_entries":3,"total_pages":1}} diff --git a/src/dnsimple/DNSimple.cs b/src/dnsimple/DNSimple.cs index ca1f7f8..d01b103 100644 --- a/src/dnsimple/DNSimple.cs +++ b/src/dnsimple/DNSimple.cs @@ -68,6 +68,13 @@ public interface IClient /// https://developer.dnsimple.com/v2/accounts/ AccountsService Accounts { get; } + /// + /// Instance of the BillingService + /// + /// + /// https://developer.dnsimple.com/v2/billing/ + BillingService Billing { get; } + /// /// Instance of the ContactsService /// @@ -260,6 +267,9 @@ public class Client : IClient /// public AccountsService Accounts { get; private set; } + /// + public BillingService Billing { get; private set; } + /// public CertificatesService Certificates { get; private set; } @@ -404,4 +414,4 @@ private void InitializeServices() Zones = new ZonesService(this); } } -} \ No newline at end of file +} diff --git a/src/dnsimple/Services/Billing.cs b/src/dnsimple/Services/Billing.cs new file mode 100644 index 0000000..43f6bd6 --- /dev/null +++ b/src/dnsimple/Services/Billing.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using dnsimple.Services.ListOptions; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using RestSharp; +using static dnsimple.Services.Paths; + +namespace dnsimple.Services +{ + /// + /// https://developer.dnsimple.com/v2/certificates/ + public class BillingService : ServiceBase + { + /// + public BillingService(IClient client) : base(client) + { + } + + /// + /// Lists the billing charges for an account. + /// + /// The account ID + /// Options passed to the list (filtering, pagination, and sorting) + /// A ChargesResponse containing a list of charges for the + /// account. + public PaginatedResponse ListCharges(long accountId, ListChargesOptions options = null) + { + var builder = BuildRequestForPath(ChargesPath(accountId)); + AddListOptionsToRequest(options, ref builder); + + return new PaginatedResponse(Execute(builder.Request)); + } + } + + /// + /// Represents the charge data. + /// + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public struct Charge + { + public DateTime InvoicedAt { get; set; } + public decimal TotalAmount { get; set; } + public decimal BalanceAmount { get; set; } + public string Reference { get; set; } + public string State { get; set; } + public List Items { get; set; } + } + + /// + /// Represents the charge item data. + /// + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy), ItemNullValueHandling = NullValueHandling.Ignore)] + + public struct ChargeItem + { + public string Description { get; set; } + public decimal Amount { get; set; } + public long ProductId { get; set; } + public string ProductType { get; set; } + public string ProductReference { get; set; } + } + +} diff --git a/src/dnsimple/Services/ListOptions/ListChargesOptions.cs b/src/dnsimple/Services/ListOptions/ListChargesOptions.cs new file mode 100644 index 0000000..ed20b3a --- /dev/null +++ b/src/dnsimple/Services/ListOptions/ListChargesOptions.cs @@ -0,0 +1,43 @@ +namespace dnsimple.Services.ListOptions +{ + /// + /// Defines the options you may want to send to list charges, + /// such as pagination and sorting + /// + /// + public class ListChargesOptions : ListOptionsWithFiltering + { + /// + /// Sets the start date to be filtered by. + /// + /// The start date we want to filter by. + /// The instance of the ChargesListOptions + public ListChargesOptions FilterByStartDate(string date) + { + AddFilter(new Filter { Field = "start_date", Value = date }); + return this; + } + + /// + /// Sets the end date to be filtered by. + /// + /// The end date we want to filter by. + /// The instance of the ChargesListOptions + public ListChargesOptions FilterByEndDate(string date) + { + AddFilter(new Filter { Field = "end_date", Value = date }); + return this; + } + + /// + /// Sets the order by which to sort by invoice date. + /// + /// The order in which we want to sort (asc or desc) + /// The instance of ChargesListOptions + public ListChargesOptions SortByInvoiceDate(Order order) + { + AddSortCriteria(new Sort { Field = "invoiced", Order = order }); + return this; + } + } +} diff --git a/src/dnsimple/Services/Paths.cs b/src/dnsimple/Services/Paths.cs index 4e2fc5b..f47e1e2 100644 --- a/src/dnsimple/Services/Paths.cs +++ b/src/dnsimple/Services/Paths.cs @@ -2,6 +2,11 @@ namespace dnsimple.Services { public readonly struct Paths { + public static string ChargesPath(long accountId) + { + return $"/{accountId}/billing/charges"; + } + public static string CollaboratorsPath(long accountId, string domainIdentifier) {