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

Interfaces for polymorphic API resources (external accounts and payment sources) #1320

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
5 changes: 2 additions & 3 deletions src/Stripe.net/Entities/Accounts/Account.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ namespace Stripe
{
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Stripe.Infrastructure;

public class Account : StripeEntity, IHasId, IHasObject
public class Account : StripeEntity, IHasId, IHasObject, IPaymentSource
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we implement IPaymentSource on Account? Forgot Account is a source now, damn that is confusing lol

{
[JsonProperty("id")]
public string Id { get; set; }
Expand Down Expand Up @@ -73,7 +72,7 @@ internal object InternalBusinessLogo
public string Email { get; set; }

[JsonProperty("external_accounts")]
public StripeList<ExternalAccount> ExternalAccounts { get; set; }
public StripeList<IExternalAccount> ExternalAccounts { get; set; }

[JsonProperty("keys")]
public CustomAccountKeys Keys { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/Stripe.net/Entities/BankAccounts/BankAccount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Stripe
using Newtonsoft.Json;
using Stripe.Infrastructure;

public class BankAccount : StripeEntity, IHasId, IHasObject
public class BankAccount : StripeEntity, IHasId, IHasObject, IExternalAccount, IPaymentSource
{
[JsonProperty("id")]
public string Id { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/Stripe.net/Entities/Cards/Card.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Stripe
using Newtonsoft.Json;
using Stripe.Infrastructure;

public class Card : StripeEntity, IHasId, IHasObject
public class Card : StripeEntity, IHasId, IHasObject, IExternalAccount, IPaymentSource
{
[JsonProperty("id")]
public string Id { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/Stripe.net/Entities/Charges/Charge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ internal object InternalReview
/// For most Stripe users, the source of every charge is a credit or debit card. This hash is then the card object describing that card.
/// </summary>
[JsonProperty("source")]
public PaymentSource Source { get; set; }
public IPaymentSource Source { get; set; }

#region Expandable Transfer

Expand Down
30 changes: 0 additions & 30 deletions src/Stripe.net/Entities/Common/PaymentSource.cs

This file was deleted.

6 changes: 3 additions & 3 deletions src/Stripe.net/Entities/Customers/Customer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ public class Customer : StripeEntity, IHasId, IHasObject
public string DefaultSourceId { get; set; }

[JsonIgnore]
public PaymentSource DefaultSource { get; set; }
public IPaymentSource DefaultSource { get; set; }

[JsonProperty("default_source")]
internal object InternalDefaultSource
{
set
{
StringOrObject<PaymentSource>.Map(value, s => this.DefaultSourceId = s, o => this.DefaultSource = o);
StringOrObject<IPaymentSource>.Map(value, s => this.DefaultSourceId = s, o => this.DefaultSource = o);
}
}

Expand Down Expand Up @@ -99,7 +99,7 @@ internal object InternalDefaultSource
/// The customer’s payment sources, if any
/// </summary>
[JsonProperty("sources")]
public StripeList<PaymentSource> Sources { get; set; }
public StripeList<IPaymentSource> Sources { get; set; }

/// <summary>
/// The customer’s current subscriptions, if any
Expand Down
24 changes: 0 additions & 24 deletions src/Stripe.net/Entities/ExternalAccounts/ExternalAccount.cs

This file was deleted.

4 changes: 2 additions & 2 deletions src/Stripe.net/Entities/PaymentIntents/PaymentIntent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,14 @@ internal object InternalReview
public string SourceId { get; set; }

[JsonIgnore]
public PaymentSource Source { get; set; }
public IPaymentSource Source { get; set; }

[JsonProperty("source")]
internal object InternalSource
{
set
{
StringOrObject<PaymentSource>.Map(value, s => this.SourceId = s, o => this.Source = o);
StringOrObject<IPaymentSource>.Map(value, s => this.SourceId = s, o => this.Source = o);
}
}
#endregion
Expand Down
4 changes: 2 additions & 2 deletions src/Stripe.net/Entities/Payouts/Payout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ internal object InternalBalanceTransaction
public string DestinationId { get; set; }

[JsonIgnore]
public ExternalAccount Destination { get; set; }
public IExternalAccount Destination { get; set; }

[JsonProperty("destination")]
internal object InternalDestination
{
set
{
StringOrObject<ExternalAccount>.Map(value, s => this.DestinationId = s, o => this.Destination = o);
StringOrObject<IExternalAccount>.Map(value, s => this.DestinationId = s, o => this.Destination = o);
}
}
#endregion
Expand Down
3 changes: 1 addition & 2 deletions src/Stripe.net/Entities/Sources/Source.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ namespace Stripe
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Stripe.Infrastructure;

/// <summary>
/// Source objects allow you to accept a variety of payment methods. They represent a customer's payment instrument and can be used with the Source API just like a card object: once chargeable, they can be charged, or attached to customers.
/// </summary>
public class Source : StripeEntity, IHasId, IHasObject
public class Source : StripeEntity, IHasId, IHasObject, IPaymentSource
{
[JsonProperty("id")]
public string Id { get; set; }
Expand Down
13 changes: 13 additions & 0 deletions src/Stripe.net/Entities/_interfaces/IExternalAccount.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Stripe
{
/// <summary>
/// Resources that implement this interface can be used as external accounts, i.e. they can
/// be attached to a Stripe account to receive payouts.
/// </summary>
public interface IExternalAccount : IStripeEntity, IHasId, IHasObject
{
Account Account { get; set; }

string AccountId { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we have properties in this interface. I thought maybe you wanted to be smart and use this for the nested service but it does not seem to be the case. Can you clarify?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having the properties declared in the interface lets you access those properties without casting, e.g.:

var payout = payoutService.Get("po_123");
var account = payout.Destination.AccountId;

vs.

var payout = payoutService.Get("po_123");
var destination = payout.Destination;
var account = null;
if (destination is BankAccount)
{
    account = ((BankAccount)destination).AccountId;
}
else if (destination is Card) {
    account = ((Card)destination).AccountId;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wanted to do something similar with IPaymentSource and Customer / CustomerId, but Account is an IPaymentSource that can never be attached to a customer :(

I could add an additional ICustomerPaymentSource interface to do this, but not sure if worth the trouble.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you access the Account id on an external account? You already know which account it's on since you retrieved the Payout on that account right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think "why" matters. Conceptually, an external account is a payment destination that is attached to a (Stripe) account, so it makes sense that the interface reflects this. It also costs us literally nothing to add this since the two concrete classes that implement this interface already fulfill that contract.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree though. I think it matters to be consistent though and the fact that it does not happen on IPaymentSource feels confusing to me and not useful. If you were using it explicitly for the service I would say maybe but here I don't see the benefits. Like you could also say the same about Last4 which is on card and bank account and really useful to access without a cast right?

This is my personal opinion and I am fine to merge as is though as I agree it ultimately works and I don't want to block.

}
}
9 changes: 9 additions & 0 deletions src/Stripe.net/Entities/_interfaces/IPaymentSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Stripe
{
/// <summary>
/// Resources that implement this interface can be used to create charges.
/// </summary>
public interface IPaymentSource : IStripeEntity, IHasId, IHasObject
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace Stripe.Infrastructure
{
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

/// <summary>
/// This converter is used to deserialize polymorphic resources. The resources must implement
/// a common interface.
/// </summary>
/// <typeparam name="I">interface implemented by all resources that this converter can
/// return</typeparam>
internal abstract class AbstractStripeObjectConverter<I> : JsonConverter
where I : IHasObject
{
public override bool CanWrite => false;

/// <summary>
/// This is the only property that needs to be declared by concrete converters. It is a
/// mapping of object names (e.g. <c>"card"</c>) to mapper functions (e.g.
/// <c>Mapper&lt;Card&gt;.FromJson</c>).
/// </summary>
protected abstract Dictionary<string, Func<string, I>> ObjectsToMapperFuncs { get; }

public override bool CanConvert(Type objectType)
{
return objectType == typeof(I);
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var incoming = JObject.FromObject(value);
incoming.WriteTo(writer);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var incoming = JObject.Load(reader);
var obj = default(I);
var objectName = incoming.SelectToken("object")?.ToString();
if (this.ObjectsToMapperFuncs.ContainsKey(objectName))
{
var mapperFunc = this.ObjectsToMapperFuncs[objectName];
obj = mapperFunc(incoming.ToString());
}

return obj;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,47 +1,15 @@
namespace Stripe.Infrastructure
{
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;

internal class ExternalAccountConverter : JsonConverter
internal class ExternalAccountConverter : AbstractStripeObjectConverter<IExternalAccount>
{
public override bool CanWrite => false;

public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var incoming = JObject.FromObject(value);

incoming.WriteTo(writer);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
protected override Dictionary<string, Func<string, IExternalAccount>> ObjectsToMapperFuncs
=> new Dictionary<string, Func<string, IExternalAccount>>()
{
var incoming = JObject.Load(reader);

var externalAccount = new ExternalAccount
{
Id = incoming.SelectToken("id").ToString()
};

if (incoming.SelectToken("object")?.ToString() == "bank_account")
{
externalAccount.Type = ExternalAccountType.BankAccount;
externalAccount.BankAccount = Mapper<BankAccount>.MapFromJson(incoming.ToString());
}

if (incoming.SelectToken("object")?.ToString() == "card")
{
externalAccount.Type = ExternalAccountType.Card;
externalAccount.Card = Mapper<Card>.MapFromJson(incoming.ToString());
}

return externalAccount;
}
{ "bank_account", Mapper<BankAccount>.MapFromJson },
{ "card", Mapper<Card>.MapFromJson },
};
}
}
Original file line number Diff line number Diff line change
@@ -1,59 +1,17 @@
namespace Stripe.Infrastructure
{
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;

internal class PaymentSourceConverter : JsonConverter
internal class PaymentSourceConverter : AbstractStripeObjectConverter<IPaymentSource>
{
public override bool CanWrite => false;

public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var incoming = JObject.FromObject(value);

incoming.WriteTo(writer);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
protected override Dictionary<string, Func<string, IPaymentSource>> ObjectsToMapperFuncs =>
new Dictionary<string, Func<string, IPaymentSource>>()
{
var incoming = JObject.Load(reader);

var source = new PaymentSource
{
Id = incoming.SelectToken("id").ToString()
};

if (incoming.SelectToken("object")?.ToString() == "account")
{
source.Type = PaymentSourceType.Account;
source.Account = Mapper<Account>.MapFromJson(incoming.ToString());
}

if (incoming.SelectToken("object")?.ToString() == "bank_account")
{
source.Type = PaymentSourceType.BankAccount;
source.BankAccount = Mapper<BankAccount>.MapFromJson(incoming.ToString());
}

if (incoming.SelectToken("object")?.ToString() == "card")
{
source.Type = PaymentSourceType.Card;
source.Card = Mapper<Card>.MapFromJson(incoming.ToString());
}

if (incoming.SelectToken("object")?.ToString() == "source")
{
source.Type = PaymentSourceType.Source;
source.SourceObject = Mapper<Source>.MapFromJson(incoming.ToString());
}

return source;
}
{ "account", Mapper<Account>.MapFromJson },
{ "bank_account", Mapper<BankAccount>.MapFromJson },
{ "card", Mapper<Card>.MapFromJson },
{ "source", Mapper<Source>.MapFromJson },
};
}
}
Loading