diff --git a/appveyor.yml b/appveyor.yml index b88dd36ea8..6c97fee5eb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ environment: COVERALLS_REPO_TOKEN: secure: T0PmP8uyzCseacBCDRBlti2y9Tz5DL6fknea0MKWvbPYrzADmLY2/5kOTfYIsPUk # If you bump this, don't forget to bump `MinimumMockVersion` in `StripeMockFixture.cs` as well. - STRIPE_MOCK_VERSION: 0.52.0 + STRIPE_MOCK_VERSION: 0.53.0 deploy: - provider: NuGet diff --git a/src/Stripe.net/Entities/Checkout/Sessions/Session.cs b/src/Stripe.net/Entities/Checkout/Sessions/Session.cs new file mode 100644 index 0000000000..bfb4e9f53b --- /dev/null +++ b/src/Stripe.net/Entities/Checkout/Sessions/Session.cs @@ -0,0 +1,162 @@ +namespace Stripe.Checkout +{ + using System; + using System.Collections.Generic; + using Newtonsoft.Json; + using Stripe.Infrastructure; + + public class Session : StripeEntity, IHasId, IHasObject + { + /// + /// Unique identifier for the object. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// String representing the object’s type. Objects of the same type share the same value. + /// + [JsonProperty("object")] + public string Object { get; set; } + + /// + /// Specify whether Checkout should collect the customer’s billing address. If set to + /// required, Checkout will always collect the customer’s billing address. If left + /// blank or set to auto Checkout will only collect the billing address when + /// necessary. + /// + [JsonProperty("billing_address_collection")] + public string BillingAddressCollection { get; set; } + + /// + /// The URL the customer will be directed to if they decide to go back to your website. + /// + [JsonProperty("cancel_url")] + public string CancelUrl { get; set; } + + /// + /// A unique string to reference the Checkout Session. This can be a customer ID, a cart + /// ID, or similar. It is included in the checkout.session.completed webhook and can + /// be used to fulfill the purchase. + /// + [JsonProperty("client_reference_id")] + public string ClientReferenceId { get; set; } + + #region Expandable Customer + + /// + /// ID of the customer this Session is for if one exists. + /// + [JsonIgnore] + public string CustomerId { get; set; } + + [JsonIgnore] + public Customer Customer { get; set; } + + [JsonProperty("customer")] + internal object InternalCustomer + { + get + { + return this.Customer ?? (object)this.CustomerId; + } + + set + { + StringOrObject.Map(value, s => this.CustomerId = s, o => this.Customer = o); + } + } + #endregion + + /// + /// The email address used to create the customer object. + /// + [JsonProperty("customer_email")] + public string CustomerEmail { get; set; } + + /// + /// The line items, plans, or SKUs that were purchased by the customer. + /// + [JsonProperty("display_items")] + public List DisplayItems { get; set; } + + /// + /// Has the value true if the object exists in live mode or the value + /// false if the object exists in test mode. + /// + [JsonProperty("livemode")] + public bool Livemode { get; set; } + + /// + /// The IETF language tag of the locale Checkout is displayed in. If blank or auto, + /// the browser’s locale is used. + /// + [JsonProperty("locale")] + public string Locale { get; set; } + + #region Expandable PaymentIntent + + /// + /// The ID of the PaymentIntent created if SKUs or line items were provided. + /// + [JsonIgnore] + public string PaymentIntentId { get; set; } + + [JsonIgnore] + public PaymentIntent PaymentIntent { get; set; } + + [JsonProperty("payment_intent")] + internal object InternalPaymentIntent + { + get + { + return this.PaymentIntent ?? (object)this.PaymentIntentId; + } + + set + { + StringOrObject.Map(value, s => this.PaymentIntentId = s, o => this.PaymentIntent = o); + } + } + #endregion + + /// + /// The list of payment method types (e.g. card) that this Checkout Session is allowed to + /// use. + /// + [JsonProperty("payment_method_types")] + public List PaymentMethodTypes { get; set; } + + #region Expandable Subscription + + /// + /// The ID of the subscription created if one or more plans were provided. + /// + [JsonProperty("subscription")] + public string SubscriptionId { get; set; } + + [JsonIgnore] + public Subscription Subscription { get; set; } + + [JsonProperty("subscription")] + internal object InternalSubscription + { + get + { + return this.Subscription ?? (object)this.SubscriptionId; + } + + set + { + StringOrObject.Map(value, s => this.SubscriptionId = s, o => this.Subscription = o); + } + } + #endregion + + /// + /// The URL the customer will be directed to after a successful payment. + /// + [JsonProperty("success_url")] + public string SuccessUrl { get; set; } + } +} diff --git a/src/Stripe.net/Entities/Checkout/Sessions/SessionDisplayItem.cs b/src/Stripe.net/Entities/Checkout/Sessions/SessionDisplayItem.cs new file mode 100644 index 0000000000..ad6440b64d --- /dev/null +++ b/src/Stripe.net/Entities/Checkout/Sessions/SessionDisplayItem.cs @@ -0,0 +1,49 @@ +namespace Stripe +{ + using Newtonsoft.Json; + + public class SessionDisplayItem : StripeEntity + { + /// + /// Amount for the display item. + /// + [JsonProperty("amount")] + public long? Amount { get; set; } + + /// + /// Three-letter ISO currency code, in lowercase. Must be a supported currency. + /// + [JsonProperty("currency")] + public string Currency { get; set; } + + /// + /// Details about the display item if it's of type custom. + /// + [JsonProperty("custom")] + public SessionDisplayItemCustom Custom { get; set; } + + /// + /// The Plan if the display item is of type plan. + /// + [JsonProperty("plan")] + public Plan Plan { get; set; } + + /// + /// Quantity of the display item being purchased. + /// + [JsonProperty("quantity")] + public long? Quantity { get; set; } + + /// + /// The Sku if the display item is of type sku. + /// + [JsonProperty("sku")] + public Sku Sku { get; set; } + + /// + /// The type of display item. + /// + [JsonProperty("type")] + public string Type { get; set; } + } +} diff --git a/src/Stripe.net/Entities/Checkout/Sessions/SessionDisplayItemCustom.cs b/src/Stripe.net/Entities/Checkout/Sessions/SessionDisplayItemCustom.cs new file mode 100644 index 0000000000..aa862a8eb7 --- /dev/null +++ b/src/Stripe.net/Entities/Checkout/Sessions/SessionDisplayItemCustom.cs @@ -0,0 +1,26 @@ +namespace Stripe +{ + using System.Collections.Generic; + using Newtonsoft.Json; + + public class SessionDisplayItemCustom : StripeEntity + { + /// + /// The description of the line item. + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// The images of the line item. + /// + [JsonProperty("images")] + public List Images { get; set; } + + /// + /// The name of the line item. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Stripe.net/Infrastructure/StripeTypeRegistry.cs b/src/Stripe.net/Infrastructure/StripeTypeRegistry.cs index 95598a2f30..946197ef19 100644 --- a/src/Stripe.net/Infrastructure/StripeTypeRegistry.cs +++ b/src/Stripe.net/Infrastructure/StripeTypeRegistry.cs @@ -23,6 +23,7 @@ internal static class StripeTypeRegistry { "bank_account", typeof(BankAccount) }, { "card", typeof(Card) }, { "charge", typeof(Charge) }, + { "checkout.session", typeof(Checkout.Session) }, { "country_spec", typeof(CountrySpec) }, { "coupon", typeof(Coupon) }, { "customer", typeof(Customer) }, diff --git a/src/Stripe.net/Services/Checkout/SessionCreateOptions.cs b/src/Stripe.net/Services/Checkout/SessionCreateOptions.cs new file mode 100644 index 0000000000..fb304d951a --- /dev/null +++ b/src/Stripe.net/Services/Checkout/SessionCreateOptions.cs @@ -0,0 +1,86 @@ +namespace Stripe.Checkout +{ + using System; + using System.Collections.Generic; + using Newtonsoft.Json; + using Stripe.Infrastructure; + + public class SessionCreateOptions : BaseOptions + { + /// + /// Specify whether Checkout should collect the customer’s billing address. If set to + /// required, Checkout will always collect the customer’s billing address. If left + /// blank or set to auto Checkout will only collect the billing address when + /// necessary. + /// + [JsonProperty("billing_address_collection")] + public string BillingAddressCollection { get; set; } + + /// + /// The URL the customer will be directed to if they decide to go back to your website. + /// + [JsonProperty("cancel_url")] + public string CancelUrl { get; set; } + + /// + /// A unique string to reference the Checkout Session. This can be a customer ID, a cart + /// ID, or similar. It is included in the checkout.session.completed webhook and can + /// be used to fulfill the purchase. + /// + [JsonProperty("client_reference_id")] + public string ClientReferenceId { get; set; } + + /// + /// The email address used to create the customer object. If you already know your + /// customer’s email address, use this attribute to prefill it on Checkout. + /// + [JsonProperty("customer_email")] + public string CustomerEmail { get; set; } + + /// + /// ID of the customer this Checkout Session is for if one exists. May only be used with + /// LineItems. Usage with SubscriptionData is not yet available. + /// + [JsonProperty("customer")] + public string CustomerId { get; set; } + + /// + /// A list of items your customer is purchasing. + /// + [JsonProperty("line_items")] + public List LineItems { get; set; } + + /// + /// The IETF language tag of the locale Checkout is displayed in. If blank or auto, + /// the browser’s locale is used. + /// + [JsonProperty("locale")] + public string Locale { get; set; } + + /// + /// The list of payment method types (e.g. card) that this Checkout Session is allowed to + /// use. + /// + [JsonProperty("payment_intent_data")] + public SessionPaymentIntentDataOptions PaymentIntentData { get; set; } + + /// + /// The list of payment method types (e.g. card) that this Checkout Session is allowed to + /// use. + /// + [JsonProperty("payment_method_types")] + public List PaymentMethodTypes { get; set; } + + /// + /// A subset of parameters to be passed to subscription creation. + /// + [JsonProperty("subscription_data")] + public SessionSubscriptionDataOptions SubscriptionData { get; set; } + + /// + /// The URL the customer will be directed to after a successful payment. + /// + [JsonProperty("success_url")] + public string SuccessUrl { get; set; } + } +} diff --git a/src/Stripe.net/Services/Checkout/SessionLineItemOptions.cs b/src/Stripe.net/Services/Checkout/SessionLineItemOptions.cs new file mode 100644 index 0000000000..f3ceb68d49 --- /dev/null +++ b/src/Stripe.net/Services/Checkout/SessionLineItemOptions.cs @@ -0,0 +1,44 @@ +namespace Stripe.Checkout +{ + using System.Collections.Generic; + using Newtonsoft.Json; + + public class SessionLineItemOptions : INestedOptions + { + /// + /// Per item amount to be collected + /// + [JsonProperty("amount")] + public long? Amount { get; set; } + + /// + /// Three-letter ISO currency code, in lowercase. Must be a supported currency. + /// + [JsonProperty("currency")] + public string Currency { get; set; } + + /// + /// The description for the line item. + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// A list of images representing this line item. + /// + [JsonProperty("images")] + public List Images { get; set; } + + /// + /// The name for the line item. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Quantity of the line item being purchased. + /// + [JsonProperty("quantity")] + public long? Quantity { get; set; } + } +} diff --git a/src/Stripe.net/Services/Checkout/SessionPaymentIntentDataOptions.cs b/src/Stripe.net/Services/Checkout/SessionPaymentIntentDataOptions.cs new file mode 100644 index 0000000000..4587c4d1a6 --- /dev/null +++ b/src/Stripe.net/Services/Checkout/SessionPaymentIntentDataOptions.cs @@ -0,0 +1,65 @@ +namespace Stripe.Checkout +{ + using System.Collections.Generic; + using Newtonsoft.Json; + + public class SessionPaymentIntentDataOptions : INestedOptions + { + /// + /// The amount of the application fee (if any) that will be applied to the payment and + /// transferred to the application owner’s Stripe account. + /// + [JsonProperty("application_fee_amount")] + public long? ApplicationFeeAmount { get; set; } + + /// + /// Capture method of this PaymentIntent, one of automatic or manual. + /// + [JsonProperty("capture_method")] + public string CaptureMethod { get; set; } + + /// + /// An arbitrary string attached to the object. Often useful for displaying to users. + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Set of key-value pairs that you can attach to an object. This can be useful for storing + /// additional information about the object in a structured format. + /// + [JsonProperty("metadata")] + public Dictionary Metadata { get; set; } + + /// + /// The Stripe account ID for which these funds are intended. + /// + [JsonProperty("on_behalf_of")] + public string OnBehalfOf { get; set; } + + /// + /// Email address that the receipt for the resulting payment will be sent to. + /// + [JsonProperty("receipt_email")] + public string ReceiptEmail { get; set; } + + /// + /// Shipping information for this payment. + /// + [JsonProperty("shipping")] + public ChargeShippingOptions Shipping { get; set; } + + /// + /// Extra information about the payment. This will appear on your customer’s statement when + /// this payment succeeds in creating a charge. + /// + [JsonProperty("statement_descriptor")] + public string StatementDescriptor { get; set; } + + /// + /// The parameters used to automatically create a Transfer when the payment succeeds. + /// + [JsonProperty("transfer_data")] + public SessionPaymentIntentTransferDataOptions TransferData { get; set; } + } +} diff --git a/src/Stripe.net/Services/Checkout/SessionPaymentIntentTransferDataOptions.cs b/src/Stripe.net/Services/Checkout/SessionPaymentIntentTransferDataOptions.cs new file mode 100644 index 0000000000..af2464dd20 --- /dev/null +++ b/src/Stripe.net/Services/Checkout/SessionPaymentIntentTransferDataOptions.cs @@ -0,0 +1,11 @@ +namespace Stripe +{ + using System; + using Newtonsoft.Json; + + public class SessionPaymentIntentTransferDataOptions : INestedOptions + { + [JsonProperty("destination")] + public string Destination { get; set; } + } +} diff --git a/src/Stripe.net/Services/Checkout/SessionService.cs b/src/Stripe.net/Services/Checkout/SessionService.cs new file mode 100644 index 0000000000..31d9bcb1ed --- /dev/null +++ b/src/Stripe.net/Services/Checkout/SessionService.cs @@ -0,0 +1,44 @@ +namespace Stripe.Checkout +{ + using System.Collections.Generic; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using Stripe.Infrastructure; + + public class SessionService : Service, + ICreatable + { + public SessionService() + : base(null) + { + } + + public SessionService(string apiKey) + : base(apiKey) + { + } + + public override string BasePath => "/checkout/sessions"; + + public virtual Session Create(SessionCreateOptions options, RequestOptions requestOptions = null) + { + return this.CreateEntity(options, requestOptions); + } + + public virtual Task CreateAsync(SessionCreateOptions options, RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return this.CreateEntityAsync(options, requestOptions, cancellationToken); + } + + public virtual Session Get(string sessionId, RequestOptions requestOptions = null) + { + return this.GetEntity(sessionId, null, requestOptions); + } + + public virtual Task GetAsync(string sessionId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return this.GetEntityAsync(sessionId, null, requestOptions, cancellationToken); + } + } +} diff --git a/src/Stripe.net/Services/Checkout/SessionSubscriptionDataItemOptions.cs b/src/Stripe.net/Services/Checkout/SessionSubscriptionDataItemOptions.cs new file mode 100644 index 0000000000..f43d5abe42 --- /dev/null +++ b/src/Stripe.net/Services/Checkout/SessionSubscriptionDataItemOptions.cs @@ -0,0 +1,19 @@ +namespace Stripe +{ + using Newtonsoft.Json; + + public class SessionSubscriptionDataItemOptions : INestedOptions + { + /// + /// Plan ID for this item. + /// + [JsonProperty("plan")] + public string PlanId { get; set; } + + /// + /// Quantity for this item. + /// + [JsonProperty("quantity")] + public long? Quantity { get; set; } + } +} diff --git a/src/Stripe.net/Services/Checkout/SessionSubscriptionDataOptions.cs b/src/Stripe.net/Services/Checkout/SessionSubscriptionDataOptions.cs new file mode 100644 index 0000000000..8343923193 --- /dev/null +++ b/src/Stripe.net/Services/Checkout/SessionSubscriptionDataOptions.cs @@ -0,0 +1,37 @@ +namespace Stripe.Checkout +{ + using System; + using System.Collections.Generic; + using Newtonsoft.Json; + using Stripe.Infrastructure; + + public class SessionSubscriptionDataOptions : INestedOptions + { + /// + /// List of items, each with an attached plan. + /// + [JsonProperty("items")] + public List Items { get; set; } + + /// + /// Set of key-value pairs that you can attach to an object. This can be useful for storing + /// additional information about the object in a structured format. + /// + [JsonProperty("metadata")] + public Dictionary Metadata { get; set; } + + /// + /// Unix timestamp representing the end of the trial period the customer will get before + /// being charged for the first time. Has to be at least 48h in the future. + /// + [JsonProperty("trial_end")] + [JsonConverter(typeof(DateTimeConverter))] + public DateTime? TrialEnd { get; set; } + + /// + /// Integer representing the number of trial period days before the customer is charged for the first time. + /// + [JsonProperty("trial_period_days")] + public long? TrialPeriodDays { get; set; } + } +} diff --git a/src/StripeTests/Entities/Checkout/SessionTest.cs b/src/StripeTests/Entities/Checkout/SessionTest.cs new file mode 100644 index 0000000000..33489d5bed --- /dev/null +++ b/src/StripeTests/Entities/Checkout/SessionTest.cs @@ -0,0 +1,54 @@ +namespace StripeTests.Checkout +{ + using Newtonsoft.Json; + using Stripe; + using Stripe.Checkout; + using Xunit; + + public class SessionTest : BaseStripeTest + { + public SessionTest(StripeMockFixture stripeMockFixture) + : base(stripeMockFixture) + { + } + + [Fact] + public void Deserialize() + { + string json = this.GetFixture("/v1/checkout/sessions/cs_123"); + var session = JsonConvert.DeserializeObject(json); + Assert.NotNull(session); + Assert.IsType(session); + Assert.NotNull(session.Id); + Assert.Equal("checkout.session", session.Object); + } + + [Fact] + public void DeserializeWithExpansions() + { + // TODO: fix stripe-mock to properly expand application_fee + string[] expansions = + { + "customer", + "payment_intent", + "subscription", + }; + + string json = this.GetFixture("/v1/checkout/sessions/cs_123"); + var session = JsonConvert.DeserializeObject(json); + Assert.NotNull(session); + Assert.IsType(session); + Assert.NotNull(session.Id); + Assert.Equal("checkout.session", session.Object); + + Assert.NotNull(charge.Customer); + Assert.Equal("customer", charge.Customer.Object); + + Assert.NotNull(charge.PaymentIntent); + Assert.Equal("payment_intent", charge.PaymentIntent.Object); + + Assert.NotNull(charge.Subscription); + Assert.Equal("subscription", charge.Subscription.Object); + } + } +} diff --git a/src/StripeTests/Services/Checkout/SessionServiceTest.cs b/src/StripeTests/Services/Checkout/SessionServiceTest.cs new file mode 100644 index 0000000000..e3b7d0a1a5 --- /dev/null +++ b/src/StripeTests/Services/Checkout/SessionServiceTest.cs @@ -0,0 +1,103 @@ +namespace StripeTests.Checkout +{ + using System.Collections.Generic; + using System.Net.Http; + using System.Threading.Tasks; + + using Stripe; + using Stripe.Checkout; + using Xunit; + + public class SessionServiceTest : BaseStripeTest + { + private const string SessionId = "cs_123"; + private readonly SessionService service; + private readonly SessionCreateOptions createOptions; + + public SessionServiceTest(MockHttpClientFixture mockHttpClientFixture) + : base(mockHttpClientFixture) + { + this.service = new SessionService(); + + this.createOptions = new SessionCreateOptions + { + CancelUrl = "https://stripe.com/cancel", + ClientReferenceId = "1234", + LineItems = new List + { + new SessionLineItemOptions + { + Amount = 1234, + Currency = "usd", + Description = "item1", + Images = new List + { + "https://stripe.com/image1", + }, + Name = "item name", + Quantity = 2, + }, + }, + PaymentIntentData = new SessionPaymentIntentDataOptions + { + Description = "description", + Shipping = new ChargeShippingOptions + { + Name = "name", + Phone = "555-555-5555", + Address = new AddressOptions + { + State = "CA", + City = "City", + Line1 = "Line1", + Line2 = "Line2", + PostalCode = "90210", + Country = "US", + }, + } + }, + PaymentMethodTypes = new List + { + "card", + }, + SuccessUrl = "https://stripe.com/success", + }; + } + + [Fact] + public void Create() + { + var session = this.service.Create(this.createOptions); + this.AssertRequest(HttpMethod.Post, "/v1/checkout/sessions"); + Assert.NotNull(session); + Assert.Equal("checkout.session", session.Object); + } + + [Fact] + public async Task CreateAsync() + { + var session = await this.service.CreateAsync(this.createOptions); + this.AssertRequest(HttpMethod.Post, "/v1/checkout/sessions"); + Assert.NotNull(session); + Assert.Equal("checkout.session", session.Object); + } + + [Fact] + public void Get() + { + var session = this.service.Get(SessionId); + this.AssertRequest(HttpMethod.Get, "/v1/checkout/sessions/cs_123"); + Assert.NotNull(session); + Assert.Equal("checkout.session", session.Object); + } + + [Fact] + public async Task GetAsync() + { + var session = await this.service.GetAsync(SessionId); + this.AssertRequest(HttpMethod.Get, "/v1/checkout/sessions/cs_123"); + Assert.NotNull(session); + Assert.Equal("checkout.session", session.Object); + } + } +} diff --git a/src/StripeTests/StripeMockFixture.cs b/src/StripeTests/StripeMockFixture.cs index f21553764c..a432bcb961 100644 --- a/src/StripeTests/StripeMockFixture.cs +++ b/src/StripeTests/StripeMockFixture.cs @@ -12,7 +12,7 @@ public class StripeMockFixture : IDisposable /// /// If you bump this, don't forget to bump `STRIPE_MOCK_VERSION` in `appveyor.yml` as well. /// - private const string MockMinimumVersion = "0.52.0"; + private const string MockMinimumVersion = "0.53.0"; private readonly string origApiBase; private readonly string origFilesBase;