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

OFFI-126: Adding several Stripe API endpoints and basics of Subscription #510

Merged
merged 53 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f5301e4
WIP: Stripe headless
wAsnk Oct 9, 2024
869b9d2
Fix reference
wAsnk Oct 9, 2024
53fcc82
Make stripe workflow possible on headless
wAsnk Oct 10, 2024
6b7b012
Fixing possible NRE
wAsnk Oct 10, 2024
47074f9
Deleting not used viewmodels
wAsnk Oct 11, 2024
e33e2dc
Fixing folder typo
wAsnk Oct 11, 2024
57d8d7c
Code cleanup
wAsnk Oct 11, 2024
1264c77
Code cleanup
wAsnk Oct 11, 2024
ffdb1bd
More cleanup
wAsnk Oct 11, 2024
d70dd40
Adding changes so order creation is easy through minimal api
wAsnk Oct 16, 2024
93b90fa
Adding subscription elements
wAsnk Oct 17, 2024
c6c2b01
Adding stripe product part
wAsnk Oct 24, 2024
869d35f
Adding logic for subscription and stripe checkout
wAsnk Oct 28, 2024
08d5be4
Merge remote-tracking branch 'origin/main' into issue/OFFI-126
wAsnk Oct 28, 2024
593d5f7
Separating and adding logic supporting stripe subscription
wAsnk Oct 30, 2024
abfca0f
Code refactoring and drying
wAsnk Oct 30, 2024
7225adb
Using actual function
wAsnk Oct 31, 2024
babd6ec
Fixing null issue
wAsnk Oct 31, 2024
8af23db
Drying and refactoring
wAsnk Oct 31, 2024
e0b199d
Drying and refactoring and documenting
wAsnk Oct 31, 2024
f864ddb
Merge remote-tracking branch 'origin/issue/OFFI-126' into issue/OFFI-126
wAsnk Oct 31, 2024
813fbf0
Start date should not be overriden every time
wAsnk Nov 11, 2024
c1101de
Should be datetime not date
wAsnk Nov 11, 2024
0b8b196
Added permissions
wAsnk Nov 11, 2024
19ffc43
Adding new part of the subscription update logic
wAsnk Nov 11, 2024
15af68e
Use the actual start date
wAsnk Nov 12, 2024
170b1ac
Merge remote-tracking branch 'origin/main' into issue/OFFI-126
wAsnk Nov 12, 2024
f28d7a1
Making populate function public
wAsnk Nov 13, 2024
7b585c1
Removing update step
wAsnk Nov 13, 2024
76ba8c9
Refactoring and documenting
wAsnk Nov 18, 2024
0d3db3c
Adding search
wAsnk Nov 18, 2024
740f436
Merge remote-tracking branch 'origin/main' into issue/OFFI-126
wAsnk Nov 18, 2024
3352a07
Moving stripe services to use DI
wAsnk Nov 20, 2024
f7b613e
Adding get subscription endpoint
wAsnk Nov 20, 2024
340a15d
Mocking services
wAsnk Nov 20, 2024
696ec98
Adding testing possibility
wAsnk Nov 20, 2024
8d7547b
Simple textfield
wAsnk Nov 20, 2024
dff9578
Creating new event data for everything
wAsnk Nov 21, 2024
bf8aa31
Deleting not needed using
wAsnk Nov 21, 2024
a882964
Fixing altering
wAsnk Nov 21, 2024
740fa72
Small refactoring
wAsnk Nov 21, 2024
e93ee04
Fixing spelling stuff
wAsnk Nov 21, 2024
a9261d3
Adding reference
wAsnk Nov 21, 2024
7b5a847
Adding version
wAsnk Nov 21, 2024
3065caf
Adding newest version
wAsnk Nov 21, 2024
ef81b69
Update Directory.Packages.props
wAsnk Nov 28, 2024
ceb2f50
Abstracting logic so it is not that stripe related
wAsnk Nov 28, 2024
82f3c2e
Renaming
wAsnk Nov 28, 2024
38c2e99
Sorting out endpoint paths
wAsnk Nov 28, 2024
bc4b777
Adding breaking changes
wAsnk Nov 28, 2024
e0cfbed
Not needed using
wAsnk Nov 28, 2024
eb5b612
Removing not needed usings
wAsnk Nov 28, 2024
95265df
Using list style
wAsnk Nov 28, 2024
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
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
should use Orchard Core references for the latest patch version to pull all versions up in the final app. -->
<OrchardCoreVersion>2.0.0</OrchardCoreVersion>

