Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[V2] Add RevRec Performance Obligations Feature #816

Merged
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
41 changes: 41 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Contributing

## Adding a new entity

If you are adding a new entity, it is usually easier to begin with an existing entity and modify it to suit your needs. Choose a "fully-featured" entity (i.e. all CRUD operations, including a `*List` class) to start as it will be easier to delete methods rather than add methods manually. As you edit, keep the following in mind:

- If a class member shouldn't be fully accessible (i.e. `public`), then specify the access modifier (i.e. `protected` or `private`) and specify the getter/setter as appropriate (e.g. `public string FirstName { get; private set; }` restricts the value to be set internally, such as when the response XML is parsed).
- If properties can be set on create but remain readonly afterward, you will likely need another `WriteXml()` method (e.g. `WriteUpdateXml()`) that you will then need to reference in your `Update()` method.
- Decide the flexibility of multiple constructors. _Every_ entity should have the empty constructor, but you may want to add additional constructors for convenience. For example, `public Account(string accountCode)` is useful for fetching an account from the API, but `public Account(string firstName, string lastName)` is not useful because you can't create an account without an account code.

### Lists

If your entity supports an index/list endpoint, then you need to create an additional model in the `Library/List/` folder. Again, it is easier to copy/paste from an existing list and modify it to suit your needs. If your endpoint supports pagination, then ensure all of the pagination methods, along with constructors that accept `filterCriteria`, are included in your new model. Additionally, if your endpoint supports filtering with custom query params, you will likely want to define an additional constructor (or two if you want the option to omit the pagination `filterCriteria`).


## Updating an existing entity

If you are updating an existing entity, the process is less intensive. Generally, you will need to add the new class member(s) to the model and modify your `ReadXml()`/`WriteXml()` methods to read/write the new fields, respectively.


## Testing

### Unit tests

You should always strive to write unit tests for your changes. If you are adding a new entity, you should use a fixture in your unit tests to ensure that the XML parsing is correct. There are a lot of tests that are actually calling out to a site in production, but that pattern should be reversed for obvious reasons.

To add your new unit test and fixture:

1. Add your fixture(s) in `Test/Fixtures/<my_new_features>`. This usually includes at least a `show-200.xml` and `index-200.xml` file.
2. Register the fixtures within the appropriate item group in `Test/Recurly.Test.csproj`.
3. Make the code aware of the new fixture(s) by registering your feature in `Test/Fixtures/FixtureImporter.cs`. Follow the existing pattern for already-registered fixtures.
4. In your unit test, load the fixture into your model to test the XML parsing (e.g. for a "get one" endpoint):
```csharp
var myFeature = new MyFeature();

var xmlFixture = FixtureImporter.Get(FixtureType.MyFeature, "show-200").Xml;
XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(xmlFixture));
myFeature.ReadXml(reader);

myFeature.Id.Should().Be("abc123");
```
1 change: 1 addition & 0 deletions Library/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Xml;
using Recurly.Configuration;

[assembly: InternalsVisibleTo("TestRig")]
[assembly: InternalsVisibleTo("Recurly.Test")]

namespace Recurly
Expand Down
14 changes: 11 additions & 3 deletions Library/Configuration/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,17 @@ public int? RequestTimeoutMilliseconds
private set { _requestTimeoutMilliseconds = value; }
}

protected const string RecurlyServerUri = "https://{0}.recurly.com/v2{1}";
public const string RecurlyApiVersion = "2.99";
public const string ValidDomain = ".recurly.com";
protected string RecurlyServerUri
{
get
{
var portSuffix = Port != null ? $":{Port}" : string.Empty;
return "https://{0}" + ValidDomain + portSuffix + "/v2{1}";
}
}
public string RecurlyApiVersion = "2.29";
public static string ValidDomain = ".recurly.com";
public static int? Port { get; set; }

// static, unlikely to change
public string UserAgent
Expand Down
44 changes: 44 additions & 0 deletions Library/List/PerformanceObligationList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Xml;

