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