<LombiqHelpfulLibrariesVersion>11.0.0</LombiqHelpfulLibrariesVersion>
<LombiqHelpfulLibrariesVersion>11.0.1-alpha.0.offi-126</LombiqHelpfulLibrariesVersion>
<LombiqTestsUIVersion>11.0.0</LombiqTestsUIVersion>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="Lombiq.Analyzers.OrchardCore" Version="5.0.0" />
<PackageVersion Include="Lombiq.HelpfulLibraries.OrchardCore" Version="$(LombiqHelpfulLibrariesVersion)" />
<PackageVersion Include="Lombiq.HelpfulLibraries.AspNetCore" Version="$(LombiqHelpfulLibrariesVersion)" />
<PackageVersion Include="Lombiq.HelpfulLibraries.Refit" Version="$(LombiqHelpfulLibrariesVersion)" />
<PackageVersion Include="Lombiq.NodeJs.Extensions" Version="2.1.0" />
<PackageVersion Include="Lombiq.Tests" Version="3.0.0" />
Expand All @@ -29,6 +30,7 @@
<PackageVersion Include="OrchardCore.ContentManagement.Abstractions" Version="$(OrchardCoreVersion)" />
<PackageVersion Include="OrchardCore.ContentTypes" Version="$(OrchardCoreVersion)" />
<PackageVersion Include="OrchardCore.ContentTypes.Abstractions" Version="$(OrchardCoreVersion)" />
<PackageVersion Include="OrchardCore.Flows" Version="$(OrchardCoreVersion)" />
<PackageVersion Include="OrchardCore.Html" Version="$(OrchardCoreVersion)" />
<PackageVersion Include="OrchardCore.Indexing.Abstractions" Version="$(OrchardCoreVersion)" />
<PackageVersion Include="OrchardCore.Localization" Version="$(OrchardCoreVersion)" />
Expand Down
5 changes: 5 additions & 0 deletions docs/releases/3.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ The `order_line_item_view_models_and_tax_rates` Liquid filter has been removed.

The new `amount_to_string` filter processes the input object as `Amount` (like the `amount` filter) and correctly formats it just like the `Amount.ToString()` override in C#. You can use `amount_to_string: dot: ","` to make it display a comma as the decimal separator when it would use a dot. Unlike `amount`, you can also use this filter on a number with the `currency: "three-letter-code""` argument (e.g. `{{ value | amount_to_string: currency: "EUR" }}`). This will display any numeric value as the given currency.

### Stripe controllers and endpoints

- The `PaymentConfirmationMiddleware` action with the `checkout/middleware/Stripe` path in `StripeController` was changed to `PaymentConfirmation` and its path to `stripe/middleware`.
- The `ConfirmPaymentParameters` action with the `checkout/params/Stripe` path in `StripeController` was change to `stripe/params` path.

## Change Logs