namespace Recurly
{
public class PerformanceObligationList : RecurlyList<PerformanceObligation>
{
internal PerformanceObligationList()
{
}

internal PerformanceObligationList(string baseUrl) : base(Client.HttpRequestMethod.Get, baseUrl)
{
}

public override RecurlyList<PerformanceObligation> Start
{
get { return HasStartPage() ? new PerformanceObligationList(StartUrl) : RecurlyList.Empty<PerformanceObligation>(); }
}

public override RecurlyList<PerformanceObligation> Next
{
get { return HasNextPage() ? new PerformanceObligationList(NextUrl) : RecurlyList.Empty<PerformanceObligation>(); }
}

public override RecurlyList<PerformanceObligation> Prev
{
get { return HasPrevPage() ? new PerformanceObligationList(PrevUrl) : RecurlyList.Empty<PerformanceObligation>(); }
}

internal override void ReadXml(XmlTextReader reader)
{
while (reader.Read())
{
if (reader.Name == "performance_obligations" && reader.NodeType == XmlNodeType.EndElement)
break;

if (reader.NodeType == XmlNodeType.Element && reader.Name == "performance_obligation")
{
Add(new PerformanceObligation(reader));
}
}
}
}
}
123 changes: 123 additions & 0 deletions Library/PerformanceObligation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Xml;

namespace Recurly
{

/// <summary>
/// A general ledger account in Recurly.
///
/// </summary>
public class PerformanceObligation : RecurlyEntity
{
public string Id { get; private set; }

public string Name { get; private set; }

public DateTime? CreatedAt { get; private set; }

public DateTime? UpdatedAt { get; private set; }

internal const string UrlPrefix = "/performance_obligations/";

#region Constructors

public PerformanceObligation()
{
}

internal PerformanceObligation(XmlTextReader xmlReader)
{
ReadXml(xmlReader);
}

#endregion

internal override void ReadXml(XmlTextReader reader)
{
while (reader.Read())
{
DateTime dateVal;

if (reader.Name == "performance_obligation" && reader.NodeType == XmlNodeType.EndElement)
break;

if (reader.NodeType != XmlNodeType.Element) continue;

switch (reader.Name)
{
case "id":
Id = reader.ReadElementContentAsString();
break;

case "name":
Name = 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("performance_obligation");

xmlWriter.WriteElementString("name", Name);

xmlWriter.WriteEndElement();
}
}

public sealed class PerformanceObligations
{
internal const string UrlPrefix = "/performance_obligations/";

/// <summary>
/// Retrieves a list of all general ledger accounts.
/// </summary>
/// <returns></returns>
public static RecurlyList<PerformanceObligation> List()
{
return List(null);
}

public static RecurlyList<PerformanceObligation> List(FilterCriteria filter)
{
filter = filter == null ? FilterCriteria.Instance : filter;
return new PerformanceObligationList(PerformanceObligation.UrlPrefix + "?" + filter.ToNamedValueCollection().ToString());
}

public static PerformanceObligation Get(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return null;
}

var pob = new PerformanceObligation();

var statusCode = Client.Instance.PerformRequest(Client.HttpRequestMethod.Get,
UrlPrefix + Uri.EscapeDataString(name),
pob.ReadXml);

return statusCode == HttpStatusCode.NotFound ? null : pob;
}

}

}
2 changes: 2 additions & 0 deletions Test/Fixtures/FixtureImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,7 @@ public enum FixtureType
ExternalInvoices,
[Description("general_ledger_accounts")]
GeneralLedgerAccounts,
[Description("performance_obligations")]
PerformanceObligations,
}
}
42 changes: 42 additions & 0 deletions Test/Fixtures/performance_obligations/index-200.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
HTTP/1.1 200 OK
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<performance_obligations type="array">
<performance_obligation href="https://api.recurly.com/v2/performance_obligations/1">
<id>1</id>
<name>Material Right</name>
<created_at type="datetime">2024-01-29T23:02:30Z</created_at>
<updated_at type="datetime">2024-01-29T23:02:30Z</updated_at>
</performance_obligation>
<performance_obligation href="https://api.recurly.com/v2/performance_obligations/2">
<id>2</id>
<name>Manual Journal</name>
<created_at type="datetime">2024-01-29T23:02:30Z</created_at>
<updated_at type="datetime">2024-01-29T23:02:30Z</updated_at>
</performance_obligation>
<performance_obligation href="https://api.recurly.com/v2/performance_obligations/3">
<id>3</id>
<name>Manually Recognize</name>
<created_at type="datetime">2024-01-29T23:02:30Z</created_at>
<updated_at type="datetime">2024-01-29T23:02:30Z</updated_at>
</performance_obligation>
<performance_obligation href="https://api.recurly.com/v2/performance_obligations/4">
<id>4</id>
<name>Point in Time</name>
<created_at type="datetime">2024-01-29T23:02:30Z</created_at>
<updated_at type="datetime">2024-01-29T23:02:30Z</updated_at>
</performance_obligation>
<performance_obligation href="https://api.recurly.com/v2/performance_obligations/5">
<id>5</id>
<name>Over Time (Partial Monthly)</name>
<created_at type="datetime">2024-01-29T23:02:30Z</created_at>
<updated_at type="datetime">2024-01-29T23:02:30Z</updated_at>
</performance_obligation>
<performance_obligation href="https://api.recurly.com/v2/performance_obligations/6">
<id>6</id>
<name>Over Time (Daily)</name>
<created_at type="datetime">2024-01-29T23:02:30Z</created_at>
<updated_at type="datetime">2024-01-29T23:02:30Z</updated_at>
</performance_obligation>
</performance_obligations>
10 changes: 10 additions & 0 deletions Test/Fixtures/performance_obligations/show-200.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HTTP/1.1 200 OK
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<performance_obligation href="https://api.recurly.com/v2/performance_obligations/6">
<id>6</id>
<name>Over Time (Daily)</name>
<created_at type="datetime">2024-01-29T23:02:30Z</created_at>
<updated_at type="datetime">2024-01-29T23:02:30Z</updated_at>
</performance_obligation>
35 changes: 35 additions & 0 deletions Test/List/PerformanceObligationListTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Xml;
using FluentAssertions;
using Recurly.Test.Fixtures;

