From 57f945bb0ac10f402862419ac01d10159dfa3eac Mon Sep 17 00:00:00 2001 From: Chris Smola Date: Thu, 18 Jan 2024 15:59:12 -0500 Subject: [PATCH] [add-revrec-general-ledger-accounts-feature] --- Library/Client.cs | 158 +++++++------ Library/Errors.cs | 3 +- Library/GeneralLedgerAccount.cs | 215 ++++++++++++++++++ Library/GeneralLedgerAccountType.cs | 17 ++ Library/List/GeneralLedgerAccountList.cs | 44 ++++ Test/Fixtures/FixtureImporter.cs | 2 + .../general_ledger_accounts/index-200.xml | 22 ++ .../general_ledger_accounts/show-200.xml | 12 + Test/GeneralLedgerAccountTest.cs | 29 +++ Test/List/GeneralLedgerAccountListTest.cs | 39 ++++ Test/Recurly.Test.csproj | 6 + 11 files changed, 477 insertions(+), 70 deletions(-) create mode 100644 Library/GeneralLedgerAccount.cs create mode 100644 Library/GeneralLedgerAccountType.cs create mode 100644 Library/List/GeneralLedgerAccountList.cs create mode 100644 Test/Fixtures/general_ledger_accounts/index-200.xml create mode 100644 Test/Fixtures/general_ledger_accounts/show-200.xml create mode 100644 Test/GeneralLedgerAccountTest.cs create mode 100644 Test/List/GeneralLedgerAccountListTest.cs diff --git a/Library/Client.cs b/Library/Client.cs index fea97617..cad44a03 100644 --- a/Library/Client.cs +++ b/Library/Client.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.IO; using System.Net; using System.Runtime.CompilerServices; @@ -128,55 +127,27 @@ public HttpStatusCode PerformRequest(HttpRequestMethod method, string urlPath, } protected virtual HttpStatusCode PerformRequest(HttpRequestMethod method, string urlPath, - WriteXmlDelegate writeXmlDelegate, ReadXmlDelegate readXmlDelegate, ReadXmlListDelegate readXmlListDelegate, ReadResponseDelegate reseponseDelegate) + WriteXmlDelegate writeXmlDelegate, ReadXmlDelegate readXmlDelegate, ReadXmlListDelegate readXmlListDelegate, ReadResponseDelegate responseDelegate) { - const int sixtySeconds = 60000; var url = Settings.GetServerUri(urlPath); #if (DEBUG) Console.WriteLine("Requesting " + method + " " + url); #endif var request = (HttpWebRequest)WebRequest.Create(url); - if (!request.RequestUri.Host.EndsWith(Settings.ValidDomain)) - { - throw new RecurlyException("Domain " + request.RequestUri.Host + " is not a valid Recurly domain"); - } - - request.Accept = "application/xml"; // Tells the server to return XML instead of HTML - request.ContentType = "application/xml; charset=utf-8"; // The request is an XML document - request.SendChunked = false; // Send it all as one request - request.UserAgent = Settings.UserAgent; - request.Headers.Add(HttpRequestHeader.Authorization, Settings.AuthorizationHeaderValue); - request.Headers.Add("X-Api-Version", Settings.RecurlyApiVersion); - request.Method = method.ToString().ToUpper(); - request.Timeout = Settings.RequestTimeoutMilliseconds ?? request.Timeout; + ValidateDomain(request); + AddRequestMetadata(request, method); Console.WriteLine(String.Format("Recurly: Requesting {0} {1}", request.Method, request.RequestUri)); - if ((method == HttpRequestMethod.Post || method == HttpRequestMethod.Put) && (writeXmlDelegate != null)) - { - // 60 second timeout -- some payment gateways (e.g. PayPal) can take a while to respond - request.Timeout = Settings.RequestTimeoutMilliseconds.HasValue ? request.Timeout : sixtySeconds; - - // Write POST/PUT body - using (var requestStream = request.GetRequestStream()) - { - WritePostParameters(requestStream, writeXmlDelegate); - } - } - else - { - request.ContentLength = 0; - } + WriteRequestParameters(request, method, writeXmlDelegate); try { using (var response = (HttpWebResponse)request.GetResponse()) { - - ReadWebResponse(response, readXmlDelegate, readXmlListDelegate, reseponseDelegate); + ReadWebResponse(response, readXmlDelegate, readXmlListDelegate, responseDelegate); return response.StatusCode; - } } catch (WebException ex) @@ -185,50 +156,22 @@ protected virtual HttpStatusCode PerformRequest(HttpRequestMethod method, string var response = (HttpWebResponse)ex.Response; var statusCode = response.StatusCode; - Errors errors; Console.WriteLine(String.Format("Recurly Library Received: {0} - {1}", (int)statusCode, statusCode)); - switch (response.StatusCode) + switch (statusCode) { case HttpStatusCode.OK: case HttpStatusCode.Accepted: case HttpStatusCode.Created: case HttpStatusCode.NoContent: - ReadWebResponse(response, readXmlDelegate, readXmlListDelegate, reseponseDelegate); + ReadWebResponse(response, readXmlDelegate, readXmlListDelegate, responseDelegate); return HttpStatusCode.NoContent; - case HttpStatusCode.NotFound: - errors = Errors.ReadResponseAndParseErrors(response); - if (errors.ValidationErrors.HasAny()) - throw new NotFoundException(errors.ValidationErrors[0].Message, errors); - throw new NotFoundException("The requested object was not found.", errors); - - case HttpStatusCode.Unauthorized: - case HttpStatusCode.Forbidden: - errors = Errors.ReadResponseAndParseErrors(response); - throw new InvalidCredentialsException(errors); - - case HttpStatusCode.BadRequest: - case HttpStatusCode.PreconditionFailed: - errors = Errors.ReadResponseAndParseErrors(response); - throw new ValidationException(errors); - - case HttpStatusCode.ServiceUnavailable: - throw new TemporarilyUnavailableException(); - - case HttpStatusCode.InternalServerError: - errors = Errors.ReadResponseAndParseErrors(response); - throw new ServerException(errors); - } - - if ((int)statusCode == ValidationException.HttpStatusCode) // Unprocessable Entity - { - errors = Errors.ReadResponseAndParseErrors(response); - if (errors.ValidationErrors.HasAny()) Console.WriteLine(errors.ValidationErrors[0].ToString()); - else Console.WriteLine("Client Error: " + response.ToString()); - throw new ValidationException(errors); + default: + ProcessErrorResponse(response); + break; } throw; @@ -415,7 +358,6 @@ protected virtual void WritePostParameters(Stream outputStream, WriteXmlDelegate } Console.WriteLine(Encoding.UTF8.GetString(s.ToArray())); #endif - } protected virtual MemoryStream CopyAndClose(Stream inputStream) @@ -435,5 +377,85 @@ protected virtual MemoryStream CopyAndClose(Stream inputStream) return ms; } + private void ValidateDomain(HttpWebRequest request) + { + if (!request.RequestUri.Host.EndsWith(Settings.ValidDomain)) + { + throw new RecurlyException("Domain " + request.RequestUri.Host + " is not a valid Recurly domain"); + } + } + + private void AddRequestMetadata(HttpWebRequest request, HttpRequestMethod method) + { + request.Accept = "application/xml"; // Tells the server to return XML instead of HTML + request.ContentType = "application/xml; charset=utf-8"; // The request is an XML document + request.SendChunked = false; // Send it all as one request + request.UserAgent = Settings.UserAgent; + request.Headers.Add(HttpRequestHeader.Authorization, Settings.AuthorizationHeaderValue); + request.Headers.Add("X-Api-Version", Settings.RecurlyApiVersion); + request.Method = method.ToString().ToUpper(); + request.Timeout = Settings.RequestTimeoutMilliseconds ?? request.Timeout; + } + + private void WriteRequestParameters(HttpWebRequest request, + HttpRequestMethod method, + WriteXmlDelegate writeXmlDelegate) + { + if ((method == HttpRequestMethod.Post || method == HttpRequestMethod.Put) && (writeXmlDelegate != null)) + { + // 60 second timeout -- some payment gateways (e.g. PayPal) can take a while to respond + request.Timeout = Settings.RequestTimeoutMilliseconds.HasValue ? request.Timeout : 60000; + + // Write POST/PUT body + using (var requestStream = request.GetRequestStream()) + { + WritePostParameters(requestStream, writeXmlDelegate); + } + } + else + { + request.ContentLength = 0; + } + } + + private void ProcessErrorResponse(HttpWebResponse response) + { + var statusCode = response.StatusCode; + Errors errors; + + switch (statusCode) + { + case HttpStatusCode.NotFound: + errors = Errors.ReadResponseAndParseErrors(response); + if (errors.ValidationErrors.HasAny()) + throw new NotFoundException(errors.ValidationErrors[0].Message, errors); + throw new NotFoundException("The requested object was not found.", errors); + + case HttpStatusCode.Unauthorized: + case HttpStatusCode.Forbidden: + errors = Errors.ReadResponseAndParseErrors(response); + throw new InvalidCredentialsException(errors); + + case HttpStatusCode.BadRequest: + case HttpStatusCode.PreconditionFailed: + errors = Errors.ReadResponseAndParseErrors(response); + throw new ValidationException(errors); + + case HttpStatusCode.ServiceUnavailable: + throw new TemporarilyUnavailableException(); + + case HttpStatusCode.InternalServerError: + errors = Errors.ReadResponseAndParseErrors(response); + throw new ServerException(errors); + } + + if ((int)statusCode == ValidationException.HttpStatusCode) // Unprocessable Entity + { + errors = Errors.ReadResponseAndParseErrors(response); + if (errors.ValidationErrors.HasAny()) Console.WriteLine(errors.ValidationErrors[0].ToString()); + else Console.WriteLine("Client Error: " + response.ToString()); + throw new ValidationException(errors); + } + } } } diff --git a/Library/Errors.cs b/Library/Errors.cs index 8590b8b9..b519452b 100644 --- a/Library/Errors.cs +++ b/Library/Errors.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Net; using System.Xml; diff --git a/Library/GeneralLedgerAccount.cs b/Library/GeneralLedgerAccount.cs new file mode 100644 index 00000000..f264e521 --- /dev/null +++ b/Library/GeneralLedgerAccount.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Xml; + +namespace Recurly +{ + + /// + /// A general ledger account in Recurly. + /// + /// + public class GeneralLedgerAccount : RecurlyEntity + { + public GeneralLedgerAccountType AccountType { get; private set; } + + public string Id { get; private set; } + + public string Code { get; set; } + + public string Description { get; set; } + + public DateTime? CreatedAt { get; private set; } + + public DateTime? UpdatedAt { get; private set; } + + internal const string UrlPrefix = "/general_ledger_accounts/"; + + #region Constructors + + public GeneralLedgerAccount() + { + } + + internal GeneralLedgerAccount(XmlTextReader xmlReader) + { + ReadXml(xmlReader); + } + + public GeneralLedgerAccount(string code, string accountType) + { + Code = code; + AccountType = ParseAccountType(accountType); + } + + public GeneralLedgerAccount(string code, GeneralLedgerAccountType accountType) + { + Code = code; + AccountType = accountType; + } + + /// + /// Allows for selecting an account type by its string value (e.g. "liability"). + /// + private GeneralLedgerAccountType ParseAccountType(string accountType) + { + var aType = char.ToUpper(accountType[0]) + accountType.Substring(1); + return (GeneralLedgerAccountType)Enum.Parse(typeof(GeneralLedgerAccountType), aType); + } + + #endregion + + /// + /// Create a new general ledger account in Recurly. + /// + public void Create() + { + Client.Instance.PerformRequest(Client.HttpRequestMethod.Post, + UrlPrefix, + WriteXml, + ReadXml); + } + + /// + /// Update an existing general ledger account in Recurly. + /// + public void Update() + { + // PUT /general_ledger_accounts/ + Client.Instance.PerformRequest(Client.HttpRequestMethod.Put, + UrlPrefix + Uri.EscapeDataString(Id), + WriteUpdateXml); + } + + internal override void ReadXml(XmlTextReader reader) + { + while (reader.Read()) + { + DateTime dateVal; + + if (reader.Name == "general_ledger_account" && reader.NodeType == XmlNodeType.EndElement) + break; + + if (reader.NodeType != XmlNodeType.Element) continue; + + switch (reader.Name) + { + case "id": + Id = reader.ReadElementContentAsString(); + break; + + case "account_type": + AccountType = ParseAccountType(reader.ReadElementContentAsString()); + break; + + case "code": + Code = reader.ReadElementContentAsString(); + break; + + case "description": + Description = reader.ReadElementContentAsString(); + break; + + case "created_at": + if (DateTime.TryParse(reader.ReadElementContentAsString(), out dateVal)) + { + CreatedAt = dateVal; + } + break; + + case "updated_at": + if (DateTime.TryParse(reader.ReadElementContentAsString(), out dateVal)) + { + UpdatedAt = dateVal; + } + break; + } + } + } + + internal override void WriteXml(XmlTextWriter xmlWriter) + { + xmlWriter.WriteStartElement("general_ledger_account"); + + xmlWriter.WriteElementString("account_type", AccountType.ToString().EnumNameToTransportCase()); + xmlWriter.WriteElementString("code", Code); + xmlWriter.WriteStringIfValid("description", Description); + + xmlWriter.WriteEndElement(); + } + + internal void WriteUpdateXml(XmlTextWriter xmlWriter) + { + xmlWriter.WriteStartElement("general_ledger_account"); + + xmlWriter.WriteElementString("code", Code); + xmlWriter.WriteStringIfValid("description", Description); + + xmlWriter.WriteEndElement(); + } + } + + public sealed class GeneralLedgerAccounts + { + internal const string UrlPrefix = "/general_ledger_accounts/"; + + /// + /// Retrieves a list of all general ledger accounts. + /// + /// + public static RecurlyList List() + { + return List(null); + } + + public static RecurlyList List(FilterCriteria filter) + { + filter = filter == null ? FilterCriteria.Instance : filter; + return new GeneralLedgerAccountList(GeneralLedgerAccount.UrlPrefix + "?" + filter.ToNamedValueCollection().ToString()); + } + + /// + /// Lists general ledger accounts, limited to state + /// + /// Retrieve GLAs of a particular type + /// + public static RecurlyList List(GeneralLedgerAccountType accountType) + { + return List(accountType, null); + } + + /// + /// Lists general ledger accounts, limited to state + /// + /// Retrieve GLAs of a particular type + /// FilterCriteria used to apply server side sorting and filtering + /// + public static RecurlyList List(GeneralLedgerAccountType accountType, + FilterCriteria filter) + { + filter = filter ?? FilterCriteria.Instance; + var parameters = filter.ToNamedValueCollection(); + parameters["account_type"] = accountType.ToString().EnumNameToTransportCase(); + return new GeneralLedgerAccountList(GeneralLedgerAccount.UrlPrefix + "?" + parameters.ToString()); + } + + public static GeneralLedgerAccount Get(string code) + { + if (string.IsNullOrWhiteSpace(code)) + { + return null; + } + + var generalLedgerAccount = new GeneralLedgerAccount(); + + var statusCode = Client.Instance.PerformRequest(Client.HttpRequestMethod.Get, + UrlPrefix + Uri.EscapeDataString(code), + generalLedgerAccount.ReadXml); + + return statusCode == HttpStatusCode.NotFound ? null : generalLedgerAccount; + } + + } + +} diff --git a/Library/GeneralLedgerAccountType.cs b/Library/GeneralLedgerAccountType.cs new file mode 100644 index 00000000..fc773914 --- /dev/null +++ b/Library/GeneralLedgerAccountType.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Recurly +{ + /// + /// Recurly supports the balance sheet (Liability) account and income (Revenue) account to + /// be specified for any given general ledger account entity. + /// + public enum GeneralLedgerAccountType + { + [EnumMember(Value = "liability")] + Liability, + + [EnumMember(Value = "revenue")] + Revenue, + } +} diff --git a/Library/List/GeneralLedgerAccountList.cs b/Library/List/GeneralLedgerAccountList.cs new file mode 100644 index 00000000..4fd347d9 --- /dev/null +++ b/Library/List/GeneralLedgerAccountList.cs @@ -0,0 +1,44 @@ +using System.Xml; + +namespace Recurly +{ + public class GeneralLedgerAccountList : RecurlyList + { + internal GeneralLedgerAccountList() + { + } + + internal GeneralLedgerAccountList(string baseUrl) : base(Client.HttpRequestMethod.Get, baseUrl) + { + } + + public override RecurlyList Start + { + get { return HasStartPage() ? new GeneralLedgerAccountList(StartUrl) : RecurlyList.Empty(); } + } + + public override RecurlyList Next + { + get { return HasNextPage() ? new GeneralLedgerAccountList(NextUrl) : RecurlyList.Empty(); } + } + + public override RecurlyList Prev + { + get { return HasPrevPage() ? new GeneralLedgerAccountList(PrevUrl) : RecurlyList.Empty(); } + } + + internal override void ReadXml(XmlTextReader reader) + { + while (reader.Read()) + { + if (reader.Name == "general_ledger_accounts" && reader.NodeType == XmlNodeType.EndElement) + break; + + if (reader.NodeType == XmlNodeType.Element && reader.Name == "general_ledger_account") + { + Add(new GeneralLedgerAccount(reader)); + } + } + } + } +} diff --git a/Test/Fixtures/FixtureImporter.cs b/Test/Fixtures/FixtureImporter.cs index b01b30db..53842bfa 100644 --- a/Test/Fixtures/FixtureImporter.cs +++ b/Test/Fixtures/FixtureImporter.cs @@ -80,5 +80,7 @@ public enum FixtureType ExternalPaymentPhases, [Description("external_invoices")] ExternalInvoices, + [Description("general_ledger_accounts")] + GeneralLedgerAccounts, } } diff --git a/Test/Fixtures/general_ledger_accounts/index-200.xml b/Test/Fixtures/general_ledger_accounts/index-200.xml new file mode 100644 index 00000000..b793c63b --- /dev/null +++ b/Test/Fixtures/general_ledger_accounts/index-200.xml @@ -0,0 +1,22 @@ +HTTP/1.1 200 OK +Content-Type: application/xml; charset=utf-8 + + + + + ua8iegmiu2ag + 100 + liability + A test description + 2024-01-22T17:43:38Z + 2024-01-22T17:43:38Z + + + lagie9mxu2ap + 200 + revenue + Another test description + 2024-01-22T17:53:38Z + 2024-01-22T17:53:38Z + + diff --git a/Test/Fixtures/general_ledger_accounts/show-200.xml b/Test/Fixtures/general_ledger_accounts/show-200.xml new file mode 100644 index 00000000..fea32a53 --- /dev/null +++ b/Test/Fixtures/general_ledger_accounts/show-200.xml @@ -0,0 +1,12 @@ +HTTP/1.1 200 OK +Content-Type: application/xml; charset=utf-8 + + + + ua8iegmiu2ag + 100 + liability + A test description + 2024-01-22T17:43:38Z + 2024-01-22T17:43:38Z + diff --git a/Test/GeneralLedgerAccountTest.cs b/Test/GeneralLedgerAccountTest.cs new file mode 100644 index 00000000..7e826f68 --- /dev/null +++ b/Test/GeneralLedgerAccountTest.cs @@ -0,0 +1,29 @@ +using System; +using System.Xml; +using FluentAssertions; + +using Recurly.Test.Fixtures; + + +namespace Recurly.Test +{ + public class GeneralLedgerAccountTest : BaseTest + { + [RecurlyFact(TestEnvironment.Type.Integration)] + public void GetGeneralLedgerAccount() + { + var gla = new GeneralLedgerAccount(); + + var xmlFixture = FixtureImporter.Get(FixtureType.GeneralLedgerAccounts, "show-200").Xml; + XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(xmlFixture)); + gla.ReadXml(reader); + + gla.Id.Should().Be("ua8iegmiu2ag"); + gla.AccountType.Should().Be(GeneralLedgerAccountType.Liability); + gla.Code.Should().Be("100"); + gla.Description.Should().Be("A test description"); + gla.CreatedAt.Should().Be(new DateTime(2024, 1, 22, 17, 43, 38, DateTimeKind.Utc)); + gla.UpdatedAt.Should().Be(new DateTime(2024, 1, 22, 17, 43, 38, DateTimeKind.Utc)); + } + } +} diff --git a/Test/List/GeneralLedgerAccountListTest.cs b/Test/List/GeneralLedgerAccountListTest.cs new file mode 100644 index 00000000..dd7de355 --- /dev/null +++ b/Test/List/GeneralLedgerAccountListTest.cs @@ -0,0 +1,39 @@ +using System; +using System.Xml; +using FluentAssertions; +using Recurly.Test.Fixtures; + +namespace Recurly.Test +{ + public class GeneralLedgerAccountListTest : BaseTest + { + [RecurlyFact(TestEnvironment.Type.Integration)] + public void List() + { + var glas = new GeneralLedgerAccountList(); + + var xmlFixture = FixtureImporter.Get(FixtureType.GeneralLedgerAccounts, "index-200").Xml; + XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(xmlFixture)); + glas.ReadXml(reader); + + glas.Should().HaveCount(2); + + var liabilityGla = glas[0]; + var revenueGla = glas[1]; + + liabilityGla.Id.Should().Be("ua8iegmiu2ag"); + liabilityGla.AccountType.Should().Be(GeneralLedgerAccountType.Liability); + liabilityGla.Code.Should().Be("100"); + liabilityGla.Description.Should().Be("A test description"); + liabilityGla.CreatedAt.Should().Be(new DateTime(2024, 1, 22, 17, 43, 38, DateTimeKind.Utc)); + liabilityGla.UpdatedAt.Should().Be(new DateTime(2024, 1, 22, 17, 43, 38, DateTimeKind.Utc)); + + revenueGla.Id.Should().Be("lagie9mxu2ap"); + revenueGla.AccountType.Should().Be(GeneralLedgerAccountType.Revenue); + revenueGla.Code.Should().Be("200"); + revenueGla.Description.Should().Be("Another test description"); + revenueGla.CreatedAt.Should().Be(new DateTime(2024, 1, 22, 17, 53, 38, DateTimeKind.Utc)); + revenueGla.UpdatedAt.Should().Be(new DateTime(2024, 1, 22, 17, 53, 38, DateTimeKind.Utc)); + } + } +} diff --git a/Test/Recurly.Test.csproj b/Test/Recurly.Test.csproj index 4fbb7ec8..c7156640 100644 --- a/Test/Recurly.Test.csproj +++ b/Test/Recurly.Test.csproj @@ -158,6 +158,12 @@ Always + + Always + + + Always + Always