Please check the GitHub release entry [here](https://github.com/OrchardCMS/OrchardCore.Commerce/releases/tag/v3.0.0).
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public static class ContentTypes
public const string ShoppingCartWidget = nameof(ShoppingCartWidget);
public const string UserAddresses = nameof(UserAddresses);
public const string UserDetails = nameof(UserDetails);
public const string Subscription = nameof(Subscription);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;

namespace OrchardCore.Commerce.Abstractions.ViewModels;

public class ShoppingCartViewModel
{
public string Id { get; set; }

[JsonIgnore]
public IList<LocalizedHtmlString> InvalidReasons { get; } = new List<LocalizedHtmlString>();

[JsonIgnore]
public IList<LocalizedHtmlString> Headers { get; } = new List<LocalizedHtmlString>();

[JsonIgnore]
public IList<List<IShape>> TableShapes { get; } = new List<List<IShape>>();
public IList<ShoppingCartLineViewModel> Lines { get; } = new List<ShoppingCartLineViewModel>();
public IList<Amount> Totals { get; } = new List<Amount>();
Expand Down
30 changes: 30 additions & 0 deletions src/Libraries/OrchardCore.Commerce.MoneyDataType/Amount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using OrchardCore.Commerce.MoneyDataType.Abstractions;
using OrchardCore.Commerce.MoneyDataType.Serialization;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -82,6 +83,35 @@ public int CompareTo(Amount other)
public Amount GetRounded() =>
new(Math.Round(Value, Currency.DecimalPlaces), Currency);

/// <summary>
/// Converts the <see cref="Amount"/> to a fixed-point fractional value by keeping some digits based on the <see
/// cref="ICurrency.CurrencyIsoCode"/>.
/// </summary>
/// <param name="roundingByCurrencyCode">
/// Provides exceptional rounding rules for currencies that aren't converted according to the default. The key is
/// the <see cref="Currency"/>'s ISO code, the value pairs follow the same logic as the matching default parameters.
/// </param>
/// <param name="defaultKeepDigits">Indicates how many digits should be kept after the decimal point.</param>
/// <param name="defaultRoundTens">
/// If positive, the <see cref="Amount"/> is rounded to this many digits before converted to a fixed-point
/// fractional. Ignored otherwise.
/// </param>
public long GetFixedPointAmount(
IDictionary<string, (int KeepDigits, int RoundTens)> roundingByCurrencyCode,
int defaultKeepDigits = 2,
int defaultRoundTens = 0)
{
static int Tens(int zeroes) => (int)Math.Pow(10, zeroes);

var (keepDigits, roundTens) = roundingByCurrencyCode.TryGetValue(Currency.CurrencyIsoCode, out var pair)
? pair
: (defaultKeepDigits, defaultRoundTens);

return roundTens > 0
? (long)Math.Round(Value / Tens(roundTens)) * Tens(roundTens + keepDigits)
: (long)Math.Round(Value * Tens(keepDigits));
}

private void ThrowIfCurrencyDoesntMatch(Amount other, string operation = "compare")
{
if (Currency.Equals(other.Currency)) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Stripe;
using System.Threading.Tasks;

namespace OrchardCore.Commerce.Payment.Stripe.Abstractions;

/// <summary>
/// Service for managing Stripe confirmation tokens.
/// </summary>
public interface IStripeConfirmationTokenService
{
/// <summary>
/// Gets the Stripe confirmation token with an Id of <paramref name="confirmationTokenId"/>.
/// </summary>
/// <returns>The Stripe <see cref="ConfirmationToken"/>.</returns>
Task<ConfirmationToken> GetConfirmationTokenAsync(string confirmationTokenId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Stripe;
using System.Threading.Tasks;
using Address = OrchardCore.Commerce.AddressDataType.Address;

namespace OrchardCore.Commerce.Payment.Stripe.Abstractions;

/// <summary>
/// Stripe customer related services.
/// </summary>
public interface IStripeCustomerService
{
/// <summary>
/// Search for customers in Stripe with the given <paramref name="options"/>.
/// </summary>
Task<StripeSearchResult<Customer>> SearchCustomersAsync(CustomerSearchOptions options);

/// <summary>
/// Get the first customer with the given email in Stripe.
/// </summary>
Task<Customer> GetFirstCustomerByEmailAsync(string customerEmail);

/// <summary>
/// Returns <see cref="Customer"/> with the given Id in Stripe.
/// </summary>
Task<Customer> GetCustomerByIdAsync(string customerId);

/// <summary>
/// Returns <see cref="Customer"/> with the given email in Stripe. If not found, create a new customer.
/// </summary>
/// <param name="email">If not provided the current user's email will be used.</param>
Task<Customer> GetAndUpdateOrCreateCustomerAsync(
Address billingAddress,
Address shippingAddress,
string email,
string phone);

/// <summary>
/// Create a new customer in Stripe with the given <paramref name="customerCreateOptions"/>.
/// </summary>
/// <returns>The created Stripe <see cref="Customer"/>.</returns>
Task<Customer> CreateCustomerAsync(CustomerCreateOptions customerCreateOptions);

/// <summary>
/// Create the customer in Stripe with the given details which will be used to create the
/// <see cref="CustomerCreateOptions"/>.
/// </summary>
/// <returns>The created Stripe <see cref="Customer"/>.</returns>
Task<Customer> CreateCustomerAsync(
Address billingAddress,
Address shippingAddress,
string email,
string phone);

/// <summary>
/// Update the customer in Stripe with the given details.
/// </summary>
/// <returns>The updated Stripe <see cref="Customer"/>.</returns>
Task<Customer> UpdateCustomerAsync(
string customerId,
Address billingAddress,
Address shippingAddress,
string email,
string phone);

/// <summary>
/// Populate the returned <see cref="CustomerCreateOptions"/> with the given details.
/// </summary>
CustomerCreateOptions PopulateCustomerCreateOptions(
Address billingAddress,
Address shippingAddress,
string email,
string phone);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Stripe;

namespace OrchardCore.Commerce.Payment.Stripe.Abstractions;

/// <summary>
/// Stripe helping services, needed so we can mock this part of Stripe also.
/// </summary>
public interface IStripeHelperService
{
/// <summary>
/// Parses a JSON string from a Stripe webhook into a <see cref="Event"/> object, while
/// verifying the <a href="https://stripe.com/docs/webhooks/signatures">webhook's
/// signature</a>.
/// </summary>
/// <param name="json">The JSON string to parse.</param>
/// <param name="stripeSignatureHeader">
/// The value of the <c>Stripe-Signature</c> header from the webhook request.
/// </param>
/// <param name="secret">The webhook endpoint's signing secret.</param>
/// <param name="throwOnApiVersionMismatch">
/// If <see langword="true"/> (default), the method will throw a <see cref="StripeException"/> if the
/// API version of the event doesn't match Stripe.net's default API version (see
/// <see cref="StripeConfiguration.ApiVersion"/>).
/// </param>
/// <returns>The deserialized <see cref="Event"/>.</returns>
/// <exception cref="StripeException">
/// Thrown if the signature verification fails for any reason, of if the API version of the
/// event doesn't match Stripe.net's default API version.
/// </exception>
Event PrepareStripeEvent(string json, string stripeSignatureHeader, string secret, bool throwOnApiVersionMismatch);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using OrchardCore.Commerce.MoneyDataType;
using OrchardCore.Commerce.Payment.Stripe.Constants;
using Stripe;
using System.Threading.Tasks;

namespace OrchardCore.Commerce.Payment.Stripe.Abstractions;

/// <summary>
/// Service for managing Stripe Payment Intents.
/// </summary>
public interface IStripePaymentIntentService
{
/// <summary>
/// Gets a PaymentIntent by its Stripe Id.
/// </summary>
/// <returns>Stripe <see cref="PaymentIntent"/> model.</returns>
Task<PaymentIntent> GetPaymentIntentAsync(string paymentIntentId);

/// <summary>
/// Gets the PaymentIntent by its Stripe Id if it is <see cref="PaymentIntentStatuses.Succeeded"/> or
/// <see cref="PaymentIntentStatuses.Processing"/>. Otherwise, updates it with the provided
/// <paramref name="defaultTotal"/>.
/// </summary>
/// <returns>Updated or original Stripe <see cref="PaymentIntent"/> model.</returns>
Task<PaymentIntent> GetOrUpdatePaymentIntentAsync(
string paymentIntentId,
Amount defaultTotal);

/// <summary>
/// Creates a PaymentIntent with the provided <paramref name="total"/>. And adds description and other values to
/// the payment intent. Check the implementation for more details.
/// </summary>
/// <returns>Created Stripe <see cref="PaymentIntent"/>.</returns>
Task<PaymentIntent> CreatePaymentIntentAsync(Amount total);

/// <summary>
/// Creates a PaymentIntent with the provided <paramref name="options"/>.
/// </summary>
/// <returns>Created Stripe <see cref="PaymentIntent"/> model.</returns>
Task<PaymentIntent> CreatePaymentIntentAsync(PaymentIntentCreateOptions options);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,14 @@ namespace OrchardCore.Commerce.Payment.Stripe.Abstractions;
public interface IStripePaymentService
{
/// <summary>
/// Handles the payment and authentication, sends back the necessary data to the client./>.
/// </summary>
Task<string> CreateClientSecretAsync(Amount total, ShoppingCartViewModel cart);

/// <summary>
/// Returns a <see cref="PaymentIntent"/> object for the given <paramref name="paymentIntentId"/>.
/// Returns the public key of the Stripe account.
/// </summary>
Task<PaymentIntent> GetPaymentIntentAsync(string paymentIntentId);
Task<string> GetPublicKeyAsync();

/// <summary>
/// Returns a <see cref="PaymentIntent"/> object based on the given <paramref name="total"/>.
/// Handles the payment and authentication, sends back the necessary data to the client./>.
/// </summary>
Task<PaymentIntent> CreatePaymentIntentAsync(Amount total);
Task<string> CreateClientSecretAsync(Amount total, ShoppingCartViewModel cart);

/// <summary>
/// Creates an order content item in the database, based on the stored <see cref="PaymentIntent"/> and on the
Expand All @@ -39,7 +34,8 @@ public interface IStripePaymentService
Task<ContentItem> CreateOrUpdateOrderFromShoppingCartAsync(
IUpdateModelAccessor updateModelAccessor,
string shoppingCartId,
string paymentIntentId = null);
string paymentIntentId = null,
OrderPart orderPart = null);

/// <summary>
/// Updates the corresponding order status to <see cref="OrderStatuses.Ordered"/> for the given
Expand All @@ -57,6 +53,11 @@ Task<ContentItem> CreateOrUpdateOrderFromShoppingCartAsync(
/// </summary>
Task<OrderPayment> GetOrderPaymentByPaymentIntentIdAsync(string paymentIntentId);

/// <summary>
/// Save the order payment for the given <paramref name="orderContentItemId"/> and <paramref name="paymentIntentId"/>.
/// </summary>
Task SaveOrderPaymentAsync(string orderContentItemId, string paymentIntentId);

/// <summary>
/// A shortcut method for updating the <paramref name="order"/> status to <see cref="OrderStatuses.Ordered"/>, doing
/// final modifications and then redirecting to the success page.
Expand All @@ -70,8 +71,10 @@ string shoppingCartId
/// <summary>
/// Get the confirmation parameters for Stripe.
/// </summary>
/// <param name="middlewareAbsoluteUrl">The url for the middleware of Stripe.</param>
Task<PaymentIntentConfirmOptions> GetStripeConfirmParametersAsync(string middlewareAbsoluteUrl);
/// <param name="returnUrl">The url for the middleware of Stripe.</param>
Task<PaymentIntentConfirmOptions> GetStripeConfirmParametersAsync(
string returnUrl,
ContentItem order = null);

/// <summary>
/// Confirm the result of Stripe payment.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Stripe.Checkout;
using System.Threading.Tasks;

namespace OrchardCore.Commerce.Payment.Stripe.Abstractions;

/// <summary>
/// Event handler for Stripe sessions.
/// </summary>
public interface IStripeSessionEventHandler
{
/// <summary>
/// Called before a Stripe session is created with a pre-populated <see cref="SessionCreateOptions"/>
/// <paramref name="options"/>. Here you can modify the options before the session is created.
/// </summary>
Task StripeSessionCreatingAsync(SessionCreateOptions options) => Task.CompletedTask;

/// <summary>
/// Called after a Stripe session is created with the created <paramref name="session"/> and the
/// <paramref name="options"/> used during creation.
/// </summary>
Task StripeSessionCreatedAsync(Session session, SessionCreateOptions options) => Task.CompletedTask;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Stripe.Checkout;
using System.Threading.Tasks;

namespace OrchardCore.Commerce.Payment.Stripe.Abstractions;

/// <summary>
/// Service for managing Stripe sessions.
/// </summary>
public interface IStripeSessionService
{
/// <summary>
/// Creates a Stripe session using the given <paramref name="options"/>.
/// </summary>
/// <returns>The created Stripe <see cref="Session"/>.</returns>
Task<Session> CreateSessionAsync(SessionCreateOptions options);
}
Loading
Loading