namespace Recurly.Test
{
public class PerformanceObligationListTest : BaseTest
{
[RecurlyFact(TestEnvironment.Type.Integration)]
public void List()
{
var pobs = new PerformanceObligationList();

var xmlFixture = FixtureImporter.Get(FixtureType.PerformanceObligations, "index-200").Xml;
XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(xmlFixture));
pobs.ReadXml(reader);

pobs.Should().HaveCount(6);

var firstPob = pobs[0];
var secondPob = pobs[1];

firstPob.Id.Should().Be("1");
firstPob.Name.Should().Be("Material Right");
firstPob.CreatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc));
firstPob.UpdatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc));

secondPob.Id.Should().Be("2");
secondPob.Name.Should().Be("Manual Journal");
secondPob.CreatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc));
secondPob.UpdatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc));
}
}
}
27 changes: 27 additions & 0 deletions Test/PerformanceObligationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Xml;
using FluentAssertions;

using Recurly.Test.Fixtures;


namespace Recurly.Test
{
public class PerformanceObligationTest : BaseTest
{
[RecurlyFact(TestEnvironment.Type.Integration)]
public void GetPerformanceObligation()
{
var pob = new PerformanceObligation();

var xmlFixture = FixtureImporter.Get(FixtureType.PerformanceObligations, "show-200").Xml;
XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(xmlFixture));
pob.ReadXml(reader);

pob.Id.Should().Be("6");
pob.Name.Should().Be("Over Time (Daily)");
pob.CreatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc));
pob.UpdatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc));
}
}
}
6 changes: 6 additions & 0 deletions Test/Recurly.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@
<Content Include="Fixtures\notes\index-200.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Fixtures\performance_obligations\index-200.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Fixtures\performance_obligations\show-200.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Fixtures\plans\destroy-204.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
Expand Down
Loading