diff --git a/.github/actions/spelling/allow/occ.txt b/.github/actions/spelling/allow/occ.txt index c169d148a..9fbab3eef 100644 --- a/.github/actions/spelling/allow/occ.txt +++ b/.github/actions/spelling/allow/occ.txt @@ -9,6 +9,7 @@ CLA contentpart CVC datetimefield +deserializers disqus emoji EUR @@ -27,8 +28,8 @@ pricefield roadmap shoppingcart skus +testdiscountedproduct testproduct testproductvariant -testdiscountedproduct unpublish webhooks diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index c846a3120..90a74b416 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -4,7 +4,7 @@ docs/releases docs/requirements\.txt$ mkdocs\.yml$ src/Libraries/OrchardCore\.Commerce\.MoneyDataType/Currency\.extra\.cs$ -src/Modules/OrchardCore\.Commerce/Constants/CurrencyCollectionConstants\.cs$ -src/Modules/OrchardCore\.Commerce/Extensions/PaymentExtensions\.cs$ +src/Modules/OrchardCore\.Commerce\.Payment/Constants/CurrencyCollectionConstants\.cs$ +src/Modules/OrchardCore\.Commerce\.Payment\.Stripe/Extensions/PaymentExtensions\.cs$ test/OrchardCore\.Commerce\.Tests/Fakes/AnkhMorporkCurrencyProvider\.cs$ test/OrchardCore\.Commerce\.Tests/ProductAttributeTests\.cs$ diff --git a/OrchardCore.Commerce.sln b/OrchardCore.Commerce.sln index ad33fbdf2..59e893909 100644 --- a/OrchardCore.Commerce.sln +++ b/OrchardCore.Commerce.sln @@ -77,12 +77,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "features", "features", "{9B docs\features\tiered-price-part.md = docs\features\tiered-price-part.md docs\features\user-features.md = docs\features\user-features.md docs\features\workflows.md = docs\features\workflows.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "releases", "releases", "{E4DD8D47-02FA-41F7-8133-CBC4419645F5}" - ProjectSection(SolutionItems) = preProject - docs\releases\0.0.1.md = docs\releases\0.0.1.md - docs\releases\1.0.0.md = docs\releases\1.0.0.md + docs\features\payment-providers.md = docs\features\payment-providers.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "actions", "actions", "{83C01924-6F58-4777-A9EC-07943F7A2E31}" @@ -108,6 +103,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "libraries", "libraries", "{ docs\resources\libraries\README.md = docs\resources\libraries\README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Commerce.Payment", "src\Modules\OrchardCore.Commerce.Payment\OrchardCore.Commerce.Payment.csproj", "{58DD682C-DA5C-4B51-BCB8-C65D690AAC67}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Commerce.Payment.Stripe", "src\Modules\OrchardCore.Commerce.Payment.Stripe\OrchardCore.Commerce.Payment.Stripe.csproj", "{A4D69733-CDC0-46AE-B46A-163CCC6F77F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Commerce.Abstractions", "src\Libraries\OrchardCore.Commerce.Abstractions\OrchardCore.Commerce.Abstractions.csproj", "{28DB6CBB-1527-42A1-8EFE-3D95BF185884}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -158,6 +159,18 @@ Global {3DB5D0DD-1509-40B8-AD1A-47D5672BF484}.Debug|Any CPU.Build.0 = Debug|Any CPU {3DB5D0DD-1509-40B8-AD1A-47D5672BF484}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DB5D0DD-1509-40B8-AD1A-47D5672BF484}.Release|Any CPU.Build.0 = Release|Any CPU + {58DD682C-DA5C-4B51-BCB8-C65D690AAC67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58DD682C-DA5C-4B51-BCB8-C65D690AAC67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58DD682C-DA5C-4B51-BCB8-C65D690AAC67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58DD682C-DA5C-4B51-BCB8-C65D690AAC67}.Release|Any CPU.Build.0 = Release|Any CPU + {A4D69733-CDC0-46AE-B46A-163CCC6F77F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4D69733-CDC0-46AE-B46A-163CCC6F77F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4D69733-CDC0-46AE-B46A-163CCC6F77F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4D69733-CDC0-46AE-B46A-163CCC6F77F9}.Release|Any CPU.Build.0 = Release|Any CPU + {28DB6CBB-1527-42A1-8EFE-3D95BF185884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28DB6CBB-1527-42A1-8EFE-3D95BF185884}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28DB6CBB-1527-42A1-8EFE-3D95BF185884}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28DB6CBB-1527-42A1-8EFE-3D95BF185884}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -177,13 +190,15 @@ Global {87C422CA-C6F2-408F-987B-C8418995A331} = {E6C02BDF-EEB0-4ABD-ADEC-9932F60923AE} {93871E12-0083-4323-BB7B-3CDA6E332D87} = {E6C02BDF-EEB0-4ABD-ADEC-9932F60923AE} {9B2DB1CD-2B4A-4823-9762-CF4E90661404} = {BEBA1764-178A-4722-A193-4DEF26DCE8D1} - {E4DD8D47-02FA-41F7-8133-CBC4419645F5} = {BEBA1764-178A-4722-A193-4DEF26DCE8D1} {83C01924-6F58-4777-A9EC-07943F7A2E31} = {4561F321-6E57-484B-950C-AC46798B1F40} {E32B62B8-D737-4713-87C5-8220C9746643} = {83C01924-6F58-4777-A9EC-07943F7A2E31} {7FB7940D-EEF4-4355-BCBF-C160080F257A} = {E32B62B8-D737-4713-87C5-8220C9746643} {3DB5D0DD-1509-40B8-AD1A-47D5672BF484} = {772AFE42-DF1F-49B1-9F64-7C901E588C00} {62DF9FF9-D2B3-4333-948D-2E405699B47B} = {BEBA1764-178A-4722-A193-4DEF26DCE8D1} {C788AFFF-F440-4259-9102-5B4C1B91FAFA} = {62DF9FF9-D2B3-4333-948D-2E405699B47B} + {58DD682C-DA5C-4B51-BCB8-C65D690AAC67} = {E6C02BDF-EEB0-4ABD-ADEC-9932F60923AE} + {A4D69733-CDC0-46AE-B46A-163CCC6F77F9} = {E6C02BDF-EEB0-4ABD-ADEC-9932F60923AE} + {28DB6CBB-1527-42A1-8EFE-3D95BF185884} = {90913510-3D7F-4BCC-B55E-56343128F049} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {456CBC78-579D-483F-A4C3-AF5C12AB3324} diff --git a/Readme.md b/Readme.md index c5792f464..1fcc11f63 100644 --- a/Readme.md +++ b/Readme.md @@ -30,7 +30,7 @@ This project uses `Lombiq Node.js Extensions` to compile and lint client-side as 2. Build and run the `OrchardCore.Commerce.Web` project. 3. Thanks to [Auto Setup](https://docs.orchardcore.net/en/latest/docs/reference/modules/AutoSetup/), the site will be set up with the `OrchardCore Commerce - Development` recipe. 4. Go to the dashboard, using the credentials `admin` and `Password1!`. -5. Go to _Configuration_ → _Commerce_ → _Stripe API_. Set the keys to the test keys found [here](docs/features/stripe-payment.md). If the keys are not set, payment won't work. +5. If you want to test Stripe, go to _Configuration_ → _Commerce_ → _Stripe API_. Set the keys to the test keys found [here](docs/features/stripe-payment.md). If the keys are not set, the Stripe payment button won't appear during checkout. 6. Go to _Content_ → _Content Items_, and create your first `Product`. ## Documentation @@ -38,7 +38,8 @@ This project uses `Lombiq Node.js Extensions` to compile and lint client-side as - [Inventory](docs/features/inventory.md) - [Products and Prices](docs/features/products-and-prices.md) - [Promotions](docs/features/promotions.md) -- [Stripe Payment](docs/features/stripe-payment.md) +- [Payment providers](docs/features/payment-providers.md) + - [Stripe Payment](docs/features/stripe-payment.md) - [Taxation](docs/features/taxation.md) - [User Features](docs/features/user-features.md) - [Workflows](docs/features/workflows.md) diff --git a/docs/features/payment-providers.md b/docs/features/payment-providers.md new file mode 100644 index 000000000..853d442eb --- /dev/null +++ b/docs/features/payment-providers.md @@ -0,0 +1,20 @@ +# Payment Providers + +Orchard Core Commerce supports multiple payment providers and allows developers to extend it further with their own. + +## Official payment providers + +Each provider is a stand-alone feature you can turn on or off. + +- [Stripe](stripe-payment.md): A production-ready provider for [stripe.com](https://stripe.com/). +- Dummy: A development-only provider that lets you click through the checkout without going off-site. Mainly used for UI testing. + +## Creating your own + +To create a custom payment provider your code must contain the following: +- An implementation of `IPaymentProvider` registered as a service. +- A shape type `Checkout{provider.Name}`, such as _CheckoutStripe.cshtml_ and _CheckoutDummy.cshtml_. + +The `IPaymentProvider` contains implementable methods used by the `CheckoutController`. To learn more about the individual methods in the interface, check out the individual methods' XML documentation. + +The shape has the payment button that will be displayed on the `~/checkout` screen. It's up to you to include the front-end logic that calls out to your payment processor and to provide a callback URL. For the latter you can use the `~/checkout/callback/{providerName}/{orderId?}` action. It handles some basic state checking and redirection, but otherwise lets you resolve the pending order using `IPaymentProvider.UpdateAndRedirectToFinishedOrderAsync()`. diff --git a/docs/features/stripe-payment.md b/docs/features/stripe-payment.md index 77a957ced..d95a3c558 100644 --- a/docs/features/stripe-payment.md +++ b/docs/features/stripe-payment.md @@ -1,6 +1,6 @@ # Stripe Payment -Orchard Core Commerce [aims to support multiple payment providers](https://github.com/OrchardCMS/OrchardCore.Commerce/issues/149), but at this time [Stripe](https://stripe.com/) is the only supported implementation. This document describes how to set it up for testing, but if you just want to try out the checkout flow you can use the [cards](#cards) listed below without any configuration. +Orchard Core Commerce supports multiple [payment providers](payment-providers.md). [Stripe](https://stripe.com/) was the first implemented. This document describes how to set it up for testing, including hooking your site up to the Stripe dashboard. If you just want to try out the checkout flow you can skip the Webhook sections. ## Test data for Stripe Payment @@ -14,13 +14,15 @@ Stripe API uses a secret-publishable key pair. The following API keys are public **Publishable and secret keys are just publicly available test keys. Don't submit any personally identifiable information in requests made with this key.** -You can obtain your own test keys from the Stripe dashboard. You can find them at _Dashboard → Developers → API keys_. +You can obtain your own test keys from the Stripe dashboard. You can find them in the [developer dashboard's Api keys tab](https://dashboard.stripe.com/test/apikeys). + +In your Orchard Core site go to _Admin_ → _Configuration_ → _Commerce_ → _Stripe API_ and provide at least the _Publishable key_ and _Secret key_. Otherwise the feature can't work and the payment button will be hidden during checkout. ### Webhook signing key It is not needed, but recommended to use webhook. Otherwise, if there is a problem with redirecting the user, the payment confirmation will fail. -There is no publicly available webhook signing key. Use your own API keys and Webhook key. Create your own at _Dashboard → Developers → Webhooks_. +There is no publicly available webhook signing key. Use your own API keys and Webhook key. You can create one in the [developer dashboard's Webhooks tab](https://dashboard.stripe.com/test/webhooks). Read about webhook status codes [here](https://stripe.com/docs/webhooks/best-practices#pending-webhook-statuses). Our webhook endpoint returns _200_ if the request was received without an exception. It does not mean that it has been processed. It returns _400_ if there was an exception inside the webhook controller. Everything else comes from Stripe itself. diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ICheckoutEvents.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ICheckoutEvents.cs new file mode 100644 index 000000000..77895213b --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ICheckoutEvents.cs @@ -0,0 +1,16 @@ +using OrchardCore.Commerce.Abstractions.Models; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Abstractions.Abstractions; + +/// +/// Extension points for events related to checkout. +/// +public interface ICheckoutEvents +{ + /// + /// Invoked at the start of a new creation which is used to create the . + /// + Task OrderCreatingAsync(OrderPart orderPart, string shoppingCartId) => Task.CompletedTask; +} diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ICheckoutViewModel.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ICheckoutViewModel.cs new file mode 100644 index 000000000..bf9ca1510 --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ICheckoutViewModel.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.DisplayManagement; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace OrchardCore.Commerce.Abstractions.Abstractions; + +[SuppressMessage( + "StyleCop.CSharp.DocumentationRules", + "SA1600:Elements should be documented", + Justification = "Nothing to say besides what's already on the property names.")] +public interface ICheckoutViewModel : IPaymentViewModel, IShape +{ + string ShoppingCartId { get; } + Amount GrossTotal { get; } + IEnumerable Regions { get; set; } + IDictionary> Provinces { get; } + string UserEmail { get; } + IEnumerable CheckoutShapes { get; } +} diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/ILineItem.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ILineItem.cs similarity index 90% rename from src/Modules/OrchardCore.Commerce/Abstractions/ILineItem.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ILineItem.cs index 91e8a0f79..9ba6f909d 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/ILineItem.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ILineItem.cs @@ -1,6 +1,6 @@ using OrchardCore.Commerce.MoneyDataType; -namespace OrchardCore.Commerce.Abstractions; +namespace OrchardCore.Commerce.Abstractions.Abstractions; /// /// Represents an object that contains some of an order line item's basic info. diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IOrderEvents.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IOrderEvents.cs new file mode 100644 index 000000000..ed8ab55ca --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IOrderEvents.cs @@ -0,0 +1,27 @@ +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; +using OrchardCore.ContentManagement; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Abstractions.Abstractions; + +/// +/// Extension points for events related to orders. +/// +public interface IOrderEvents +{ + /// + /// Invoked when a new free (non-payment) order is created. + /// + Task CreatedFreeAsync(OrderPart orderPart, ShoppingCart cart, ShoppingCartViewModel viewModel) => Task.CompletedTask; + + /// + /// Invoked when the is set to the Ordered state. + /// + Task OrderedAsync(ContentItem order, string shoppingCartId) => Task.CompletedTask; + + /// + /// Invoked during cleanup after the order has been finalized. + /// + Task FinalizeAsync(ContentItem order, string shoppingCartId, string paymentProviderName) => Task.CompletedTask; +} diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IPayment.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPayment.cs similarity index 93% rename from src/Modules/OrchardCore.Commerce/Abstractions/IPayment.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPayment.cs index 8f6dfaaa0..80179343f 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IPayment.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPayment.cs @@ -1,7 +1,7 @@ using OrchardCore.Commerce.MoneyDataType; using System; -namespace OrchardCore.Commerce.Abstractions; +namespace OrchardCore.Commerce.Abstractions.Abstractions; /// /// Describes a payment transaction's details. diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPaymentViewModel.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPaymentViewModel.cs new file mode 100644 index 000000000..0bec96a8d --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPaymentViewModel.cs @@ -0,0 +1,18 @@ +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.MoneyDataType; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace OrchardCore.Commerce.Abstractions.Abstractions; + +[SuppressMessage( + "StyleCop.CSharp.DocumentationRules", + "SA1600:Elements should be documented", + Justification = "Nothing to say besides what's already on the property names.")] +public interface IPaymentViewModel +{ + Amount SingleCurrencyTotal { get; } + Amount NetTotal { get; } + IDictionary PaymentProviderData { get; } + OrderPart OrderPart { get; } +} diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IPredefinedValuesProductAttributeValue.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPredefinedValuesProductAttributeValue.cs similarity index 93% rename from src/Modules/OrchardCore.Commerce/Abstractions/IPredefinedValuesProductAttributeValue.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPredefinedValuesProductAttributeValue.cs index 8dc2d8b6f..db0312a44 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IPredefinedValuesProductAttributeValue.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IPredefinedValuesProductAttributeValue.cs @@ -1,4 +1,4 @@ -namespace OrchardCore.Commerce.Abstractions; +namespace OrchardCore.Commerce.Abstractions.Abstractions; /// /// Represents a predefined with no type specified. diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeDeserializer.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeDeserializer.cs new file mode 100644 index 000000000..cd501407c --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeDeserializer.cs @@ -0,0 +1,49 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace OrchardCore.Commerce.Abstractions.Abstractions; + +/// +/// Deserializes the attribute of the type . +/// +public interface IProductAttributeDeserializer +{ + // Not necessary to document as they are not externally accessible. +#pragma warning disable SA1600 // Elements should be documented. + internal static readonly Dictionary Deserializers = + new(StringComparer.OrdinalIgnoreCase); + + private static readonly object _lock = new(); +#pragma warning restore SA1600 + + /// + /// Gets the attribute name used to identify this deserializer. + /// + string AttributeTypeName { get; } + + /// + /// Deserializes using Newtonsoft.Json. + /// + IProductAttributeValue Deserialize(string attributeName, JObject attribute); + + /// + /// Deserializes using System.Text.Json. + /// + IProductAttributeValue Deserialize(string attributeName, JsonObject attribute); + + /// + /// Registers serializers used to deserialize product attributes. + /// + public static void AddSerializers(params IProductAttributeDeserializer[] deserializers) + { + lock (_lock) + { + foreach (var deserializer in deserializers) + { + Deserializers[deserializer.AttributeTypeName] = deserializer; + } + } + } +} diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IProductAttributeValue.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeValue.cs similarity index 93% rename from src/Modules/OrchardCore.Commerce/Abstractions/IProductAttributeValue.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeValue.cs index c0f545c2b..6c7584f0b 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IProductAttributeValue.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IProductAttributeValue.cs @@ -1,10 +1,10 @@ using Newtonsoft.Json; -using OrchardCore.Commerce.Serialization; +using OrchardCore.Commerce.Abstractions.Serialization; using System; using System.Globalization; using System.Linq; -namespace OrchardCore.Commerce.Abstractions; +namespace OrchardCore.Commerce.Abstractions.Abstractions; /// /// A specific value from a product attribute field. diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IRegionService.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IRegionService.cs similarity index 91% rename from src/Modules/OrchardCore.Commerce/Abstractions/IRegionService.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IRegionService.cs index 723d2fa0a..069510719 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IRegionService.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IRegionService.cs @@ -3,7 +3,7 @@ using System.Globalization; using System.Threading.Tasks; -namespace OrchardCore.Commerce.Abstractions; +namespace OrchardCore.Commerce.Abstractions.Abstractions; /// /// A service for accessing and customizing regions. diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IShoppingCartHelpers.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IShoppingCartHelpers.cs new file mode 100644 index 000000000..3c27a69ee --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IShoppingCartHelpers.cs @@ -0,0 +1,94 @@ +using OrchardCore.Commerce.Abstractions.Exceptions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; +using OrchardCore.Commerce.AddressDataType; +using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Abstractions.Abstractions; + +/// +/// A service to work with shopping cart data. +/// +public interface IShoppingCartHelpers +{ + /// + /// Creates a model from the current shopping cart. This includes everything except the + /// collection in . + /// + Task CreateShoppingCartViewModelAsync( + string shoppingCartId, + Address shipping = null, + Address billing = null); + + /// + /// Returns a identified by . + /// + /// + /// The name used to identify the shopping cart. refers to the default shopping cart. + /// + Task RetrieveAsync(string shoppingCartId); + + /// + /// Retrieves a using , then updates and stores it. + /// + Task UpdateAsync(string shoppingCartId, Func updateTask); + + /// + /// Calculate the total value in the . All prices must be of a single currency. + /// + /// The total value of the items in the cart, or if the cart is empty. + Task CalculateSingleCurrencyTotalAsync(ShoppingCart cart); + + /// + /// Groups the line items in the by currency and returns the value by currency code. + /// + Task> CalculateMultipleCurrencyTotalsAsync(ShoppingCart cart); + + /// + /// Adds a new entry to the shopping cart, optionally saves the cart using IShoppingCartPersistence if + /// is . + /// + /// + /// Thrown if the cart validation fails. Its can be displayed safely. + /// + Task AddToCartAsync( + string shoppingCartId, + ShoppingCartItem item, + bool storeIfOk = false); + + /// + /// Adds the product with the given to the shopping cart without saving, validates the cart + /// and calculates the display information for the added item. + /// + Task EstimateProductAsync( + string shoppingCartId, + string sku, + Address shipping = null, + Address billing = null); + + /// + /// Returns a list from the given items. + /// + Task> CreateOrderLineItemsAsync(ShoppingCart shoppingCart); +} + +public static class ShoppingCartHelpersExtensions +{ + public static Task CreateShoppingCartViewModelAsync( + this IShoppingCartHelpers service, + string shoppingCartId, + IContent order) + { + var orderPart = order as OrderPart ?? order.As(); + + return service.CreateShoppingCartViewModelAsync( + shoppingCartId, + orderPart.ShippingAddress.Address, + orderPart.BillingAddress.Address); + } +} diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ISkuHolder.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ISkuHolder.cs new file mode 100644 index 000000000..d049f1c9b --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ISkuHolder.cs @@ -0,0 +1,12 @@ +namespace OrchardCore.Commerce.Abstractions.Abstractions; + +/// +/// Represents an object with an property. This can identify a product. +/// +public interface ISkuHolder +{ + /// + /// Gets or sets the product's SKU, which can also be used as an alias for the item. + /// + public string Sku { get; set; } +} diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ISkuHolderContent.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ISkuHolderContent.cs new file mode 100644 index 000000000..fa01227c4 --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/ISkuHolderContent.cs @@ -0,0 +1,11 @@ +using OrchardCore.ContentManagement; + +namespace OrchardCore.Commerce.Abstractions.Abstractions; + +/// +/// Represents such as content item or content part that has an +/// property. +/// +public interface ISkuHolderContent : ISkuHolder, IContent +{ +} diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IUserService.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IUserService.cs similarity index 96% rename from src/Modules/OrchardCore.Commerce/Abstractions/IUserService.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IUserService.cs index 5c73a4a76..46b27012f 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IUserService.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Abstractions/IUserService.cs @@ -6,7 +6,7 @@ using System.Security.Claims; using System.Threading.Tasks; -namespace OrchardCore.Commerce.Abstractions; +namespace OrchardCore.Commerce.Abstractions.Abstractions; /// /// A service for managing user properties. diff --git a/src/Modules/OrchardCore.Commerce/Constants/ContentTypes.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/ContentTypes.cs similarity index 85% rename from src/Modules/OrchardCore.Commerce/Constants/ContentTypes.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Constants/ContentTypes.cs index 15b67cc58..5347ea43b 100644 --- a/src/Modules/OrchardCore.Commerce/Constants/ContentTypes.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/ContentTypes.cs @@ -1,4 +1,4 @@ -namespace OrchardCore.Commerce.Constants; +namespace OrchardCore.Commerce.Abstractions.Constants; public static class ContentTypes { diff --git a/src/Modules/OrchardCore.Commerce/Constants/OrderStatuses.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs similarity index 84% rename from src/Modules/OrchardCore.Commerce/Constants/OrderStatuses.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs index 516a8dff4..e5bc057a5 100644 --- a/src/Modules/OrchardCore.Commerce/Constants/OrderStatuses.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs @@ -1,4 +1,4 @@ -namespace OrchardCore.Commerce.Constants; +namespace OrchardCore.Commerce.Abstractions.Constants; public static class OrderStatuses { diff --git a/src/Modules/OrchardCore.Commerce/Exceptions/FrontendException.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs similarity index 93% rename from src/Modules/OrchardCore.Commerce/Exceptions/FrontendException.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs index 092a40ef5..193a06902 100644 --- a/src/Modules/OrchardCore.Commerce/Exceptions/FrontendException.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs @@ -2,7 +2,7 @@ using System; using System.Runtime.Serialization; -namespace OrchardCore.Commerce.Exceptions; +namespace OrchardCore.Commerce.Abstractions.Exceptions; [Serializable] public class FrontendException : Exception diff --git a/src/Modules/OrchardCore.Commerce/Fields/AddressField.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Fields/AddressField.cs similarity index 81% rename from src/Modules/OrchardCore.Commerce/Fields/AddressField.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Fields/AddressField.cs index 37c7f6cc4..5e0f80cec 100644 --- a/src/Modules/OrchardCore.Commerce/Fields/AddressField.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Fields/AddressField.cs @@ -1,7 +1,7 @@ using OrchardCore.Commerce.AddressDataType; using OrchardCore.ContentManagement; -namespace OrchardCore.Commerce.Fields; +namespace OrchardCore.Commerce.Abstractions.Fields; public class AddressField : ContentField { diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/License.md b/src/Libraries/OrchardCore.Commerce.Abstractions/License.md new file mode 100644 index 000000000..798b9cdec --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 OrchardCMS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Manifest.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Manifest.cs new file mode 100644 index 000000000..3f550a410 --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Manifest.cs @@ -0,0 +1,3 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0")] diff --git a/src/Modules/OrchardCore.Commerce/Models/OrderAdditionalCost.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderAdditionalCost.cs similarity index 92% rename from src/Modules/OrchardCore.Commerce/Models/OrderAdditionalCost.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderAdditionalCost.cs index 55e9a411f..c85a14853 100644 --- a/src/Modules/OrchardCore.Commerce/Models/OrderAdditionalCost.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderAdditionalCost.cs @@ -1,6 +1,6 @@ using OrchardCore.Commerce.MoneyDataType; -namespace OrchardCore.Commerce.Models; +namespace OrchardCore.Commerce.Abstractions.Models; public class OrderAdditionalCost { diff --git a/src/Modules/OrchardCore.Commerce/Models/OrderLineItem.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderLineItem.cs similarity index 94% rename from src/Modules/OrchardCore.Commerce/Models/OrderLineItem.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderLineItem.cs index 662f70068..fdacbe3d1 100644 --- a/src/Modules/OrchardCore.Commerce/Models/OrderLineItem.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderLineItem.cs @@ -1,9 +1,9 @@ -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.MoneyDataType; using System; using System.Collections.Generic; -namespace OrchardCore.Commerce.Models; +namespace OrchardCore.Commerce.Abstractions.Models; public class OrderLineItem { diff --git a/src/Modules/OrchardCore.Commerce/Models/OrderPart.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs similarity index 92% rename from src/Modules/OrchardCore.Commerce/Models/OrderPart.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs index d15bef804..2693c18a7 100644 --- a/src/Modules/OrchardCore.Commerce/Models/OrderPart.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs @@ -1,12 +1,12 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Fields; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.ContentFields.Fields; using OrchardCore.ContentManagement; using System.Collections.Generic; -namespace OrchardCore.Commerce.Models; +namespace OrchardCore.Commerce.Abstractions.Models; public class OrderPart : ContentPart { diff --git a/src/Modules/OrchardCore.Commerce/Models/PrioritizedPrice.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/PrioritizedPrice.cs similarity index 88% rename from src/Modules/OrchardCore.Commerce/Models/PrioritizedPrice.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Models/PrioritizedPrice.cs index 457561d81..7128fe5a3 100644 --- a/src/Modules/OrchardCore.Commerce/Models/PrioritizedPrice.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/PrioritizedPrice.cs @@ -1,10 +1,10 @@ using Newtonsoft.Json; +using OrchardCore.Commerce.Abstractions.Serialization; using OrchardCore.Commerce.MoneyDataType; -using OrchardCore.Commerce.Serialization; using System.Diagnostics; using System.Globalization; -namespace OrchardCore.Commerce.Models; +namespace OrchardCore.Commerce.Abstractions.Models; /// /// A price and its priority. diff --git a/src/Modules/OrchardCore.Commerce/Models/ShoppingCart.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCart.cs similarity index 98% rename from src/Modules/OrchardCore.Commerce/Models/ShoppingCart.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCart.cs index 899c2842c..f69a3583e 100644 --- a/src/Modules/OrchardCore.Commerce/Models/ShoppingCart.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCart.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text.Json.Serialization; -namespace OrchardCore.Commerce.Models; +namespace OrchardCore.Commerce.Abstractions.Models; /// /// A shopping cart. diff --git a/src/Modules/OrchardCore.Commerce/Models/ShoppingCartItem.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCartItem.cs similarity index 67% rename from src/Modules/OrchardCore.Commerce/Models/ShoppingCartItem.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCartItem.cs index e1473c912..cf4da44ef 100644 --- a/src/Modules/OrchardCore.Commerce/Models/ShoppingCartItem.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/ShoppingCartItem.cs @@ -1,21 +1,19 @@ -using Microsoft.AspNetCore.Mvc.Localization; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Extensions; -using OrchardCore.Commerce.Serialization; +using Newtonsoft.Json; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; +using OrchardCore.Commerce.Abstractions.Serialization; using OrchardCore.Mvc.Utilities; using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -namespace OrchardCore.Commerce.Models; +namespace OrchardCore.Commerce.Abstractions.Models; /// /// A shopping cart item. /// -[Newtonsoft.Json.JsonConverter(typeof(LegacyShoppingCartItemConverter))] -[JsonConverter(typeof(ShoppingCartItemConverter))] +[JsonConverter(typeof(LegacyShoppingCartItemConverter))] +[System.Text.Json.Serialization.JsonConverter(typeof(ShoppingCartItemConverter))] public sealed class ShoppingCartItem : IEquatable { /// @@ -110,47 +108,7 @@ public override string ToString() => public bool IsSameProductAs(ShoppingCartItem other) => ProductSku == other.ProductSku && Attributes.SetEquals(other.Attributes); - public override int GetHashCode() => (ProductSku, Quantity, Attributes).GetHashCode(); + public bool HasRawAttributes() => Attributes.Any(attribute => attribute is RawProductAttributeValue); - public async Task CreateOrderLineFromShoppingCartItemAsync( - IPriceSelectionStrategy priceSelectionStrategy, - IPriceService priceService, - IProductService productService, - string contentItemVersion, - IDictionary> selectedAttributes) - { - var quantity = Quantity; - - var item = await priceService.AddPriceAsync(this); - var price = priceSelectionStrategy.SelectPrice(item.Prices); - var fullSku = productService.GetOrderFullSku(item, await productService.GetProductAsync(ProductSku)); - - return new OrderLineItem( - quantity, - ProductSku, - fullSku, - price, - quantity * price, - contentItemVersion, - Attributes, - selectedAttributes); - } - - public static async Task GetErrorAsync( - string sku, - ShoppingCartItem item, - IHtmlLocalizer localizer, - IPriceService priceService) - { - if (item is null) - { - return localizer["Product with SKU {0} not found.", sku]; - } - - item = (await priceService.AddPricesAsync(new[] { item })).Single(); - - return item.Prices.Any() - ? null - : localizer["Can't add product {0} because it doesn't have a price, or its currency doesn't match the current display currency.", sku]; - } + public override int GetHashCode() => (ProductSku, Quantity, Attributes).GetHashCode(); } diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/OrchardCore.Commerce.Abstractions.csproj b/src/Libraries/OrchardCore.Commerce.Abstractions/OrchardCore.Commerce.Abstractions.csproj new file mode 100644 index 000000000..827e6bef3 --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/OrchardCore.Commerce.Abstractions.csproj @@ -0,0 +1,36 @@ + + + + net6.0 + false + + + + Orchard Core Commerce - Abstractions + Bertrand Le Roy + Copyright © 2018 .NET Foundation + Contains models and other abstractions that can be used by modules to extend OrchardCore Commerce. + OrchardCore;OrchardCore.Commerce;abstraction + https://github.com/OrchardCMS/OrchardCore.Commerce + https://github.com/OrchardCMS/OrchardCore.Commerce/blob/main/src/Libraries/OrchardCore.Commerce.Abstractions/Readme.md + License.md + OrchardCoreIcon.png + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/OrchardCoreIcon.png b/src/Libraries/OrchardCore.Commerce.Abstractions/OrchardCoreIcon.png new file mode 100644 index 000000000..c72b029ac Binary files /dev/null and b/src/Libraries/OrchardCore.Commerce.Abstractions/OrchardCoreIcon.png differ diff --git a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/BaseProductAttributeValue.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/BaseProductAttributeValue.cs similarity index 76% rename from src/Modules/OrchardCore.Commerce/ProductAttributeValues/BaseProductAttributeValue.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/BaseProductAttributeValue.cs index 23f6caa49..f9a44a8ea 100644 --- a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/BaseProductAttributeValue.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/BaseProductAttributeValue.cs @@ -1,8 +1,9 @@ -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -namespace OrchardCore.Commerce.ProductAttributeValues; +namespace OrchardCore.Commerce.Abstractions.ProductAttributeValues; public class BaseProductAttributeValue : IProductAttributeValue { @@ -31,6 +32,12 @@ public BaseProductAttributeValue(string attributeName, T value) public virtual string Display(CultureInfo culture = null) => FieldName + ": " + Convert.ToString(Value, culture ?? CultureInfo.InvariantCulture); + [SuppressMessage( + "Blocker Code Smell", + "S3060:\"is\" should not be used with \"this\"", + Justification = "Testing against an internal class.")] + public bool IsRaw() => this is RawProductAttributeValue; + public virtual bool Equals(IProductAttributeValue other) => other != null && AttributeName == other.AttributeName && diff --git a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/RawProductAttributeValue.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/RawProductAttributeValue.cs similarity index 72% rename from src/Modules/OrchardCore.Commerce/ProductAttributeValues/RawProductAttributeValue.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/RawProductAttributeValue.cs index 92712ca92..e2752c05b 100644 --- a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/RawProductAttributeValue.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/ProductAttributeValues/RawProductAttributeValue.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; -using OrchardCore.Commerce.Serialization; +using OrchardCore.Commerce.Abstractions.Serialization; -namespace OrchardCore.Commerce.ProductAttributeValues; +namespace OrchardCore.Commerce.Abstractions.ProductAttributeValues; /// /// Used only to deserialize attributes, before they're post-processed into concrete attribute values. @@ -15,5 +15,5 @@ public RawProductAttributeValue(object value) { } - public void SetAttributeName(string name) => AttributeName = name; + internal void SetAttributeName(string name) => AttributeName = name; } diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Readme.md b/src/Libraries/OrchardCore.Commerce.Abstractions/Readme.md new file mode 100644 index 000000000..8d5dfa3a2 --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Readme.md @@ -0,0 +1,7 @@ +# Orchard Core Commerce - Abstractions + +## About + +Contains models and other abstractions that can be used by modules to extend OrchardCore Commerce. In case of interfaces, these are implemented in `OrchardCore.Commerce`. + +For general details about and on using Orchard Core Commerce see the [root Readme](../../../Readme.md). diff --git a/src/Modules/OrchardCore.Commerce/Serialization/LegacyPrioritizedPriceConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyPrioritizedPriceConverter.cs similarity index 94% rename from src/Modules/OrchardCore.Commerce/Serialization/LegacyPrioritizedPriceConverter.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyPrioritizedPriceConverter.cs index dcf4fd048..adeb88085 100644 --- a/src/Modules/OrchardCore.Commerce/Serialization/LegacyPrioritizedPriceConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyPrioritizedPriceConverter.cs @@ -1,10 +1,10 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using System; -namespace OrchardCore.Commerce.Serialization; +namespace OrchardCore.Commerce.Abstractions.Serialization; internal sealed class LegacyPrioritizedPriceConverter : JsonConverter { diff --git a/src/Modules/OrchardCore.Commerce/Serialization/LegacyRawProductAttributeValueConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyRawProductAttributeValueConverter.cs similarity index 83% rename from src/Modules/OrchardCore.Commerce/Serialization/LegacyRawProductAttributeValueConverter.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyRawProductAttributeValueConverter.cs index 5e21055da..01eea375d 100644 --- a/src/Modules/OrchardCore.Commerce/Serialization/LegacyRawProductAttributeValueConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyRawProductAttributeValueConverter.cs @@ -1,9 +1,9 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.ProductAttributeValues; +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; using System; -namespace OrchardCore.Commerce.Serialization; +namespace OrchardCore.Commerce.Abstractions.Serialization; internal sealed class LegacyRawProductAttributeValueConverter : JsonConverter { diff --git a/src/Modules/OrchardCore.Commerce/Serialization/LegacyShoppingCartItemConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyShoppingCartItemConverter.cs similarity index 92% rename from src/Modules/OrchardCore.Commerce/Serialization/LegacyShoppingCartItemConverter.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyShoppingCartItemConverter.cs index ead933251..33e6d0278 100644 --- a/src/Modules/OrchardCore.Commerce/Serialization/LegacyShoppingCartItemConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/LegacyShoppingCartItemConverter.cs @@ -1,13 +1,13 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.ProductAttributeValues; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; using System; using System.Collections.Generic; using System.Linq; -namespace OrchardCore.Commerce.Serialization; +namespace OrchardCore.Commerce.Abstractions.Serialization; internal sealed class LegacyShoppingCartItemConverter : JsonConverter { diff --git a/src/Modules/OrchardCore.Commerce/Serialization/PrioritizedPriceConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/PrioritizedPriceConverter.cs similarity index 94% rename from src/Modules/OrchardCore.Commerce/Serialization/PrioritizedPriceConverter.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/PrioritizedPriceConverter.cs index a13619956..90a1b3e73 100644 --- a/src/Modules/OrchardCore.Commerce/Serialization/PrioritizedPriceConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/PrioritizedPriceConverter.cs @@ -1,10 +1,10 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using System; using System.Text.Json; using System.Text.Json.Serialization; -namespace OrchardCore.Commerce.Serialization; +namespace OrchardCore.Commerce.Abstractions.Serialization; internal sealed class PrioritizedPriceConverter : JsonConverter { diff --git a/src/Modules/OrchardCore.Commerce/Serialization/ProductAttributeValueConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ProductAttributeValueConverter.cs similarity index 70% rename from src/Modules/OrchardCore.Commerce/Serialization/ProductAttributeValueConverter.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ProductAttributeValueConverter.cs index e772b90de..97abe5649 100644 --- a/src/Modules/OrchardCore.Commerce/Serialization/ProductAttributeValueConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ProductAttributeValueConverter.cs @@ -1,11 +1,10 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.ProductAttributeValues; +using OrchardCore.Commerce.Abstractions.Abstractions; using System; using System.Collections.Generic; -namespace OrchardCore.Commerce.Serialization; +namespace OrchardCore.Commerce.Abstractions.Serialization; internal sealed class ProductAttributeValueConverter : JsonConverter { @@ -25,13 +24,10 @@ public override IProductAttributeValue ReadJson( var attributeName = attribute.Get(AttributeName); var typeName = attribute.Get(Type); - return typeName switch - { - nameof(TextProductAttributeValue) => new TextProductAttributeValue(attributeName, attribute.Get>(Value)), - nameof(BooleanProductAttributeValue) => new BooleanProductAttributeValue(attributeName, attribute.Get(Value)), - nameof(NumericProductAttributeValue) => new NumericProductAttributeValue(attributeName, attribute.Get(Value)), - _ => throw new InvalidOperationException($"Unknown or unsupported type \"{typeName}\"."), - }; + var deserializer = IProductAttributeDeserializer.Deserializers.GetMaybe(typeName) ?? + throw new InvalidOperationException($"Unknown or unsupported type \"{typeName}\"."); + + return deserializer.Deserialize(attributeName, attribute); } public override void WriteJson(JsonWriter writer, IProductAttributeValue productAttributeValue, JsonSerializer serializer) diff --git a/src/Modules/OrchardCore.Commerce/Serialization/RawProductAttributeValueConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/RawProductAttributeValueConverter.cs similarity index 82% rename from src/Modules/OrchardCore.Commerce/Serialization/RawProductAttributeValueConverter.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/RawProductAttributeValueConverter.cs index e18dd9d3a..d2c2c52f2 100644 --- a/src/Modules/OrchardCore.Commerce/Serialization/RawProductAttributeValueConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/RawProductAttributeValueConverter.cs @@ -1,9 +1,9 @@ -using OrchardCore.Commerce.ProductAttributeValues; +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; using System; using System.Text.Json; using System.Text.Json.Serialization; -namespace OrchardCore.Commerce.Serialization; +namespace OrchardCore.Commerce.Abstractions.Serialization; internal sealed class RawProductAttributeValueConverter : JsonConverter { diff --git a/src/Modules/OrchardCore.Commerce/Serialization/ShoppingCartItemConverter.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ShoppingCartItemConverter.cs similarity index 93% rename from src/Modules/OrchardCore.Commerce/Serialization/ShoppingCartItemConverter.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ShoppingCartItemConverter.cs index aae657bdc..10311f458 100644 --- a/src/Modules/OrchardCore.Commerce/Serialization/ShoppingCartItemConverter.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Serialization/ShoppingCartItemConverter.cs @@ -1,13 +1,13 @@ -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.ProductAttributeValues; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -namespace OrchardCore.Commerce.Serialization; +namespace OrchardCore.Commerce.Abstractions.Serialization; internal sealed class ShoppingCartItemConverter : JsonConverter { diff --git a/src/Modules/OrchardCore.Commerce/TagHelpers/MvcTitleTagHelper.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/TagHelpers/MvcTitleTagHelper.cs similarity index 77% rename from src/Modules/OrchardCore.Commerce/TagHelpers/MvcTitleTagHelper.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/TagHelpers/MvcTitleTagHelper.cs index eb268b69a..ea0d9fd30 100644 --- a/src/Modules/OrchardCore.Commerce/TagHelpers/MvcTitleTagHelper.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/TagHelpers/MvcTitleTagHelper.cs @@ -1,12 +1,14 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Razor.TagHelpers; +using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Display; using OrchardCore.DisplayManagement; using System.IO; using System.Text; using System.Threading.Tasks; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; -namespace OrchardCore.Commerce.TagHelpers; +namespace OrchardCore.Commerce.Abstractions.TagHelpers; [HtmlTargetElement("mvc-title", Attributes = "text")] public class MvcTitleTagHelper : TagHelper @@ -27,7 +29,8 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu var stringBuilder = new StringBuilder(); await using (var writer = new StringWriter(stringBuilder)) Text.WriteTo(writer, NullHtmlEncoder.Default); - var shape = await _contentItemDisplayManager.BuildMvcTitleAsync(stringBuilder.ToString()); + var header = new ContentItem { ContentType = MvcTitle, DisplayText = stringBuilder.ToString() }; + var shape = await _contentItemDisplayManager.BuildDisplayAsync(header, updater: null); var content = await _displayHelper.ShapeExecuteAsync(shape); output.TagName = null; diff --git a/src/Modules/OrchardCore.Commerce/ViewModels/ShoppingCartLineViewModel.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartLineViewModel.cs similarity index 87% rename from src/Modules/OrchardCore.Commerce/ViewModels/ShoppingCartLineViewModel.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartLineViewModel.cs index 307964e1e..4861091a5 100644 --- a/src/Modules/OrchardCore.Commerce/ViewModels/ShoppingCartLineViewModel.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartLineViewModel.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.MoneyDataType; using System.Collections.Generic; using System.Linq; -namespace OrchardCore.Commerce.ViewModels; +namespace OrchardCore.Commerce.Abstractions.ViewModels; public class ShoppingCartLineViewModel : ILineItem { @@ -21,7 +20,7 @@ public class ShoppingCartLineViewModel : ILineItem public IDictionary AdditionalData { get; } = new Dictionary(); [BindNever] - public ProductPart Product { get; set; } + public ISkuHolderContent Product { get; set; } public ShoppingCartLineViewModel(IDictionary attributes = null) => Attributes = attributes ?? new Dictionary(); diff --git a/src/Modules/OrchardCore.Commerce/ViewModels/ShoppingCartViewModel.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartViewModel.cs similarity index 65% rename from src/Modules/OrchardCore.Commerce/ViewModels/ShoppingCartViewModel.cs rename to src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartViewModel.cs index 005b9c7c5..a911ae8b5 100644 --- a/src/Modules/OrchardCore.Commerce/ViewModels/ShoppingCartViewModel.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/ViewModels/ShoppingCartViewModel.cs @@ -1,9 +1,11 @@ using Microsoft.AspNetCore.Mvc.Localization; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.DisplayManagement; +using System; using System.Collections.Generic; +using System.Linq; -namespace OrchardCore.Commerce.ViewModels; +namespace OrchardCore.Commerce.Abstractions.ViewModels; public class ShoppingCartViewModel { @@ -12,4 +14,9 @@ public class ShoppingCartViewModel public IList> TableShapes { get; } = new List>(); public IList Lines { get; } = new List(); public IList Totals { get; } = new List(); + + public IList GetTotalsOrThrowIfEmpty() => + Totals.Any() + ? Totals + : throw new InvalidOperationException("Cannot create a payment without shopping cart total(s)!"); } diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/.eslintrc.js b/src/Modules/OrchardCore.Commerce.ContentFields/.eslintrc.js new file mode 100644 index 000000000..d62e24ed6 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + // Setting root=true prevents ESLint from taking into account .eslintrc files higher up in the directory tree. + root: true, + + // The following path may have to be adjusted to your directory structure. + extends: './node_modules/nodejs-extensions/config/.eslintrc.lombiq-base.js', + + // Add custom rules and overrides here. + rules: { + }, +}; diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/.stylelintrc.js b/src/Modules/OrchardCore.Commerce.ContentFields/.stylelintrc.js new file mode 100644 index 000000000..2ae551738 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/.stylelintrc.js @@ -0,0 +1,8 @@ +module.exports = { + // The following path may have to be adjusted to your directory structure. + extends: './node_modules/nodejs-extensions/config/.stylelintrc.lombiq-base.js', + + // Add custom rules and overrides here. + rules: { + }, +}; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IFieldsOnlyDisplayManager.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Abstractions/IFieldsOnlyDisplayManager.cs similarity index 100% rename from src/Modules/OrchardCore.Commerce/Abstractions/IFieldsOnlyDisplayManager.cs rename to src/Modules/OrchardCore.Commerce.ContentFields/Abstractions/IFieldsOnlyDisplayManager.cs diff --git a/src/Modules/OrchardCore.Commerce/Assets/Scripts/commerce-regions.js b/src/Modules/OrchardCore.Commerce.ContentFields/Assets/Scripts/commerce-regions.js similarity index 100% rename from src/Modules/OrchardCore.Commerce/Assets/Scripts/commerce-regions.js rename to src/Modules/OrchardCore.Commerce.ContentFields/Assets/Scripts/commerce-regions.js diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Constants/ContentTypes.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Constants/ContentTypes.cs new file mode 100644 index 000000000..5da6b3eb5 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Constants/ContentTypes.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Commerce.ContentFields.Constants; + +public static class ContentTypes +{ + public const string UserAddresses = nameof(UserAddresses); +} diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Constants/FeatureIds.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Constants/FeatureIds.cs new file mode 100644 index 000000000..3ff2453de --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Constants/FeatureIds.cs @@ -0,0 +1,8 @@ +namespace OrchardCore.Commerce.ContentFields.Constants; + +public static class FeatureIds +{ + public const string Area = "OrchardCore.Commerce.ContentFields"; + + public const string ContentFields = Area; +} diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Constants/ResourceNames.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Constants/ResourceNames.cs new file mode 100644 index 000000000..12ccc2b54 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Constants/ResourceNames.cs @@ -0,0 +1,8 @@ +namespace OrchardCore.Commerce.ContentFields.Constants; + +public static class ResourceNames +{ + public const string JQuery = "jQuery"; + + public const string CommerceRegions = nameof(CommerceRegions); +} diff --git a/src/Modules/OrchardCore.Commerce/Drivers/AddressFieldDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/AddressFieldDisplayDriver.cs similarity index 76% rename from src/Modules/OrchardCore.Commerce/Drivers/AddressFieldDisplayDriver.cs rename to src/Modules/OrchardCore.Commerce.ContentFields/Drivers/AddressFieldDisplayDriver.cs index 2e5f73370..e13b90174 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/AddressFieldDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Drivers/AddressFieldDisplayDriver.cs @@ -1,44 +1,41 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; -using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.Commerce.AddressDataType; using OrchardCore.Commerce.AddressDataType.Abstractions; +using OrchardCore.Commerce.ContentFields.Events; using OrchardCore.Commerce.Extensions; -using OrchardCore.Commerce.Fields; -using OrchardCore.Commerce.Models; using OrchardCore.Commerce.ViewModels; using OrchardCore.ContentManagement.Display.ContentDisplay; using OrchardCore.ContentManagement.Display.Models; using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; -using System; using System.Collections.Generic; using System.Threading.Tasks; -using static OrchardCore.Commerce.Constants.ContentTypes; namespace OrchardCore.Commerce.Drivers; public class AddressFieldDisplayDriver : ContentFieldDisplayDriver { + private readonly IEnumerable _addressFieldEvents; private readonly IAddressFormatterProvider _addressFormatterProvider; private readonly IHttpContextAccessor _hca; - private readonly IUserService _userService; private readonly IRegionService _regionService; private readonly IStringLocalizer T; public AddressFieldDisplayDriver( + IEnumerable addressFieldEvents, IAddressFormatterProvider addressFormatterProvider, IHttpContextAccessor hca, - IUserService userService, IRegionService regionService, IStringLocalizer stringLocalizer) { + _addressFieldEvents = addressFieldEvents; _addressFormatterProvider = addressFormatterProvider; _hca = hca; - _userService = userService; _regionService = regionService; T = stringLocalizer; } @@ -97,29 +94,7 @@ bool IsRequiredFieldEmpty(string value, string key) field.Address = viewModel.Address; - if (viewModel.ToBeSaved && - !string.IsNullOrEmpty(viewModel.UserAddressToSave) && - await _userService.GetCurrentFullUserAsync(_hca) is { } user) - { - await _userService.AlterUserSettingAsync(user, UserAddresses, contentItem => - { - var part = contentItem.GetJObject(nameof(UserAddressesPart)); - - if (part[viewModel.UserAddressToSave] is not JObject) - { - part[viewModel.UserAddressToSave] = JObject.FromObject(new AddressField()); - } - - if (part.GetJObject(viewModel.UserAddressToSave) is not { } userAddressToSave) - { - throw new InvalidOperationException( - $"The property {viewModel.UserAddressToSave} is missing from {nameof(UserAddressesPart)}."); - } - - userAddressToSave[nameof(AddressField.Address)] = JToken.FromObject(viewModel.Address); - return contentItem; - }); - } + await _addressFieldEvents.AwaitEachAsync(handler => handler.UpdatingAsync(viewModel, field, updater, context)); return await EditAsync(field, context); } diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Events/IAddressFieldEvents.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Events/IAddressFieldEvents.cs new file mode 100644 index 000000000..a864c1b65 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Events/IAddressFieldEvents.cs @@ -0,0 +1,23 @@ +using OrchardCore.Commerce.Abstractions.Fields; +using OrchardCore.Commerce.Drivers; +using OrchardCore.Commerce.ViewModels; +using OrchardCore.ContentManagement.Display.Models; +using OrchardCore.DisplayManagement.ModelBinding; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.ContentFields.Events; + +/// +/// Events related to address fields. +/// +public interface IAddressFieldEvents +{ + /// + /// Invoked during the update by . + /// + public Task UpdatingAsync( + AddressFieldViewModel viewModel, + AddressField field, + IUpdateModel updater, + UpdateFieldEditorContext context); +} diff --git a/src/Modules/OrchardCore.Commerce/Extensions/RegionExtensions.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Extensions/RegionExtensions.cs similarity index 100% rename from src/Modules/OrchardCore.Commerce/Extensions/RegionExtensions.cs rename to src/Modules/OrchardCore.Commerce.ContentFields/Extensions/RegionExtensions.cs diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/OrchardCore.Commerce.ContentFields.csproj b/src/Modules/OrchardCore.Commerce.ContentFields/OrchardCore.Commerce.ContentFields.csproj index 2d418348d..e2bfde3e3 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/OrchardCore.Commerce.ContentFields.csproj +++ b/src/Modules/OrchardCore.Commerce.ContentFields/OrchardCore.Commerce.ContentFields.csproj @@ -24,6 +24,7 @@ + @@ -33,7 +34,7 @@ - + @@ -44,7 +45,18 @@ - + + + + + + + + + + + + diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Readme.md b/src/Modules/OrchardCore.Commerce.ContentFields/Readme.md index 3b598dcbf..bb1ea0327 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Readme.md +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Readme.md @@ -6,6 +6,7 @@ Commerce-specific content fields for Orchard Core. ## Fields +- `AddressField`: A field that contains parts of an address such as country, region, street address, etc. - `AmountField`: A field that contains a numeric value and a currency. For general details about and on using Orchard Core Commerce see the [root Readme](../../../Readme.md). diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/ResourceManagementOptionsConfiguration.cs b/src/Modules/OrchardCore.Commerce.ContentFields/ResourceManagementOptionsConfiguration.cs new file mode 100644 index 000000000..e62dd3ae3 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/ResourceManagementOptionsConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Options; +using OrchardCore.ResourceManagement; +using static OrchardCore.Commerce.ContentFields.Constants.ResourceNames; + +namespace OrchardCore.Commerce.ContentFields; + +public class ResourceManagementOptionsConfiguration : IConfigureOptions +{ + private static readonly ResourceManifest _manifest = new(); + + static ResourceManagementOptionsConfiguration() => + _manifest + .DefineScript(CommerceRegions) + .SetDependencies(JQuery) + .SetUrl( + "~/OrchardCore.Commerce.ContentFields/js/commerce-regions.min.js", + "~/OrchardCore.Commerce.ContentFields/js/commerce-regions.js") + .SetVersion("1.0.0"); + + public void Configure(ResourceManagementOptions options) => options.ResourceManifests.Add(_manifest); +} diff --git a/src/Modules/OrchardCore.Commerce/Services/FieldsOnlyDisplayManager.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Services/FieldsOnlyDisplayManager.cs similarity index 100% rename from src/Modules/OrchardCore.Commerce/Services/FieldsOnlyDisplayManager.cs rename to src/Modules/OrchardCore.Commerce.ContentFields/Services/FieldsOnlyDisplayManager.cs diff --git a/src/Modules/OrchardCore.Commerce/Settings/AddressFieldSettingsDriver.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressFieldSettingsDriver.cs similarity index 95% rename from src/Modules/OrchardCore.Commerce/Settings/AddressFieldSettingsDriver.cs rename to src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressFieldSettingsDriver.cs index 513b7b39a..367df0a21 100644 --- a/src/Modules/OrchardCore.Commerce/Settings/AddressFieldSettingsDriver.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressFieldSettingsDriver.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Fields; +using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.ContentTypes.Editors; using OrchardCore.DisplayManagement.Views; diff --git a/src/Modules/OrchardCore.Commerce/Settings/AddressPartFieldSettings.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressPartFieldSettings.cs similarity index 100% rename from src/Modules/OrchardCore.Commerce/Settings/AddressPartFieldSettings.cs rename to src/Modules/OrchardCore.Commerce.ContentFields/Settings/AddressPartFieldSettings.cs diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Startup.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Startup.cs index 349c66c53..d29ecf3d1 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Startup.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Startup.cs @@ -1,12 +1,17 @@ using Fluid; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.Commerce.ContentFields.Drivers; using OrchardCore.Commerce.ContentFields.Models; +using OrchardCore.Commerce.Drivers; +using OrchardCore.Commerce.Services; using OrchardCore.Commerce.Settings; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Display.ContentDisplay; using OrchardCore.ContentTypes.Editors; using OrchardCore.Modules; +using OrchardCore.ResourceManagement; namespace OrchardCore.Commerce.ContentFields; @@ -14,6 +19,9 @@ public class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { + services.AddTransient, ResourceManagementOptionsConfiguration>(); + services.AddScoped(); + services.Configure(option => option.MemberAccessStrategy.Register()); @@ -21,5 +29,10 @@ public override void ConfigureServices(IServiceCollection services) services.AddContentField() .UseDisplayDriver(); services.AddScoped(); + + // Address Field + services.AddContentField() + .UseDisplayDriver(); + services.AddScoped(); } } diff --git a/src/Modules/OrchardCore.Commerce/ViewModels/AddressFieldEditorViewModel.cs b/src/Modules/OrchardCore.Commerce.ContentFields/ViewModels/AddressFieldEditorViewModel.cs similarity index 95% rename from src/Modules/OrchardCore.Commerce/ViewModels/AddressFieldEditorViewModel.cs rename to src/Modules/OrchardCore.Commerce.ContentFields/ViewModels/AddressFieldEditorViewModel.cs index 16309e0dd..4d02b9734 100644 --- a/src/Modules/OrchardCore.Commerce/ViewModels/AddressFieldEditorViewModel.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/ViewModels/AddressFieldEditorViewModel.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; -using OrchardCore.Commerce.Fields; +using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.DisplayManagement.Views; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/ViewModels/AddressFieldViewModel.cs b/src/Modules/OrchardCore.Commerce.ContentFields/ViewModels/AddressFieldViewModel.cs similarity index 95% rename from src/Modules/OrchardCore.Commerce/ViewModels/AddressFieldViewModel.cs rename to src/Modules/OrchardCore.Commerce.ContentFields/ViewModels/AddressFieldViewModel.cs index 77bdf36dc..97f44f423 100644 --- a/src/Modules/OrchardCore.Commerce/ViewModels/AddressFieldViewModel.cs +++ b/src/Modules/OrchardCore.Commerce.ContentFields/ViewModels/AddressFieldViewModel.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; +using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.Commerce.AddressDataType; -using OrchardCore.Commerce.Fields; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Metadata.Models; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/Views/AddressField.cshtml b/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressField.cshtml similarity index 100% rename from src/Modules/OrchardCore.Commerce/Views/AddressField.cshtml rename to src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressField.cshtml diff --git a/src/Modules/OrchardCore.Commerce/Views/AddressFieldEditor.cshtml b/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressFieldEditor.cshtml similarity index 99% rename from src/Modules/OrchardCore.Commerce/Views/AddressFieldEditor.cshtml rename to src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressFieldEditor.cshtml index c2a290fc9..59906c811 100644 --- a/src/Modules/OrchardCore.Commerce/Views/AddressFieldEditor.cshtml +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressFieldEditor.cshtml @@ -1,6 +1,6 @@ @using Newtonsoft.Json +@using OrchardCore.Commerce.Abstractions.Fields @using OrchardCore.Commerce.AddressDataType -@using OrchardCore.Commerce.Fields @model AddressFieldEditorViewModel @{ diff --git a/src/Modules/OrchardCore.Commerce/Views/AddressFieldSettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressFieldSettings.Edit.cshtml similarity index 100% rename from src/Modules/OrchardCore.Commerce/Views/AddressFieldSettings.Edit.cshtml rename to src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressFieldSettings.Edit.cshtml diff --git a/src/Modules/OrchardCore.Commerce/Views/AddressField_Edit.cshtml b/src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressField_Edit.cshtml similarity index 100% rename from src/Modules/OrchardCore.Commerce/Views/AddressField_Edit.cshtml rename to src/Modules/OrchardCore.Commerce.ContentFields/Views/AddressField_Edit.cshtml diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Views/PriceField.Edit.cshtml b/src/Modules/OrchardCore.Commerce.ContentFields/Views/PriceField.Edit.cshtml index 6678ec5f7..1c6df88d3 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Views/PriceField.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Views/PriceField.Edit.cshtml @@ -1,5 +1,4 @@ @using OrchardCore.ContentManagement.Metadata.Models -@using Microsoft.AspNetCore.Mvc.TagHelpers @using OrchardCore.Commerce.MoneyDataType @model PriceFieldEditViewModel diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Views/PriceFieldSettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.ContentFields/Views/PriceFieldSettings.Edit.cshtml index db643345e..b76fa04a1 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Views/PriceFieldSettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Views/PriceFieldSettings.Edit.cshtml @@ -1,5 +1,3 @@ -@using Microsoft.AspNetCore.Mvc.TagHelpers - @model PriceFieldSettingsEditViewModel @{ diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Views/_ViewImports.cshtml b/src/Modules/OrchardCore.Commerce.ContentFields/Views/_ViewImports.cshtml index 696c6662e..8bc846e3f 100644 --- a/src/Modules/OrchardCore.Commerce.ContentFields/Views/_ViewImports.cshtml +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Views/_ViewImports.cshtml @@ -8,5 +8,7 @@ @addTagHelper *, OrchardCore.DisplayManagement @addTagHelper *, OrchardCore.ResourceManagement +@using OrchardCore.Commerce.ViewModels +@using OrchardCore.Commerce.ContentFields.Constants @using OrchardCore.Commerce.ContentFields.ViewModels @using OrchardCore.Commerce.ContentFields.Settings diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/package.json b/src/Modules/OrchardCore.Commerce.ContentFields/package.json new file mode 100644 index 000000000..1c1c4367e --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "scripts": { + "build": "npm explore nodejs-extensions -- pnpm build:styles", + "lint": "npm explore nodejs-extensions -- pnpm lint", + "clean": "npm explore nodejs-extensions -- pnpm clean:styles", + "watch": "npm explore nodejs-extensions -- pnpm watch:styles" + }, + "devDependencies": { + "eslint": "8.47.0", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-import": "2.28.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-only-warn": "1.1.0", + "eslint-plugin-promise": "6.1.1" + } +} diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/pnpm-lock.yaml b/src/Modules/OrchardCore.Commerce.ContentFields/pnpm-lock.yaml new file mode 100644 index 000000000..79cab4ede --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/pnpm-lock.yaml @@ -0,0 +1,1439 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + eslint: + specifier: 8.47.0 + version: 8.47.0 + eslint-config-airbnb-base: + specifier: 15.0.0 + version: 15.0.0(eslint-plugin-import@2.28.1)(eslint@8.47.0) + eslint-plugin-import: + specifier: 2.28.1 + version: 2.28.1(eslint@8.47.0) + eslint-plugin-node: + specifier: 11.1.0 + version: 11.1.0(eslint@8.47.0) + eslint-plugin-only-warn: + specifier: 1.1.0 + version: 1.1.0 + eslint-plugin-promise: + specifier: 6.1.1 + version: 6.1.1(eslint@8.47.0) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.47.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.47.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.9.0: + resolution: {integrity: sha512-zJmuCWj2VLBt4c25CfBIbMZLGLyhkvs7LznyVX5HfpzeocThgIj5XQK4L+g3U36mMcx8bPMhGyPpwCATamC4jQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.2: + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.22.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.47.0: + resolution: {integrity: sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.11: + resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /acorn-jsx@5.3.2(acorn@8.10.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: true + + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: true + + /array.prototype.findlastindex@1.2.2: + resolution: {integrity: sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + dev: true + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + dev: true + + /confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: true + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.28.1)(eslint@8.47.0): + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + dependencies: + confusing-browser-globals: 1.0.11 + eslint: 8.47.0 + eslint-plugin-import: 2.28.1(eslint@8.47.0) + object.assign: 4.1.4 + object.entries: 1.1.7 + semver: 6.3.1 + dev: true + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7 + is-core-module: 2.13.0 + resolve: 1.22.6 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.8.0(eslint-import-resolver-node@0.3.9)(eslint@8.47.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + debug: 3.2.7 + eslint: 8.47.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-es@3.0.1(eslint@8.47.0): + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.47.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + + /eslint-plugin-import@2.28.1(eslint@8.47.0): + resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.2 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.47.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(eslint-import-resolver-node@0.3.9)(eslint@8.47.0) + has: 1.0.3 + is-core-module: 2.13.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.6 + object.groupby: 1.0.0 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-node@11.1.0(eslint@8.47.0): + resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=5.16.0' + dependencies: + eslint: 8.47.0 + eslint-plugin-es: 3.0.1(eslint@8.47.0) + eslint-utils: 2.1.0 + ignore: 5.2.4 + minimatch: 3.1.2 + resolve: 1.22.6 + semver: 6.3.1 + dev: true + + /eslint-plugin-only-warn@1.1.0: + resolution: {integrity: sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==} + engines: {node: '>=6'} + dev: true + + /eslint-plugin-promise@6.1.1(eslint@8.47.0): + resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.47.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + + /eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.47.0: + resolution: {integrity: sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.47.0) + '@eslint-community/regexpp': 4.9.0 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.47.0 + '@humanwhocodes/config-array': 0.11.11 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.22.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.1.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.1.0: + resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} + engines: {node: '>=12.0.0'} + dependencies: + flatted: 3.2.9 + keyv: 4.5.3 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: true + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@13.22.0: + resolution: {integrity: sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + + /keyv@4.5.3: + resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /object.fromentries@2.0.6: + resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /object.groupby@1.0.0: + resolution: {integrity: sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + dev: true + + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: true + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: true + + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/src/Modules/OrchardCore.Commerce.Inventory/OrchardCore.Commerce.Inventory.csproj b/src/Modules/OrchardCore.Commerce.Inventory/OrchardCore.Commerce.Inventory.csproj index 1fe117a85..7dcea8ba2 100644 --- a/src/Modules/OrchardCore.Commerce.Inventory/OrchardCore.Commerce.Inventory.csproj +++ b/src/Modules/OrchardCore.Commerce.Inventory/OrchardCore.Commerce.Inventory.csproj @@ -41,7 +41,7 @@ - + diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/.eslintrc.js b/src/Modules/OrchardCore.Commerce.Payment.Stripe/.eslintrc.js new file mode 100644 index 000000000..d62e24ed6 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + // Setting root=true prevents ESLint from taking into account .eslintrc files higher up in the directory tree. + root: true, + + // The following path may have to be adjusted to your directory structure. + extends: './node_modules/nodejs-extensions/config/.eslintrc.lombiq-base.js', + + // Add custom rules and overrides here. + rules: { + }, +}; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/.stylelintrc.js b/src/Modules/OrchardCore.Commerce.Payment.Stripe/.stylelintrc.js new file mode 100644 index 000000000..2ae551738 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/.stylelintrc.js @@ -0,0 +1,8 @@ +module.exports = { + // The following path may have to be adjusted to your directory structure. + extends: './node_modules/nodejs-extensions/config/.stylelintrc.lombiq-base.js', + + // Add custom rules and overrides here. + rules: { + }, +}; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IPaymentIntentPersistence.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IPaymentIntentPersistence.cs similarity index 69% rename from src/Modules/OrchardCore.Commerce/Abstractions/IPaymentIntentPersistence.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IPaymentIntentPersistence.cs index 46d6d34a8..52cba8e9f 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IPaymentIntentPersistence.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IPaymentIntentPersistence.cs @@ -1,4 +1,4 @@ -namespace OrchardCore.Commerce.Abstractions; +namespace OrchardCore.Commerce.Payment.Stripe.Abstractions; /// /// Service that provides a way to retain the current payment intent Id from the current session. @@ -14,4 +14,9 @@ public interface IPaymentIntentPersistence /// Saves a payment intent Id to the session. /// void Store(string paymentIntentId); + + /// + /// Removes the payment intent Id stored in the current session. + /// + void Remove(); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IRequestOptionsService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IRequestOptionsService.cs new file mode 100644 index 000000000..5e70dd027 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IRequestOptionsService.cs @@ -0,0 +1,21 @@ +using Stripe; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Stripe.Abstractions; + +/// +/// A service for accessing and managing this scope's Stripe value. +/// +public interface IRequestOptionsService +{ + /// + /// Returns this scope's request options. If it doesn't exist yet, creates one by referring to the site settings. + /// + Task GetOrCreateRequestOptionsAsync(); + + /// + /// Sets the to a new unique value. If the + /// is not yet initialized, it uses to create it first. + /// + Task SetIdempotencyKeyAsync(); +} diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IStripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs similarity index 55% rename from src/Modules/OrchardCore.Commerce/Abstractions/IStripePaymentService.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs index 1e7c58adb..7a75334b8 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IStripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs @@ -1,13 +1,15 @@ -using OrchardCore.Commerce.Constants; -using OrchardCore.Commerce.Models; +using Microsoft.AspNetCore.Mvc; +using OrchardCore.Commerce.Abstractions.Constants; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.Payment.Stripe.Models; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.ModelBinding; using Stripe; -using System.Collections.Generic; using System.Threading.Tasks; -namespace OrchardCore.Commerce.Abstractions; +namespace OrchardCore.Commerce.Payment.Stripe.Abstractions; /// /// When implemented handles the payment and creates an order. @@ -17,36 +19,29 @@ public interface IStripePaymentService /// /// Handles the payment and authentication, sends back the necessary data to the client./>. /// - /// A new instance of , or an existing one for the given - /// . - Task InitializePaymentIntentAsync(string paymentIntentId); + Task CreateClientSecretAsync(Amount total, ShoppingCartViewModel cart); /// /// Returns a object for the given . /// Task GetPaymentIntentAsync(string paymentIntentId); - /// - /// Calculates payment amount based on the given . - /// - long GetPaymentAmount(Amount total); - /// /// Returns a object based on the given . /// - Task CreatePaymentIntentAsync(long amountForPayment, Amount total); + Task CreatePaymentIntentAsync(Amount total); /// - /// Creates an order content item in the database, based on the and on the current content. + /// Creates an order content item in the database, based on the stored and on the + /// current content. /// - Task CreateOrUpdateOrderFromShoppingCartAsync(PaymentIntent paymentIntent, IUpdateModelAccessor updateModelAccessor); + Task CreateOrUpdateOrderFromShoppingCartAsync(IUpdateModelAccessor updateModelAccessor); /// /// Updates the corresponding order status to for the given - /// or the provided . + /// . /// - Task UpdateOrderToOrderedAsync(PaymentIntent paymentIntent = null, ContentItem orderItem = null); + Task UpdateOrderToOrderedAsync(PaymentIntent paymentIntent); /// /// Updates the corresponding order status to failed payment for the given . @@ -59,7 +54,11 @@ public interface IStripePaymentService Task GetOrderPaymentByPaymentIntentIdAsync(string paymentIntentId); /// - /// Returns a list from the given items. + /// A shortcut method for updating the status to , doing + /// final modifications and then redirecting to the success page. /// - Task> CreateOrderLineItemsAsync(ShoppingCart shoppingCart); + Task UpdateAndRedirectToFinishedOrderAsync( + Controller controller, + ContentItem order, + PaymentIntent paymentIntent); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/AdminMenu.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/AdminMenu.cs new file mode 100644 index 000000000..5467f0557 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/AdminMenu.cs @@ -0,0 +1,23 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Navigation; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Localization; +using OrchardCore.Commerce.Payment.Stripe.Drivers; +using OrchardCore.Navigation; + +namespace OrchardCore.Commerce.Payment.Stripe; + +public class AdminMenu : AdminMenuNavigationProviderBase +{ + public AdminMenu(IHttpContextAccessor hca, IStringLocalizer stringLocalizer) + : base(hca, stringLocalizer) + { } + + protected override void Build(NavigationBuilder builder) => + builder + .Add(T["Configuration"], configuration => configuration + .Add(T["Commerce"], commerce => commerce + .Add(T["Stripe API"], T["Stripe API"], stripeApi => stripeApi + .SiteSettings(StripeApiSettingsDisplayDriver.GroupId) + .Permission(Permissions.ManageStripeApiSettings) + .LocalNav()))); +} diff --git a/src/Modules/OrchardCore.Commerce/Assets/Scripts/stripe-payment-form.js b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Assets/Scripts/stripe-payment-form.js similarity index 94% rename from src/Modules/OrchardCore.Commerce/Assets/Scripts/stripe-payment-form.js rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Assets/Scripts/stripe-payment-form.js index 783fb03ea..145831738 100644 --- a/src/Modules/OrchardCore.Commerce/Assets/Scripts/stripe-payment-form.js +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Assets/Scripts/stripe-payment-form.js @@ -5,7 +5,8 @@ window.stripePaymentForm = function stripePaymentForm( antiForgeryToken, urlPrefix, errorText, - missingText) { + missingText, + updatePaymentIntentUrl) { const phone = document.getElementById('OrderPart_Phone_Text'); const email = document.getElementById('OrderPart_Email_Text'); @@ -27,9 +28,9 @@ window.stripePaymentForm = function stripePaymentForm( const allErrorContainers = [document.querySelector('.message-error')]; const form = document.querySelector('.payment-form'); - const submitButton = form.querySelector('.pay-button'); - const payText = form.querySelector('.pay-text'); - const paymentProcessingContainer = form.querySelector('.payment-processing-container'); + const submitButton = form.querySelector('.pay-button-stripe'); + const payText = submitButton.querySelector('.pay-text'); + const paymentProcessingContainer = submitButton.querySelector('.payment-processing-container'); const stripeElements = stripe.elements({ clientSecret, }); @@ -97,6 +98,10 @@ window.stripePaymentForm = function stripePaymentForm( event.preventDefault(); toggleInputs(false); + const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret); + + await fetch(updatePaymentIntentUrl.replace('PAYMENT_INTENT', paymentIntent.id)); + let result; try { const emptyRequiredFields = Array.from(form.querySelectorAll('input')) @@ -117,7 +122,7 @@ window.stripePaymentForm = function stripePaymentForm( }); } - const validationJson = await fetchPost('checkout/validate', { body: new FormData(form) }); + const validationJson = await fetchPost('checkout/validate/Stripe', { body: new FormData(form) }); if (validationJson?.errors?.length) { toggleInputs(true); throw validationJson.errors; @@ -126,7 +131,7 @@ window.stripePaymentForm = function stripePaymentForm( result = await stripe.confirmPayment({ elements: stripeElements, confirmParams: { - return_url: `${baseUrl}${urlPrefix}/checkout/middleware`, + return_url: `${baseUrl}${urlPrefix}/checkout/middleware/Stripe`, payment_method_data: { billing_details: { email: email.value, diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Constants/FeatureIds.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Constants/FeatureIds.cs new file mode 100644 index 000000000..356aa1be0 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Constants/FeatureIds.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Commerce.Payment.Stripe.Constants; + +public static class FeatureIds +{ + public const string Area = "OrchardCore.Commerce.Payment.Stripe"; +} diff --git a/src/Modules/OrchardCore.Commerce/Constants/PaymentIntentStatuses.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Constants/PaymentIntentStatuses.cs similarity index 88% rename from src/Modules/OrchardCore.Commerce/Constants/PaymentIntentStatuses.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Constants/PaymentIntentStatuses.cs index d109fcc58..4d72ff144 100644 --- a/src/Modules/OrchardCore.Commerce/Constants/PaymentIntentStatuses.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Constants/PaymentIntentStatuses.cs @@ -1,4 +1,4 @@ -namespace OrchardCore.Commerce.Constants; +namespace OrchardCore.Commerce.Payment.Stripe.Constants; public static class PaymentIntentStatuses { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Constants/ResourceNames.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Constants/ResourceNames.cs new file mode 100644 index 000000000..02c6af307 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Constants/ResourceNames.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Commerce.Payment.Stripe.Constants; + +public static class ResourceNames +{ + public const string StripePaymentForm = nameof(StripePaymentForm); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs new file mode 100644 index 000000000..ed3484460 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Localization; +using OrchardCore.Commerce.Abstractions.Constants; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Payment.Stripe.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Constants; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Mvc.Utilities; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Stripe.Controllers; + +public class StripeController : Controller +{ + private readonly IContentManager _contentManager; + private readonly INotifier _notifier; + private readonly IPaymentIntentPersistence _paymentIntentPersistence; + private readonly IStripePaymentService _stripePaymentService; + private readonly IHtmlLocalizer H; + + public StripeController( + IContentManager contentManager, + INotifier notifier, + IPaymentIntentPersistence paymentIntentPersistence, + IStripePaymentService stripePaymentService, + IHtmlLocalizer htmlLocalizer) + { + _contentManager = contentManager; + _notifier = notifier; + _paymentIntentPersistence = paymentIntentPersistence; + _stripePaymentService = stripePaymentService; + H = htmlLocalizer; + } + + public IActionResult UpdatePaymentIntent(string paymentIntent) + { + _paymentIntentPersistence.Store(paymentIntent); + return Ok(); + } + + [AllowAnonymous] + [HttpGet("checkout/middleware/Stripe")] + public async Task PaymentConfirmationMiddleware([FromQuery(Name = "payment_intent")] string paymentIntent = null) + { + // If it is null it means the session was not loaded yet and a redirect is needed. + if (string.IsNullOrEmpty(_paymentIntentPersistence.Retrieve())) + { + return View(); + } + + // If we can't find a valid payment intent based on ID or if we can't find the associated order, then something + // went wrong and continuing from here would only cause a crash anyway. + if (await _stripePaymentService.GetPaymentIntentAsync(paymentIntent) is not { PaymentMethod: not null } fetchedPaymentIntent || + (await _stripePaymentService.GetOrderPaymentByPaymentIntentIdAsync(paymentIntent))?.OrderId is not { } orderId || + await _contentManager.GetAsync(orderId) is not { } order) + { + await _notifier.ErrorAsync( + H["Couldn't find the payment intent \"{0}\" or the order associated with it.", paymentIntent ?? string.Empty]); + return NotFound(); + } + + var status = order.As()?.Status?.Text; + var succeeded = fetchedPaymentIntent.Status == PaymentIntentStatuses.Succeeded; + + // Looks like there is nothing to do here. + if (succeeded && status == OrderStatuses.Ordered.HtmlClassify()) + { + return this.RedirectToContentDisplay(order); + } + + if (succeeded && status == OrderStatuses.Pending.HtmlClassify()) + { + return await _stripePaymentService.UpdateAndRedirectToFinishedOrderAsync( + this, + order, + fetchedPaymentIntent); + } + + if (status == OrderStatuses.PaymentFailed.HtmlClassify()) + { + return await PaymentFailedAsync(); + } + + order.Alter(part => part.RetryCounter++); + await _contentManager.UpdateAsync(order); + + if (order.As().RetryCounter <= 10) + { + return View(); + } + + // Delete payment intent from session, to create a new one. + _paymentIntentPersistence.Remove(); + return await PaymentFailedAsync(); + } + + private async Task PaymentFailedAsync() + { + await _notifier.ErrorAsync(H["The has payment failed, please try again."]); + return Redirect("~/checkout"); + } +} diff --git a/src/Modules/OrchardCore.Commerce/Controllers/WebhookController.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/WebhookController.cs similarity index 89% rename from src/Modules/OrchardCore.Commerce/Controllers/WebhookController.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/WebhookController.cs index f283c2e64..8db31e07e 100644 --- a/src/Modules/OrchardCore.Commerce/Controllers/WebhookController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/WebhookController.cs @@ -2,15 +2,15 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Payment.Stripe.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Models; using OrchardCore.Entities; using OrchardCore.Settings; using Stripe; using System.IO; using System.Threading.Tasks; -namespace OrchardCore.Commerce.Controllers; +namespace OrchardCore.Commerce.Payment.Stripe.Controllers; [Route("stripe-webhook")] [ApiController] @@ -51,7 +51,7 @@ public async Task Index() // Let the logic handle version mismatch. throwOnApiVersionMismatch: false); - if (stripeEvent.Type == Stripe.Events.ChargeSucceeded) + if (stripeEvent.Type == Events.ChargeSucceeded) { var charge = stripeEvent.Data.Object as Charge; if (charge?.PaymentIntentId is not { } paymentIntentId) @@ -62,7 +62,7 @@ public async Task Index() var paymentIntent = await _stripePaymentService.GetPaymentIntentAsync(paymentIntentId); await _stripePaymentService.UpdateOrderToOrderedAsync(paymentIntent); } - else if (stripeEvent.Type == Stripe.Events.PaymentIntentPaymentFailed) + else if (stripeEvent.Type == Events.PaymentIntentPaymentFailed) { var paymentIntent = stripeEvent.Data.Object as PaymentIntent; await _stripePaymentService.UpdateOrderToPaymentFailedAsync(paymentIntent); diff --git a/src/Modules/OrchardCore.Commerce/Drivers/StripeApiSettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Drivers/StripeApiSettingsDisplayDriver.cs similarity index 94% rename from src/Modules/OrchardCore.Commerce/Drivers/StripeApiSettingsDisplayDriver.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Drivers/StripeApiSettingsDisplayDriver.cs index c00bfcf26..ed213728b 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/StripeApiSettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Drivers/StripeApiSettingsDisplayDriver.cs @@ -2,10 +2,10 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using OrchardCore.Commerce.Extensions; -using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.Services; -using OrchardCore.Commerce.ViewModels; +using OrchardCore.Commerce.Payment.Stripe.Extensions; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.Commerce.Payment.Stripe.Services; +using OrchardCore.Commerce.Payment.Stripe.ViewModels; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; @@ -13,7 +13,7 @@ using OrchardCore.Settings; using System.Threading.Tasks; -namespace OrchardCore.Commerce.Drivers; +namespace OrchardCore.Commerce.Payment.Stripe.Drivers; public class StripeApiSettingsDisplayDriver : SectionDisplayDriver { diff --git a/src/Modules/OrchardCore.Commerce/Extensions/PaymentExtensions.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Extensions/PaymentExtensions.cs similarity index 72% rename from src/Modules/OrchardCore.Commerce/Extensions/PaymentExtensions.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Extensions/PaymentExtensions.cs index 43a3b82cd..0513a8b6e 100644 --- a/src/Modules/OrchardCore.Commerce/Extensions/PaymentExtensions.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Extensions/PaymentExtensions.cs @@ -1,16 +1,11 @@ -using Stripe; -using System; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.MoneyDataType; +using Stripe; -namespace OrchardCore.Commerce.Extensions; +namespace OrchardCore.Commerce.Payment.Stripe.Extensions; public static class PaymentExtensions { - public static RequestOptions SetIdempotencyKey(this RequestOptions requestOptions) - { - requestOptions.IdempotencyKey = Guid.NewGuid().ToString(); - return requestOptions; - } - public static void AddExpansions(this BaseOptions baseOptions) => baseOptions.AddExpand("payment_method"); public static string GetFormattedPaymentType(this PaymentMethod paymentMethod) => @@ -48,4 +43,12 @@ public static string GetFormattedPaymentType(this PaymentMethod paymentMethod) = "wechat_pay" => "WeChat Pay", _ => paymentMethod.Type, }; + + public static IPayment CreatePayment(this PaymentIntent paymentIntent, Amount amount) => + new Commerce.Models.Payment( + Kind: paymentIntent.PaymentMethod?.GetFormattedPaymentType() ?? "Unset", + ChargeText: paymentIntent.Description, + TransactionId: paymentIntent.Id, + Amount: amount, + CreatedUtc: paymentIntent.Created); } diff --git a/src/Modules/OrchardCore.Commerce/Extensions/StripeApiKeyExtensions.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Extensions/StripeApiKeyExtensions.cs similarity index 88% rename from src/Modules/OrchardCore.Commerce/Extensions/StripeApiKeyExtensions.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Extensions/StripeApiKeyExtensions.cs index 4e574925e..b330982e6 100644 --- a/src/Modules/OrchardCore.Commerce/Extensions/StripeApiKeyExtensions.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Extensions/StripeApiKeyExtensions.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging; -using OrchardCore.Commerce.Services; +using OrchardCore.Commerce.Payment.Stripe.Services; using System; -namespace OrchardCore.Commerce.Extensions; +namespace OrchardCore.Commerce.Payment.Stripe.Extensions; public static class StripeApiKeyExtensions { public static string DecryptStripeApiKey( diff --git a/src/Modules/OrchardCore.Commerce/Indexes/OrderPaymentIndex.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Indexes/OrderPaymentIndex.cs similarity index 86% rename from src/Modules/OrchardCore.Commerce/Indexes/OrderPaymentIndex.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Indexes/OrderPaymentIndex.cs index 855acc7d9..a2d5d5dbb 100644 --- a/src/Modules/OrchardCore.Commerce/Indexes/OrderPaymentIndex.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Indexes/OrderPaymentIndex.cs @@ -1,7 +1,7 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Payment.Stripe.Models; using YesSql.Indexes; -namespace OrchardCore.Commerce.Indexes; +namespace OrchardCore.Commerce.Payment.Stripe.Indexes; public class OrderPaymentIndex : MapIndex { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/License.md b/src/Modules/OrchardCore.Commerce.Payment.Stripe/License.md new file mode 100644 index 000000000..798b9cdec --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 OrchardCMS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Manifest.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Manifest.cs new file mode 100644 index 000000000..885336251 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Manifest.cs @@ -0,0 +1,15 @@ +using OrchardCore.Modules.Manifest; +using static OrchardCore.Commerce.Payment.Constants.FeatureIds; +using static OrchardCore.Commerce.Promotion.Constants.FeatureIds; + +[assembly: Module( + Name = "Orchard Core Commerce - Payment - Stripe", + Author = "The Orchard Team", + Website = "https://github.com/OrchardCMS/OrchardCore.Commerce", + Version = "0.0.1", + Description = + "Stripe payment provider for Orchard Core Commerce. Note: you must configure it in Admin > Configuration > " + + "Commerce > Stripe API or it won't appear in the front end.", + Category = "Commerce", + Dependencies = new[] { Payment, Promotion } +)] diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Migrations/StripeMigrations.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Migrations/StripeMigrations.cs new file mode 100644 index 000000000..b0b8a3d3d --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Migrations/StripeMigrations.cs @@ -0,0 +1,39 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Data; +using OrchardCore.Commerce.Payment.Stripe.Indexes; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.ContentManagement.Metadata; +using OrchardCore.ContentManagement.Metadata.Settings; +using OrchardCore.Data.Migration; +using YesSql.Sql; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; + +namespace OrchardCore.Commerce.Payment.Stripe.Migrations; + +public class StripeMigrations : DataMigration +{ + private readonly IContentDefinitionManager _contentDefinitionManager; + + public StripeMigrations(IContentDefinitionManager contentDefinitionManager) => + _contentDefinitionManager = contentDefinitionManager; + + public int Create() + { + _contentDefinitionManager + .AlterPartDefinition(builder => builder + .Configure(part => part.Attachable()) + .WithField(part => part.PaymentIntentId)); + + _contentDefinitionManager + .AlterTypeDefinition(Order, builder => builder + .WithPart(nameof(StripePaymentPart))); + + // This table may exist when migrating from an old version of the DB where it was created in a different module. + SchemaBuilder.DropMapIndexTable(typeof(OrderPaymentIndex)); + SchemaBuilder + .CreateMapIndexTable(table => table + .Column(nameof(OrderPaymentIndex.OrderId), column => column.WithCommonUniqueIdLength()) + .Column(nameof(OrderPaymentIndex.PaymentIntentId))); + + return 1; + } +} diff --git a/src/Modules/OrchardCore.Commerce/Models/OrderPayment.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/OrderPayment.cs similarity index 67% rename from src/Modules/OrchardCore.Commerce/Models/OrderPayment.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/OrderPayment.cs index 016aacd09..03d923f0e 100644 --- a/src/Modules/OrchardCore.Commerce/Models/OrderPayment.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/OrderPayment.cs @@ -1,4 +1,4 @@ -namespace OrchardCore.Commerce.Models; +namespace OrchardCore.Commerce.Payment.Stripe.Models; public class OrderPayment { diff --git a/src/Modules/OrchardCore.Commerce/Models/StripeApiSettings.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripeApiSettings.cs similarity index 85% rename from src/Modules/OrchardCore.Commerce/Models/StripeApiSettings.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripeApiSettings.cs index 281991ec8..4474ed3a1 100644 --- a/src/Modules/OrchardCore.Commerce/Models/StripeApiSettings.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripeApiSettings.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging; -using OrchardCore.Commerce.Extensions; +using OrchardCore.Commerce.Payment.Stripe.Extensions; -namespace OrchardCore.Commerce.Models; +namespace OrchardCore.Commerce.Payment.Stripe.Models; public class StripeApiSettings { diff --git a/src/Modules/OrchardCore.Commerce/Models/StripePaymentPart.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripePaymentPart.cs similarity index 80% rename from src/Modules/OrchardCore.Commerce/Models/StripePaymentPart.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripePaymentPart.cs index f972e148d..1ca54fa50 100644 --- a/src/Modules/OrchardCore.Commerce/Models/StripePaymentPart.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Models/StripePaymentPart.cs @@ -1,7 +1,7 @@ using OrchardCore.ContentFields.Fields; using OrchardCore.ContentManagement; -namespace OrchardCore.Commerce.Models; +namespace OrchardCore.Commerce.Payment.Stripe.Models; public class StripePaymentPart : ContentPart { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCore.Commerce.Payment.Stripe.csproj b/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCore.Commerce.Payment.Stripe.csproj new file mode 100644 index 000000000..f5f0e1f72 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCore.Commerce.Payment.Stripe.csproj @@ -0,0 +1,51 @@ + + + + net6.0 + true + + + + Orchard Core Commerce - Payment - Stripe + Bertrand Le Roy + Copyright © 2018 .NET Foundation + Stripe payment provider for Orchard Core Commerce + OrchardCore;OrchardCore.Commerce;Commerce;e-Commerce;Payment;Stripe + https://github.com/OrchardCMS/OrchardCore.Commerce + https://github.com/OrchardCMS/OrchardCore.Commerce/blob/main/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md + License.md + OrchardCoreIcon.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCoreIcon.png b/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCoreIcon.png new file mode 100644 index 000000000..c72b029ac Binary files /dev/null and b/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCoreIcon.png differ diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Permissions.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Permissions.cs new file mode 100644 index 000000000..4e3d86245 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Permissions.cs @@ -0,0 +1,17 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Users; +using OrchardCore.Security.Permissions; +using System.Collections.Generic; + +namespace OrchardCore.Commerce.Payment.Stripe; + +public class Permissions : AdminPermissionBase +{ + public static readonly Permission ManageStripeApiSettings = new(nameof(ManageStripeApiSettings), "Manage Stripe API Settings"); + + private static readonly IReadOnlyList _adminPermissions = new[] + { + ManageStripeApiSettings, + }; + + protected override IEnumerable AdminPermissions => _adminPermissions; +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md new file mode 100644 index 000000000..626fa7db3 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md @@ -0,0 +1,7 @@ +# Orchard Core Commerce - Inventory + +## About + +Stripe payment provider for Orchard Core Commerce. + +For general details about and on using Orchard Core Commerce see the [root Readme](../../../Readme.md). diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/ResourceManagementOptionsConfiguration.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/ResourceManagementOptionsConfiguration.cs new file mode 100644 index 000000000..0e5a4220a --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/ResourceManagementOptionsConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Options; +using OrchardCore.ResourceManagement; +using static OrchardCore.Commerce.Payment.Stripe.Constants.ResourceNames; + +namespace OrchardCore.Commerce.Payment.Stripe; + +public class ResourceManagementOptionsConfiguration : IConfigureOptions +{ + private static readonly ResourceManifest _manifest = new(); + + static ResourceManagementOptionsConfiguration() => + _manifest + .DefineScript(StripePaymentForm) + .SetUrl( + "~/OrchardCore.Commerce.Payment.Stripe/js/stripe-payment-form.min.js", + "~/OrchardCore.Commerce.Payment.Stripe/js/stripe-payment-form.js") + .SetVersion("1.0.0"); + + public void Configure(ResourceManagementOptions options) => options.ResourceManifests.Add(_manifest); +} diff --git a/src/Modules/OrchardCore.Commerce/Services/PaymentIntentPersistence.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/PaymentIntentPersistence.cs similarity index 78% rename from src/Modules/OrchardCore.Commerce/Services/PaymentIntentPersistence.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/PaymentIntentPersistence.cs index 6449d1fc5..a6c02fc22 100644 --- a/src/Modules/OrchardCore.Commerce/Services/PaymentIntentPersistence.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/PaymentIntentPersistence.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http; -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Abstractions; -namespace OrchardCore.Commerce.Services; +namespace OrchardCore.Commerce.Payment.Stripe.Services; public class PaymentIntentPersistence : IPaymentIntentPersistence { @@ -15,4 +15,6 @@ public class PaymentIntentPersistence : IPaymentIntentPersistence public string Retrieve() => Session.GetString(PaymentIntentKey); public void Store(string paymentIntentId) => Session.SetString(PaymentIntentKey, paymentIntentId); + + public void Remove() => Session.Remove(PaymentIntentKey); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/RequestOptionsService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/RequestOptionsService.cs new file mode 100644 index 000000000..4179b96d9 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/RequestOptionsService.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using OrchardCore.Commerce.Payment.Stripe.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Extensions; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.Entities; +using OrchardCore.Settings; +using Stripe; +using System; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Stripe.Services; + +public class RequestOptionsService : IRequestOptionsService +{ + private readonly ISiteService _siteService; + + private readonly Func _apiKeyAccessor; + private RequestOptions _requestOptions; + + public RequestOptionsService( + ISiteService siteService, + IDataProtectionProvider dataProtectionProvider, + ILogger logger) + { + _siteService = siteService; + + _apiKeyAccessor = siteSettings => siteSettings + .As() + .SecretKey + .DecryptStripeApiKey(dataProtectionProvider, logger); + } + + public Task GetOrCreateRequestOptionsAsync() => + _requestOptions == null ? CreateRequestOptionsAsync() : Task.FromResult(_requestOptions); + + public async Task SetIdempotencyKeyAsync() + { + var requestOptions = await GetOrCreateRequestOptionsAsync(); + requestOptions.IdempotencyKey = Guid.NewGuid().ToString(); + + return requestOptions; + } + + private async Task CreateRequestOptionsAsync() + { + var siteSettings = await _siteService.GetSiteSettingsAsync(); + var apiKey = _apiKeyAccessor(siteSettings); + + _requestOptions = new RequestOptions { ApiKey = apiKey }; + return _requestOptions; + } +} diff --git a/src/Modules/OrchardCore.Commerce/Services/StripeApiSettingsConfiguration.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeApiSettingsConfiguration.cs similarity index 91% rename from src/Modules/OrchardCore.Commerce/Services/StripeApiSettingsConfiguration.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeApiSettingsConfiguration.cs index 75ea8fb60..a3943d4a5 100644 --- a/src/Modules/OrchardCore.Commerce/Services/StripeApiSettingsConfiguration.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeApiSettingsConfiguration.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Payment.Stripe.Models; using OrchardCore.Entities; using OrchardCore.Settings; -namespace OrchardCore.Commerce.Services; +namespace OrchardCore.Commerce.Payment.Stripe.Services; public class StripeApiSettingsConfiguration : IConfigureOptions { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeOrderContentTypeDefinitionExclusionProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeOrderContentTypeDefinitionExclusionProvider.cs new file mode 100644 index 000000000..c7de7f603 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripeOrderContentTypeDefinitionExclusionProvider.cs @@ -0,0 +1,18 @@ +using OrchardCore.Commerce.Payment.Abstractions; +using System; +using System.Collections.Generic; + +namespace OrchardCore.Commerce.Payment.Stripe.Services; + +public class StripeOrderContentTypeDefinitionExclusionProvider : IOrderContentTypeDefinitionExclusionProvider +{ + private static readonly string[] _excludedShapes = + { + "Order_Checkout__StripePaymentPart__PaymentIntentId", + "Order_Checkout__StripePaymentPart__PaymentMethodId", + }; + + public IEnumerable GetExcludedShapes( + IEnumerable<(string ShapeType, Uri Url, bool IsNew)> templateLinks) => + _excludedShapes; +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentProvider.cs new file mode 100644 index 000000000..76d68cad9 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentProvider.cs @@ -0,0 +1,92 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Controllers; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.Entities; +using OrchardCore.Settings; +using Stripe; +using System; +using System.Threading.Tasks; +using ISession = YesSql.ISession; + +namespace OrchardCore.Commerce.Payment.Stripe.Services; + +public class StripePaymentProvider : IPaymentProvider +{ + public const string ProviderName = "Stripe"; + + private readonly IHttpContextAccessor _hca; + private readonly IPaymentIntentPersistence _paymentIntentPersistence; + private readonly ISession _session; + private readonly ISiteService _siteService; + private readonly IStripePaymentService _stripePaymentService; + + public string Name => ProviderName; + + public StripePaymentProvider( + IHttpContextAccessor hca, + IPaymentIntentPersistence paymentIntentPersistence, + ISession session, + ISiteService siteService, + IStripePaymentService stripePaymentService) + { + _hca = hca; + _paymentIntentPersistence = paymentIntentPersistence; + _session = session; + _siteService = siteService; + _stripePaymentService = stripePaymentService; + } + + public async Task CreatePaymentProviderDataAsync(IPaymentViewModel model) + { + PaymentIntent paymentIntent; + + try + { + paymentIntent = await _stripePaymentService.CreatePaymentIntentAsync(model.SingleCurrencyTotal); + } + catch (StripeException exception) when (exception.Message.StartsWithOrdinal("No API key provided.")) + { + return null; + } + + if (_hca.HttpContext?.GetRouteValue("action")?.ToString() == nameof(PaymentController.PaymentRequest)) + { + _session.Save(new OrderPayment + { + OrderId = model.OrderPart.ContentItem.ContentItemId, + PaymentIntentId = paymentIntent.Id, + }); + } + + return new + { + (await _siteService.GetSiteSettingsAsync()).As().PublishableKey, + paymentIntent.ClientSecret, + }; + } + + public Task ValidateAsync(IUpdateModelAccessor updateModelAccessor) => + _stripePaymentService.CreateOrUpdateOrderFromShoppingCartAsync(updateModelAccessor); + + public Task FinalModificationOfOrderAsync(ContentItem order, string shoppingCartId) + { + // A new payment intent should be created on the next checkout. + _paymentIntentPersistence.Remove(); + + return Task.CompletedTask; + } + + public Task UpdateAndRedirectToFinishedOrderAsync( + Controller controller, + ContentItem order, + string shoppingCartId) => + throw new NotSupportedException( + "This code should never be reached, because Stripe payment uses ~/checkout/middleware/Stripe, not ~/checkout/callback/Stripe."); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs new file mode 100644 index 000000000..6ad636c0f --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs @@ -0,0 +1,247 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; +using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Constants; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; +using OrchardCore.Commerce.Constants; +using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.Payment.Stripe.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Constants; +using OrchardCore.Commerce.Payment.Stripe.Extensions; +using OrchardCore.Commerce.Payment.Stripe.Indexes; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.Commerce.Promotion.Extensions; +using OrchardCore.ContentFields.Fields; +using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.Entities; +using OrchardCore.Mvc.Utilities; +using OrchardCore.Settings; +using Stripe; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using YesSql; + +namespace OrchardCore.Commerce.Payment.Stripe.Services; + +public class StripePaymentService : IStripePaymentService +{ + private readonly PaymentIntentService _paymentIntentService = new(); + + private readonly IContentManager _contentManager; + private readonly ISiteService _siteService; + private readonly IRequestOptionsService _requestOptionsService; + private readonly IStringLocalizer T; + private readonly ISession _session; + private readonly IPaymentIntentPersistence _paymentIntentPersistence; + private readonly IPaymentService _paymentService; + + public StripePaymentService( + IContentManager contentManager, + ISiteService siteService, + IRequestOptionsService requestOptionsService, + IStringLocalizer stringLocalizer, + ISession session, + IPaymentIntentPersistence paymentIntentPersistence, + IPaymentService paymentService) + { + _contentManager = contentManager; + _siteService = siteService; + _requestOptionsService = requestOptionsService; + _session = session; + _paymentIntentPersistence = paymentIntentPersistence; + T = stringLocalizer; + _paymentService = paymentService; + } + + public async Task CreateClientSecretAsync(Amount total, ShoppingCartViewModel cart) + { + var stripeApiSettings = (await _siteService.GetSiteSettingsAsync()).As(); + + if (string.IsNullOrEmpty(stripeApiSettings.PublishableKey) || + string.IsNullOrEmpty(stripeApiSettings.SecretKey) || + total.Value <= 0) + { + return null; + } + + var paymentIntentId = _paymentIntentPersistence.Retrieve(); + var totals = cart.GetTotalsOrThrowIfEmpty(); + + // Same here as on the checkout page: Later we have to figure out what to do if there are multiple + // totals i.e., multiple currencies. https://github.com/OrchardCMS/OrchardCore.Commerce/issues/132 + var defaultTotal = totals.SingleOrDefault(); + + var initPaymentIntent = string.IsNullOrEmpty(paymentIntentId) + ? await CreatePaymentIntentAsync(defaultTotal) + : await GetOrUpdatePaymentIntentAsync(paymentIntentId, defaultTotal); + + return initPaymentIntent.ClientSecret; + } + + public async Task GetPaymentIntentAsync(string paymentIntentId) + { + var paymentIntentGetOptions = new PaymentIntentGetOptions(); + paymentIntentGetOptions.AddExpansions(); + return await _paymentIntentService.GetAsync( + paymentIntentId, + paymentIntentGetOptions, + await _requestOptionsService.SetIdempotencyKeyAsync()); + } + + public async Task UpdateOrderToOrderedAsync(PaymentIntent paymentIntent) => + await _paymentService.UpdateOrderToOrderedAsync( + await GetOrderByPaymentIntentIdAsync(paymentIntent.Id), + shoppingCartId: null, + CreateChargesProvider(paymentIntent)); + + public Task UpdateAndRedirectToFinishedOrderAsync( + Controller controller, + ContentItem order, + PaymentIntent paymentIntent) => + _paymentService.UpdateAndRedirectToFinishedOrderAsync( + controller, + order, + shoppingCartId: null, + StripePaymentProvider.ProviderName, + CreateChargesProvider(paymentIntent)); + + private static Func> CreateChargesProvider(PaymentIntent paymentIntent) => + orderPart => orderPart.Charges.Select(charge => paymentIntent.CreatePayment(charge.Amount)); + + public async Task UpdateOrderToPaymentFailedAsync(PaymentIntent paymentIntent) + { + var order = await GetOrderByPaymentIntentIdAsync(paymentIntent.Id); + order.Alter(orderPart => + orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.PaymentFailed.HtmlClassify() }); + + await _contentManager.UpdateAsync(order); + } + + public Task GetOrderPaymentByPaymentIntentIdAsync(string paymentIntentId) => + _session + .Query(index => index.PaymentIntentId == paymentIntentId) + .FirstOrDefaultAsync(); + + public async Task CreateOrUpdateOrderFromShoppingCartAsync(IUpdateModelAccessor updateModelAccessor) + { + var paymentIntent = await GetPaymentIntentAsync(_paymentIntentPersistence.Retrieve()); + + // Stripe doesn't support multiple shopping cart IDs because we can't send that info to the middleware anyway. + var (order, isNew) = await _paymentService.CreateOrUpdateOrderFromShoppingCartAsync( + updateModelAccessor, + (await GetOrderPaymentByPaymentIntentIdAsync(paymentIntent.Id))?.OrderId, + shoppingCartId: null, + (order, _, total, cartViewModel, lineItems) => + { + order.Alter(orderPart => + { + orderPart.Charges.Clear(); + orderPart.Charges.Add(paymentIntent.CreatePayment(total)); + + if (cartViewModel is null) return; + + // Shopping cart + orderPart.LineItems.SetItems(lineItems); + orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.Pending.HtmlClassify() }; + + // Store the current applicable discount info so they will be available in the future. + orderPart.AdditionalData.SetDiscountsByProduct(cartViewModel + .Lines + .Where(line => line.AdditionalData.GetDiscounts().Any()) + .ToDictionary( + line => line.ProductSku, + line => line.AdditionalData.GetDiscounts())); + }); + + order.Alter(part => + part.PaymentIntentId = new TextField { ContentItem = order, Text = paymentIntent.Id }); + + return Task.CompletedTask; + }); + + if (!order.As().LineItems.Any()) + { + updateModelAccessor.ModelUpdater.ModelState.AddModelError( + nameof(OrderPart.LineItems), + T["The order is empty."].Value); + } + + if (isNew) + { + _session.Save(new OrderPayment + { + OrderId = order.ContentItemId, + PaymentIntentId = paymentIntent.Id, + }); + } + + return order; + } + + private static long GetPaymentAmount(Amount total) + { + if (CurrencyCollectionConstants.ZeroDecimalCurrencies.Contains(total.Currency.CurrencyIsoCode)) + { + return (long)Math.Round(total.Value); + } + + return CurrencyCollectionConstants.SpecialCases.Contains(total.Currency.CurrencyIsoCode) + ? (long)Math.Round(total.Value / 100m) * 10000 + : (long)Math.Round(total.Value * 100); + } + + public async Task CreatePaymentIntentAsync(Amount total) + { + var siteSettings = await _siteService.GetSiteSettingsAsync(); + var paymentIntentOptions = new PaymentIntentCreateOptions + { + Amount = GetPaymentAmount(total), + Currency = total.Currency.CurrencyIsoCode, + Description = T["User checkout on {0}", siteSettings.SiteName].Value, + AutomaticPaymentMethods = new PaymentIntentAutomaticPaymentMethodsOptions { Enabled = true, }, + }; + + var paymentIntent = await _paymentIntentService.CreateAsync( + paymentIntentOptions, + await _requestOptionsService.SetIdempotencyKeyAsync()); + + _paymentIntentPersistence.Store(paymentIntent.Id); + + return paymentIntent; + } + + private async Task GetOrUpdatePaymentIntentAsync( + string paymentIntentId, + Amount defaultTotal) + { + var paymentIntent = await GetPaymentIntentAsync(paymentIntentId); + + if (paymentIntent?.Status is PaymentIntentStatuses.Succeeded or PaymentIntentStatuses.Processing) + { + return paymentIntent; + } + + var updateOptions = new PaymentIntentUpdateOptions + { + Amount = GetPaymentAmount(defaultTotal), + Currency = defaultTotal.Currency.CurrencyIsoCode, + }; + + updateOptions.AddExpansions(); + return await _paymentIntentService.UpdateAsync( + paymentIntentId, + updateOptions, + await _requestOptionsService.SetIdempotencyKeyAsync()); + } + + private async Task GetOrderByPaymentIntentIdAsync(string paymentIntentId) + { + var orderId = (await GetOrderPaymentByPaymentIntentIdAsync(paymentIntentId))?.OrderId; + return await _contentManager.GetAsync(orderId); + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Startup.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Startup.cs new file mode 100644 index 000000000..46defe8d1 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Startup.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.Drivers; +using OrchardCore.Commerce.Payment.Stripe.Indexes; +using OrchardCore.Commerce.Payment.Stripe.Migrations; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.Commerce.Payment.Stripe.Services; +using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Modules; +using OrchardCore.Navigation; +using OrchardCore.ResourceManagement; +using OrchardCore.Settings; + +namespace OrchardCore.Commerce.Payment.Stripe; + +public class Startup : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) + { + services.AddTransient, ResourceManagementOptionsConfiguration>(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient, StripeApiSettingsConfiguration>(); + + services.AddContentPart().WithMigration().WithIndex(); + services.AddScoped, StripeApiSettingsDisplayDriver>(); + + services.AddScoped(); + } +} diff --git a/src/Modules/OrchardCore.Commerce/ViewModels/StripeApiSettingsViewModel.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/ViewModels/StripeApiSettingsViewModel.cs similarity index 76% rename from src/Modules/OrchardCore.Commerce/ViewModels/StripeApiSettingsViewModel.cs rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/ViewModels/StripeApiSettingsViewModel.cs index cfc3099fe..07154f506 100644 --- a/src/Modules/OrchardCore.Commerce/ViewModels/StripeApiSettingsViewModel.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/ViewModels/StripeApiSettingsViewModel.cs @@ -1,4 +1,4 @@ -namespace OrchardCore.Commerce.ViewModels; +namespace OrchardCore.Commerce.Payment.Stripe.ViewModels; public class StripeApiSettingsViewModel { diff --git a/src/Modules/OrchardCore.Commerce/Views/StripeCheckout.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/CheckoutStripe.cshtml similarity index 60% rename from src/Modules/OrchardCore.Commerce/Views/StripeCheckout.cshtml rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/CheckoutStripe.cshtml index 6a3141d6c..cd24fcb02 100644 --- a/src/Modules/OrchardCore.Commerce/Views/StripeCheckout.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/CheckoutStripe.cshtml @@ -1,7 +1,8 @@ @using Microsoft.AspNetCore.Mvc.Localization +@using OrchardCore @using OrchardCore.Commerce.MoneyDataType +@using OrchardCore.Commerce.Payment.Stripe.Controllers @using OrchardCore.Mvc.Core.Utilities -@using Microsoft.AspNetCore.Html @{ if (Model.DefaultTotal is not Amount defaultTotal) @@ -9,16 +10,8 @@ throw new InvalidOperationException("Missing DefaultTotal"); } - string publishableKey = Model.PublishableKey ?? string.Empty; - string clientSecret = Model.ClientSecret ?? string.Empty; - - // We use HtmlString to exclude this from the localization string. This way translators don't have to worry about - // ruining functional HTML by accident. - var totalHtml = new HtmlString( - "" + - $"{HtmlEncoder.Encode(defaultTotal.ToString())}"); + string publishableKey = Model.PaymentProviderData?.PublishableKey ?? string.Empty; + string clientSecret = Model.PaymentProviderData?.ClientSecret ?? string.Empty; } @@ -42,18 +35,7 @@ } -
- -
+ @@ -69,6 +51,7 @@ document.querySelector('input[name="__RequestVerificationToken"]').value, @Url.Content("~/").TrimEnd('/').JsonHtmlContent(), @T["There was an error during confirming the payment, please try again."].Json(), - @T["A value is required for %LABEL%."].Json()); + @T["A value is required for %LABEL%."].Json(), + @(Orchard.Action(controller => controller.UpdatePaymentIntent("PAYMENT_INTENT")).JsonHtmlContent())); } diff --git a/src/Modules/OrchardCore.Commerce/Views/Payment/PaymentConfirmationMiddleware.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/Stripe/PaymentConfirmationMiddleware.cshtml similarity index 100% rename from src/Modules/OrchardCore.Commerce/Views/Payment/PaymentConfirmationMiddleware.cshtml rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/Stripe/PaymentConfirmationMiddleware.cshtml diff --git a/src/Modules/OrchardCore.Commerce/Views/StripeApiSettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml similarity index 95% rename from src/Modules/OrchardCore.Commerce/Views/StripeApiSettings.Edit.cshtml rename to src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml index cc0d47ff3..faa386cd7 100644 --- a/src/Modules/OrchardCore.Commerce/Views/StripeApiSettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml @@ -1,4 +1,4 @@ -@model StripeApiSettingsViewModel +@model OrchardCore.Commerce.Payment.Stripe.ViewModels.StripeApiSettingsViewModel

@T["The current tenant will be reloaded when the settings are saved."]

diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/_ViewImports.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/_ViewImports.cshtml new file mode 100644 index 000000000..d5e5ec391 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/_ViewImports.cshtml @@ -0,0 +1,12 @@ +@* + For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 +*@ + +@inherits OrchardCore.DisplayManagement.Razor.RazorPage +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, OrchardCore.DisplayManagement +@addTagHelper *, OrchardCore.ResourceManagement + +@using OrchardCore.Commerce.Payment.Stripe.Constants +@using OrchardCore.Entities +@using OrchardCore.Workflows.Helpers diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/package.json b/src/Modules/OrchardCore.Commerce.Payment.Stripe/package.json new file mode 100644 index 000000000..1c1c4367e --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "scripts": { + "build": "npm explore nodejs-extensions -- pnpm build:styles", + "lint": "npm explore nodejs-extensions -- pnpm lint", + "clean": "npm explore nodejs-extensions -- pnpm clean:styles", + "watch": "npm explore nodejs-extensions -- pnpm watch:styles" + }, + "devDependencies": { + "eslint": "8.47.0", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-import": "2.28.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-only-warn": "1.1.0", + "eslint-plugin-promise": "6.1.1" + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/placement.json b/src/Modules/OrchardCore.Commerce.Payment.Stripe/placement.json new file mode 100644 index 000000000..db6907c90 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/placement.json @@ -0,0 +1,12 @@ +{ + "TextField": [ + { + "differentiator": "StripePaymentPart-PaymentMethodId", + "place": "-" + }, + { + "differentiator": "StripePaymentPart-PaymentIntentId", + "place": "-" + } + ] +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/pnpm-lock.yaml b/src/Modules/OrchardCore.Commerce.Payment.Stripe/pnpm-lock.yaml new file mode 100644 index 000000000..79cab4ede --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/pnpm-lock.yaml @@ -0,0 +1,1439 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + eslint: + specifier: 8.47.0 + version: 8.47.0 + eslint-config-airbnb-base: + specifier: 15.0.0 + version: 15.0.0(eslint-plugin-import@2.28.1)(eslint@8.47.0) + eslint-plugin-import: + specifier: 2.28.1 + version: 2.28.1(eslint@8.47.0) + eslint-plugin-node: + specifier: 11.1.0 + version: 11.1.0(eslint@8.47.0) + eslint-plugin-only-warn: + specifier: 1.1.0 + version: 1.1.0 + eslint-plugin-promise: + specifier: 6.1.1 + version: 6.1.1(eslint@8.47.0) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.47.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.47.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.9.0: + resolution: {integrity: sha512-zJmuCWj2VLBt4c25CfBIbMZLGLyhkvs7LznyVX5HfpzeocThgIj5XQK4L+g3U36mMcx8bPMhGyPpwCATamC4jQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.2: + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.22.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.47.0: + resolution: {integrity: sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.11: + resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /acorn-jsx@5.3.2(acorn@8.10.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: true + + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: true + + /array.prototype.findlastindex@1.2.2: + resolution: {integrity: sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + dev: true + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + dev: true + + /confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: true + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.28.1)(eslint@8.47.0): + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + dependencies: + confusing-browser-globals: 1.0.11 + eslint: 8.47.0 + eslint-plugin-import: 2.28.1(eslint@8.47.0) + object.assign: 4.1.4 + object.entries: 1.1.7 + semver: 6.3.1 + dev: true + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7 + is-core-module: 2.13.0 + resolve: 1.22.6 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.8.0(eslint-import-resolver-node@0.3.9)(eslint@8.47.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + debug: 3.2.7 + eslint: 8.47.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-es@3.0.1(eslint@8.47.0): + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.47.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + + /eslint-plugin-import@2.28.1(eslint@8.47.0): + resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.2 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.47.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(eslint-import-resolver-node@0.3.9)(eslint@8.47.0) + has: 1.0.3 + is-core-module: 2.13.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.6 + object.groupby: 1.0.0 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-node@11.1.0(eslint@8.47.0): + resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=5.16.0' + dependencies: + eslint: 8.47.0 + eslint-plugin-es: 3.0.1(eslint@8.47.0) + eslint-utils: 2.1.0 + ignore: 5.2.4 + minimatch: 3.1.2 + resolve: 1.22.6 + semver: 6.3.1 + dev: true + + /eslint-plugin-only-warn@1.1.0: + resolution: {integrity: sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==} + engines: {node: '>=6'} + dev: true + + /eslint-plugin-promise@6.1.1(eslint@8.47.0): + resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.47.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + + /eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.47.0: + resolution: {integrity: sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.47.0) + '@eslint-community/regexpp': 4.9.0 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.47.0 + '@humanwhocodes/config-array': 0.11.11 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.22.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.1.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.1.0: + resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} + engines: {node: '>=12.0.0'} + dependencies: + flatted: 3.2.9 + keyv: 4.5.3 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: true + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@13.22.0: + resolution: {integrity: sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + + /keyv@4.5.3: + resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /object.fromentries@2.0.6: + resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /object.groupby@1.0.0: + resolution: {integrity: sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + dev: true + + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: true + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: true + + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/src/Modules/OrchardCore.Commerce.Payment/.eslintrc.js b/src/Modules/OrchardCore.Commerce.Payment/.eslintrc.js new file mode 100644 index 000000000..d62e24ed6 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + // Setting root=true prevents ESLint from taking into account .eslintrc files higher up in the directory tree. + root: true, + + // The following path may have to be adjusted to your directory structure. + extends: './node_modules/nodejs-extensions/config/.eslintrc.lombiq-base.js', + + // Add custom rules and overrides here. + rules: { + }, +}; diff --git a/src/Modules/OrchardCore.Commerce.Payment/.stylelintrc.js b/src/Modules/OrchardCore.Commerce.Payment/.stylelintrc.js new file mode 100644 index 000000000..2ae551738 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/.stylelintrc.js @@ -0,0 +1,8 @@ +module.exports = { + // The following path may have to be adjusted to your directory structure. + extends: './node_modules/nodejs-extensions/config/.stylelintrc.lombiq-base.js', + + // Add custom rules and overrides here. + rules: { + }, +}; diff --git a/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IOrderContentTypeDefinitionExclusionProvider.cs b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IOrderContentTypeDefinitionExclusionProvider.cs new file mode 100644 index 000000000..e90a1c42b --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IOrderContentTypeDefinitionExclusionProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace OrchardCore.Commerce.Payment.Abstractions; + +/// +/// An extension point to specify which fields to exclude in OrderContentTypeDefinitionDisplayDriver. +/// +public interface IOrderContentTypeDefinitionExclusionProvider +{ + /// + /// Returns the list of field shape types to exclude when editing OrderContentTypeDefinitionDisplayDriver. + /// + IEnumerable GetExcludedShapes(IEnumerable<(string ShapeType, Uri Url, bool IsNew)> templateLinks); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentProvider.cs new file mode 100644 index 000000000..d30efeda3 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentProvider.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Mvc; +using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Constants; +using OrchardCore.Commerce.Controllers; +using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.ModelBinding; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Abstractions; + +/// +/// A payment provider that can be used during checkout to add a charge to the order. +/// +public interface IPaymentProvider +{ + /// + /// Gets the name used to identify additional payment provider data in the and as a + /// suffix to the Checkout{Name} shape used to display the payment UI during checkout or in the order view. + /// + string Name { get; } + + /// + /// Creates the additional data specific to the payment provider that's passed to the Checkout{Name} shape. + /// + /// + /// Arbitrary data which will be set as the value in . If it + /// returns then the shape won't be displayed. + /// + Task CreatePaymentProviderDataAsync(IPaymentViewModel model); + + /// + /// Validates the data POSTed to the action. + /// + Task ValidateAsync(IUpdateModelAccessor updateModelAccessor) => Task.CompletedTask; + + /// + /// Invoked at the end of . + /// + Task FinalModificationOfOrderAsync(ContentItem order, string? shoppingCartId) => Task.CompletedTask; + + /// + /// Invoked in if the order status is . + /// If the provider thinks it can be resolved, best approach is to return . + /// + Task UpdateAndRedirectToFinishedOrderAsync( + Controller controller, + ContentItem order, + string? shoppingCartId); +} + +public static class PaymentProviderExtensions +{ + public static IEnumerable WhereName(this IEnumerable providers, string name) => + providers.Where(provider => provider.Name.EqualsOrdinalIgnoreCase(name)); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs new file mode 100644 index 000000000..cc89ebcfe --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Mvc; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Constants; +using OrchardCore.Commerce.Abstractions.Exceptions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; +using OrchardCore.Commerce.Controllers; +using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.Payment.Constants; +using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.Mvc.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Abstractions; + +/// +/// Services related to payment and PaymentController. +/// +public interface IPaymentService +{ + /// + /// Creates and returns . + /// + Task CreateCheckoutViewModelAsync( + string? shoppingCartId, + Action? updateOrderPart = null); + + /// + /// When the order is payed this logic should be run to set properties that represents its state. + /// + Task FinalModificationOfOrderAsync(ContentItem order, string? shoppingCartId, string? paymentProviderName); + + /// + /// Creates an order content item without payment in the database based on the current content. + /// + /// + /// If , then the order totals will be checked. They must be zero, otherwise an error + /// notification will be sent and is returned. If , this is ignored. + /// + Task CreatePendingOrderFromShoppingCartAsync(string? shoppingCartId, bool mustBeFree); + + /// + /// Updates the 's status to . + /// + /// + /// A callback to set the charges of the order. If returns then nothing the won't be altered, otherwise it will be replaced with the returned value. + /// + Task UpdateOrderToOrderedAsync( + ContentItem order, + string? shoppingCartId, + Func?>? getCharges = null); + + /// + /// Tries to get the order identified by , or creates a new one if it's not there. Then + /// updates the order's line items and address using the data in the and the + /// shopping cart identified by the . Additional alterations may be done via + /// , and finally the order is created or updated. + /// + /// Thrown if the order validation failed. + Task<(ContentItem Order, bool IsNew)> CreateOrUpdateOrderFromShoppingCartAsync( + IUpdateModelAccessor updateModelAccessor, + string? orderId, + string? shoppingCartId, + AlterOrderAsyncDelegate? alterOrderAsync = null); +} + +public delegate Task AlterOrderAsyncDelegate( + ContentItem order, + bool isNew, + Amount total, + ShoppingCartViewModel? cartViewModel, + IList lineItems); + +public static class PaymentServiceExtensions +{ + public static async Task UpdateAndRedirectToFinishedOrderAsync( + this IPaymentService service, + Controller controller, + ContentItem order, + string? shoppingCartId, + string? paymentProviderName = null, + Func?>? getCharges = null) + { + await service.UpdateOrderToOrderedAsync(order, shoppingCartId, getCharges); + await service.FinalModificationOfOrderAsync(order, shoppingCartId, paymentProviderName); + + return controller.RedirectToAction( + nameof(PaymentController.Success), + typeof(PaymentController).ControllerName(), + new { area = FeatureIds.Area, orderId = order.ContentItemId, }); + } +} diff --git a/src/Modules/OrchardCore.Commerce/Assets/Styles/payment-form.scss b/src/Modules/OrchardCore.Commerce.Payment/Assets/Styles/payment-form.scss similarity index 87% rename from src/Modules/OrchardCore.Commerce/Assets/Styles/payment-form.scss rename to src/Modules/OrchardCore.Commerce.Payment/Assets/Styles/payment-form.scss index 58627af3e..f30998df5 100644 --- a/src/Modules/OrchardCore.Commerce/Assets/Styles/payment-form.scss +++ b/src/Modules/OrchardCore.Commerce.Payment/Assets/Styles/payment-form.scss @@ -1,5 +1,3 @@ -@import "general/error"; - .row.payment-intent > * { margin: 2rem 0; } diff --git a/src/Modules/OrchardCore.Commerce/Constants/CurrencyCollectionConstants.cs b/src/Modules/OrchardCore.Commerce.Payment/Constants/CurrencyCollectionConstants.cs similarity index 100% rename from src/Modules/OrchardCore.Commerce/Constants/CurrencyCollectionConstants.cs rename to src/Modules/OrchardCore.Commerce.Payment/Constants/CurrencyCollectionConstants.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment/Constants/FeatureIds.cs b/src/Modules/OrchardCore.Commerce.Payment/Constants/FeatureIds.cs new file mode 100644 index 000000000..7465b827d --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Constants/FeatureIds.cs @@ -0,0 +1,9 @@ +namespace OrchardCore.Commerce.Payment.Constants; + +public static class FeatureIds +{ + public const string Area = "OrchardCore.Commerce.Payment"; + + public const string Payment = Area; + public const string DummyProvider = $"{Area}.{nameof(DummyProvider)}"; +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/Constants/ResourceNames.cs b/src/Modules/OrchardCore.Commerce.Payment/Constants/ResourceNames.cs new file mode 100644 index 000000000..901bda14e --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Constants/ResourceNames.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Commerce.Payment.Constants; + +public static class ResourceNames +{ + public const string PaymentForm = nameof(PaymentForm); +} diff --git a/src/Modules/OrchardCore.Commerce/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs similarity index 57% rename from src/Modules/OrchardCore.Commerce/Controllers/PaymentController.cs rename to src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index b8b6a73bc..bce83a2aa 100644 --- a/src/Modules/OrchardCore.Commerce/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -1,79 +1,69 @@ using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Constants; +using OrchardCore.Commerce.Abstractions.Exceptions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.AddressDataType; -using OrchardCore.Commerce.Constants; -using OrchardCore.Commerce.Extensions; -using OrchardCore.Commerce.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.MoneyDataType.Abstractions; using OrchardCore.Commerce.MoneyDataType.Extensions; +using OrchardCore.Commerce.Payment; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.ViewModels; using OrchardCore.Commerce.ViewModels; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; -using OrchardCore.Entities; -using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Mvc.Utilities; -using OrchardCore.Settings; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using YesSql.Services; -using ISession = YesSql.ISession; namespace OrchardCore.Commerce.Controllers; public class PaymentController : Controller { - private const string FormValidationExceptionMessage = "An exception has occurred during checkout form validation."; - - private readonly ISiteService _siteService; - private readonly ISession _session; private readonly IAuthorizationService _authorizationService; - private readonly IStripePaymentService _stripePaymentService; private readonly ILogger _logger; private readonly IContentManager _contentManager; private readonly IUpdateModelAccessor _updateModelAccessor; private readonly IStringLocalizer T; private readonly IHtmlLocalizer H; - private readonly IPaymentIntentPersistence _paymentIntentPersistence; private readonly INotifier _notifier; private readonly IMoneyService _moneyService; + private readonly IEnumerable _paymentProviders; private readonly IPaymentService _paymentService; public PaymentController( - IStripePaymentService stripePaymentService, IOrchardServices services, IUpdateModelAccessor updateModelAccessor, - IPaymentIntentPersistence paymentIntentPersistence, INotifier notifier, IMoneyService moneyService, + IEnumerable paymentProviders, IPaymentService paymentService) { _authorizationService = services.AuthorizationService.Value; - _stripePaymentService = stripePaymentService; _logger = services.Logger.Value; _contentManager = services.ContentManager.Value; _updateModelAccessor = updateModelAccessor; - _session = services.Session.Value; T = services.StringLocalizer.Value; H = services.HtmlLocalizer.Value; - _paymentIntentPersistence = paymentIntentPersistence; _notifier = notifier; _moneyService = moneyService; + _paymentProviders = paymentProviders; _paymentService = paymentService; - _siteService = services.SiteService.Value; } [Route("checkout")] - public async Task Index(string shoppingCartId = null) + public async Task Index(string? shoppingCartId) { if (!await _authorizationService.AuthorizeAsync(User, Permissions.Checkout)) { @@ -82,9 +72,7 @@ public async Task Index(string shoppingCartId = null) if (await _paymentService.CreateCheckoutViewModelAsync(shoppingCartId) is not { } checkoutViewModel) { - return RedirectToAction( - nameof(ShoppingCartController.Empty), - typeof(ShoppingCartController).ControllerName()); + return Redirect("~/cart/empty"); } foreach (dynamic shape in checkoutViewModel.CheckoutShapes) shape.ViewModel = checkoutViewModel; @@ -97,7 +85,7 @@ public async Task Index(string shoppingCartId = null) [Route("checkout/price")] [HttpPost] [ValidateAntiForgeryToken] - public async Task Price(string shoppingCartId) => + public async Task Price(string? shoppingCartId) => await this.SafeJsonAsync(async () => { if (!await _authorizationService.AuthorizeAsync(User, Permissions.Checkout)) @@ -133,27 +121,33 @@ await this.SafeJsonAsync(async () => }; }); - [Route("checkout/validate")] + [Route("checkout/validate/{providerName}")] [HttpPost] [ValidateAntiForgeryToken] - public async Task Validate() + public async Task Validate(string providerName) { + if (string.IsNullOrEmpty(providerName)) return NotFound(); + try { - var paymentIntent = _paymentIntentPersistence.Retrieve(); - var paymentIntentInstance = await _stripePaymentService.GetPaymentIntentAsync(paymentIntent); - await _stripePaymentService.CreateOrUpdateOrderFromShoppingCartAsync(paymentIntentInstance, _updateModelAccessor); + await _paymentProviders + .WhereName(providerName) + .AwaitEachAsync(provider => provider.ValidateAsync(_updateModelAccessor)); var errors = _updateModelAccessor.ModelUpdater.GetModelErrorMessages().ToList(); return Json(new { Errors = errors }); } + catch (FrontendException exception) + { + return Json(new { Errors = new[] { exception.HtmlMessage.Html() } }); + } catch (Exception exception) { - _logger.LogError(exception, FormValidationExceptionMessage); + _logger.LogError(exception, "An exception has occurred during checkout form validation."); var errorMessage = HttpContext.IsDevelopmentAndLocalhost() ? exception.ToString() - : FormValidationExceptionMessage; + : T["An exception has occurred during checkout form validation."].Value; return Json(new { Errors = new[] { errorMessage } }); } @@ -162,15 +156,17 @@ public async Task Validate() [Route("checkout/paymentrequest/{orderId}")] public async Task PaymentRequest(string orderId) { - if (await _contentManager.GetAsync(orderId) is not { } order) return NotFound(); + if (await _contentManager.GetAsync(orderId) is not { } order || + order.As() is not { } orderPart) + { + return NotFound(); + } - if (string.IsNullOrEmpty(User.Identity.Name)) + if (string.IsNullOrEmpty(User.Identity?.Name)) { return LocalRedirect($"~/Login?ReturnUrl=~/Contents/ContentItems/{orderId}"); } - var orderPart = order.As(); - // If there are no line items, there is nothing to be done. if (!orderPart.LineItems.Any()) { @@ -192,24 +188,17 @@ public async Task PaymentRequest(string orderId) return this.RedirectToContentDisplay(orderId); } - var stripeApiSettings = (await _siteService.GetSiteSettingsAsync()).As(); - var paymentAmount = _stripePaymentService.GetPaymentAmount(singleCurrencyTotal); - var paymentIntent = await _stripePaymentService.CreatePaymentIntentAsync(paymentAmount, singleCurrencyTotal); + var viewModel = new PaymentViewModel(orderPart, singleCurrencyTotal, singleCurrencyTotal); + await viewModel.WithProviderDataAsync(_paymentProviders); - _session.Save(new OrderPayment + if (!viewModel.PaymentProviderData.Any()) { - OrderId = order.ContentItemId, - PaymentIntentId = paymentIntent.Id, - }); + await _notifier.WarningAsync(new HtmlString(" ").Join( + H["There are no applicable payment providers for this site."], + H["Please make sure there is at least one enabled and properly configured."])); + } - return View(new - { - SingleCurrencyTotal = singleCurrencyTotal, - NetTotal = singleCurrencyTotal, - StripePublishableKey = stripeApiSettings.PublishableKey, - PaymentIntentClientSecret = paymentIntent.ClientSecret, - OrderPart = orderPart, - }); + return View(viewModel); } [Route("success/{orderId}")] @@ -219,7 +208,7 @@ public async Task Success(string orderId) // Regular users should only see their own Orders, while users with the ManageOrders permission should be // able to see all Orders. - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageOrders) && order.Author != User.Identity.Name) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageOrders) && order.Author != User.Identity?.Name) { return NotFound(); } @@ -232,70 +221,46 @@ public async Task Success(string orderId) [AllowAnonymous] [HttpPost] [ValidateAntiForgeryToken] - public async Task CheckoutWithoutPayment() - { - if (await _paymentService.CreateNoPaymentOrderFromShoppingCartAsync() is not { } order) - { - return NotFound(); - } - - await _stripePaymentService.UpdateOrderToOrderedAsync(orderItem: order); - await _paymentService.FinalModificationOfOrderAsync(order); - - return RedirectToAction(nameof(Success), new { orderId = order.ContentItem.ContentItemId }); - } + [Route("checkout/free")] + public async Task CheckoutWithoutPayment(string? shoppingCartId) => + await _paymentService.CreatePendingOrderFromShoppingCartAsync(shoppingCartId, mustBeFree: true) is { } order + ? await _paymentService.UpdateAndRedirectToFinishedOrderAsync(this, order, shoppingCartId) + : NotFound(); [AllowAnonymous] - [HttpGet("checkout/middleware")] - public async Task PaymentConfirmationMiddleware([FromQuery(Name = "payment_intent")] string paymentIntent = null) + [HttpPost] + [ValidateAntiForgeryToken] + [Route("checkout/callback/{paymentProviderName}/{orderId?}")] + public async Task Callback(string paymentProviderName, string? orderId, string? shoppingCartId) { - // If it is null it means the session was not loaded yet and a redirect is needed. - if (string.IsNullOrEmpty(_paymentIntentPersistence.Retrieve())) - { - return View(); - } + if (string.IsNullOrWhiteSpace(paymentProviderName)) return NotFound(); - var fetchedPaymentIntent = await _stripePaymentService.GetPaymentIntentAsync(paymentIntent); - var orderId = (await _stripePaymentService.GetOrderPaymentByPaymentIntentIdAsync(paymentIntent))?.OrderId; + var order = string.IsNullOrEmpty(orderId) + ? await _paymentService.CreatePendingOrderFromShoppingCartAsync(shoppingCartId, mustBeFree: false) + : await _contentManager.GetAsync(orderId); + if (order is null) return NotFound(); - var order = await _contentManager.GetAsync(orderId); - var status = order?.As()?.Status?.Text; - var succeeded = fetchedPaymentIntent.Status == PaymentIntentStatuses.Succeeded; - var finished = succeeded && - order != null && - status == OrderStatuses.Ordered.HtmlClassify(); + var status = order.As()?.Status?.Text ?? OrderStatuses.Pending.HtmlClassify(); - if (succeeded && status == OrderStatuses.Pending.HtmlClassify()) + if (status == OrderStatuses.Ordered.HtmlClassify()) { - await _stripePaymentService.UpdateOrderToOrderedAsync(fetchedPaymentIntent); - finished = true; + return this.RedirectToContentDisplay(order); } - if (finished) + if (status == OrderStatuses.Pending.HtmlClassify()) { - await _paymentService.FinalModificationOfOrderAsync(order); - - return RedirectToAction(nameof(Success), new { orderId }); - } - - var errorMessage = H["The payment failed, please try again."]; - if (status == OrderStatuses.PaymentFailed.HtmlClassify()) - { - await _notifier.ErrorAsync(errorMessage); - return RedirectToAction(nameof(Index)); - } - - order.Alter(part => part.RetryCounter++); - await _contentManager.UpdateAsync(order); + foreach (var provider in _paymentProviders.WhereName(paymentProviderName)) + { + if (await provider.UpdateAndRedirectToFinishedOrderAsync(this, order, shoppingCartId) is { } result) + { + return result; + } + } - if (order.As().RetryCounter <= 10) - { - return View(); + return this.RedirectToContentDisplay(order); } - // Delete payment intent from session, to create a new one. - _paymentIntentPersistence.Store(string.Empty); - await _notifier.ErrorAsync(errorMessage); + await _notifier.ErrorAsync(H["The payment has failed, please try again."]); return RedirectToAction(nameof(Index)); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment/License.md b/src/Modules/OrchardCore.Commerce.Payment/License.md new file mode 100644 index 000000000..798b9cdec --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 OrchardCMS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Modules/OrchardCore.Commerce.Payment/Manifest.cs b/src/Modules/OrchardCore.Commerce.Payment/Manifest.cs new file mode 100644 index 000000000..e8218a895 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Manifest.cs @@ -0,0 +1,29 @@ +using OrchardCore.Modules.Manifest; +using static OrchardCore.Commerce.ContentFields.Constants.FeatureIds; +using static OrchardCore.Commerce.Payment.Constants.FeatureIds; +using static OrchardCore.Commerce.Tax.Constants.FeatureIds; + +[assembly: Module( + Name = "Orchard Core Commerce - Payment", + Author = "The Orchard Team", + Website = "https://github.com/OrchardCMS/OrchardCore.Commerce", + Version = "0.0.1", + Description = "Payment for Orchard Core Commerce.", + Category = "Commerce" +)] + +[assembly: Feature( + Id = Payment, + Name = "Orchard Core Commerce - Payment", + Category = "Commerce", + Description = "Payment for Orchard Core Commerce.", + Dependencies = new[] { ContentFields, Tax } +)] + +[assembly: Feature( + Id = DummyProvider, + Name = "Orchard Core Commerce - Payment - Dummy Provider", + Category = "Commerce", + Description = "Dummy payment provider used for development and testing.", + Dependencies = new[] { Payment } +)] diff --git a/src/Modules/OrchardCore.Commerce.Payment/Models/Payment.cs b/src/Modules/OrchardCore.Commerce.Payment/Models/Payment.cs new file mode 100644 index 000000000..64bb4b42c --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Models/Payment.cs @@ -0,0 +1,13 @@ +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.MoneyDataType; +using System; + +namespace OrchardCore.Commerce.Models; + +public record Payment( + string Kind, + string TransactionId, + string ChargeText, + Amount Amount, + DateTime CreatedUtc) + : IPayment; diff --git a/src/Modules/OrchardCore.Commerce.Payment/OrchardCore.Commerce.Payment.csproj b/src/Modules/OrchardCore.Commerce.Payment/OrchardCore.Commerce.Payment.csproj new file mode 100644 index 000000000..ec061d67d --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/OrchardCore.Commerce.Payment.csproj @@ -0,0 +1,60 @@ + + + + net6.0 + true + enable + + + + Orchard Core Commerce - Payment + Bertrand Le Roy + Copyright © 2018 .NET Foundation + Payment for Orchard Core Commerce + OrchardCore;OrchardCore.Commerce;Commerce;e-Commerce;Payment + https://github.com/OrchardCMS/OrchardCore.Commerce + https://github.com/OrchardCMS/OrchardCore.Commerce/blob/main/src/Modules/OrchardCore.Commerce.Payment/Readme.md + License.md + OrchardCoreIcon.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Modules/OrchardCore.Commerce.Payment/OrchardCoreIcon.png b/src/Modules/OrchardCore.Commerce.Payment/OrchardCoreIcon.png new file mode 100644 index 000000000..c72b029ac Binary files /dev/null and b/src/Modules/OrchardCore.Commerce.Payment/OrchardCoreIcon.png differ diff --git a/src/Modules/OrchardCore.Commerce.Payment/Permissions.cs b/src/Modules/OrchardCore.Commerce.Payment/Permissions.cs new file mode 100644 index 000000000..c419766c2 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Permissions.cs @@ -0,0 +1,19 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Users; +using OrchardCore.Security.Permissions; +using System.Collections.Generic; + +namespace OrchardCore.Commerce.Payment; + +public class Permissions : AdminPermissionBase +{ + public static readonly Permission ManageOrders = new(nameof(ManageOrders), "Manage Orders"); + public static readonly Permission Checkout = new(nameof(Checkout), "Ability to checkout"); + + private static readonly IReadOnlyList _adminPermissions = new[] + { + ManageOrders, + Checkout, + }; + + protected override IEnumerable AdminPermissions => _adminPermissions; +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/Readme.md b/src/Modules/OrchardCore.Commerce.Payment/Readme.md new file mode 100644 index 000000000..2c5948c1a --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Readme.md @@ -0,0 +1,7 @@ +# Orchard Core Commerce - Inventory + +## About + +Payment for Orchard Core Commerce. + +For general details about and on using Orchard Core Commerce see the [root Readme](../../../Readme.md). diff --git a/src/Modules/OrchardCore.Commerce.Payment/ResourceManagementOptionsConfiguration.cs b/src/Modules/OrchardCore.Commerce.Payment/ResourceManagementOptionsConfiguration.cs new file mode 100644 index 000000000..af24bd224 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/ResourceManagementOptionsConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Options; +using OrchardCore.ResourceManagement; +using static OrchardCore.Commerce.Payment.Constants.ResourceNames; + +namespace OrchardCore.Commerce.Payment; + +public class ResourceManagementOptionsConfiguration : IConfigureOptions +{ + private static readonly ResourceManifest _manifest = new(); + + static ResourceManagementOptionsConfiguration() => + _manifest + .DefineStyle(PaymentForm) + .SetUrl( + "~/OrchardCore.Commerce.Payment/css/payment-form.min.css", + "~/OrchardCore.Commerce.Payment/css/payment-form.css") + .SetVersion("1.0.0"); + + public void Configure(ResourceManagementOptions options) => options.ResourceManifests.Add(_manifest); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/Services/DummyPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment/Services/DummyPaymentProvider.cs new file mode 100644 index 000000000..bcd9c5c43 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Services/DummyPaymentProvider.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.ContentManagement; +using OrchardCore.Modules; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Services; + +public class DummyPaymentProvider : IPaymentProvider +{ + public const string ProviderName = "Dummy"; + + private readonly IClock _clock; + private readonly IHttpContextAccessor _hca; + private readonly Lazy _paymentServiceLazy; + private readonly IShoppingCartHelpers _shoppingCartHelpers; + + public string Name => ProviderName; + + public DummyPaymentProvider( + IClock clock, + IHttpContextAccessor hca, + Lazy paymentServiceLazy, + IShoppingCartHelpers shoppingCartHelpers) + { + _clock = clock; + _hca = hca; + _paymentServiceLazy = paymentServiceLazy; + _shoppingCartHelpers = shoppingCartHelpers; + } + + public Task CreatePaymentProviderDataAsync(IPaymentViewModel model) => + // This provider doesn't have any special data, and it should only be displayed during development even if the + // feature is enabled. So if the condition is met a blank object is returned, otherwise null which will cause + // the provider to be skipped when used through the viewModel.WithProviderDataAsync(providers) method. + Task.FromResult(_hca.HttpContext.IsDevelopmentAndLocalhost() ? new object() : null); + + public async Task UpdateAndRedirectToFinishedOrderAsync(Controller controller, ContentItem order, string? shoppingCartId) + { + var createdUtc = order.CreatedUtc ?? _clock.UtcNow; + var cart = await _shoppingCartHelpers.CreateShoppingCartViewModelAsync(shoppingCartId, order); + var totals = cart + .GetTotalsOrThrowIfEmpty() + .Select((total, index) => new Models.Payment( + Kind: "Dummy Payment", + TransactionId: $"{order.ContentItemId}:{index.ToTechnicalString()}", + ChargeText: $"Dummy transaction of {total.Currency.EnglishName}.", + total, + createdUtc)); + + return await _paymentServiceLazy + .Value + .UpdateAndRedirectToFinishedOrderAsync( + controller, + order, + shoppingCartId, + ProviderName, + _ => totals); + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs new file mode 100644 index 000000000..7f0f7c238 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs @@ -0,0 +1,283 @@ +using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Localization; +using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Constants; +using OrchardCore.Commerce.Abstractions.Exceptions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Extensions; +using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.MoneyDataType.Extensions; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Tax.Extensions; +using OrchardCore.Commerce.ViewModels; +using OrchardCore.ContentFields.Fields; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Display; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Mvc.Utilities; +using OrchardCore.Users; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; + +namespace OrchardCore.Commerce.Services; + +public class PaymentService : IPaymentService +{ + private readonly IFieldsOnlyDisplayManager _fieldsOnlyDisplayManager; + private readonly IContentManager _contentManager; + private readonly IShoppingCartHelpers _shoppingCartHelpers; + private readonly UserManager _userManager; + private readonly IRegionService _regionService; + private readonly IHttpContextAccessor _hca; + private readonly IUpdateModelAccessor _updateModelAccessor; + private readonly IContentItemDisplayManager _contentItemDisplayManager; + private readonly IEnumerable _orderEvents; + private readonly Lazy> _paymentProvidersLazy; + private readonly IEnumerable _checkoutEvents; + private readonly INotifier _notifier; + private readonly IHtmlLocalizer H; + + // We need all of them. +#pragma warning disable S107 // Methods should not have too many parameters + public PaymentService( + IFieldsOnlyDisplayManager fieldsOnlyDisplayManager, + IOrchardServices services, + IShoppingCartHelpers shoppingCartHelpers, + IRegionService regionService, + IUpdateModelAccessor updateModelAccessor, + IContentItemDisplayManager contentItemDisplayManager, + IEnumerable orderEvents, + Lazy> paymentProvidersLazy, + IEnumerable checkoutEvents, + INotifier notifier) +#pragma warning restore S107 // Methods should not have too many parameters + { + _fieldsOnlyDisplayManager = fieldsOnlyDisplayManager; + _contentManager = services.ContentManager.Value; + _shoppingCartHelpers = shoppingCartHelpers; + _userManager = services.UserManager.Value; + _regionService = regionService; + _updateModelAccessor = updateModelAccessor; + _contentItemDisplayManager = contentItemDisplayManager; + _orderEvents = orderEvents; + _paymentProvidersLazy = paymentProvidersLazy; + _checkoutEvents = checkoutEvents; + _notifier = notifier; + _hca = services.HttpContextAccessor.Value; + H = services.HtmlLocalizer.Value; + } + + public async Task CreateCheckoutViewModelAsync( + string? shoppingCartId, + Action? updateOrderPart = null) + { + var orderPart = new OrderPart(); + + await _checkoutEvents.AwaitEachAsync(checkoutEvents => + checkoutEvents.OrderCreatingAsync(orderPart, shoppingCartId)); + + var email = _hca.HttpContext?.User is { Identity.IsAuthenticated: true } user + ? await _userManager.GetEmailAsync(await _userManager.GetUserAsync(user)) + : string.Empty; + + orderPart.Email.Text = email; + orderPart.ShippingAddress.UserAddressToSave = nameof(orderPart.ShippingAddress); + orderPart.BillingAddress.UserAddressToSave = nameof(orderPart.BillingAddress); + + updateOrderPart?.Invoke(orderPart); + + var cart = await _shoppingCartHelpers.CreateShoppingCartViewModelAsync(shoppingCartId, orderPart); + if (cart?.Totals.Single() is not { } total) return null; + + var checkoutShapes = (await _fieldsOnlyDisplayManager.DisplayFieldsAsync( + await _contentManager.NewAsync(Order), + "Checkout")) + .ToList(); + + var currency = total.Currency; + var netTotal = new Amount(0, currency); + var grossTotal = new Amount(0, currency); + + var lines = cart.Lines; + foreach (var line in lines) + { + var additionalData = line.AdditionalData; + var grossAmount = additionalData.GetGrossPrice(); + if (grossAmount.Value > 0) + { + grossTotal += grossAmount * line.Quantity; + } + + var netAmount = additionalData.GetNetPrice(); + var netPrice = netAmount.Value > 0 ? netAmount : line.UnitPrice; + netTotal += netPrice * line.Quantity; + } + + var viewModel = new CheckoutViewModel(orderPart, total, netTotal) + { + ShoppingCartId = shoppingCartId, + Regions = (await _regionService.GetAvailableRegionsAsync()).CreateSelectListOptions(), + GrossTotal = grossTotal, + UserEmail = email, + CheckoutShapes = checkoutShapes, + }; + + if (viewModel.SingleCurrencyTotal.Value > 0) + { + await viewModel.WithProviderDataAsync(_paymentProvidersLazy.Value); + + if (!viewModel.PaymentProviderData.Any()) + { + await _notifier.WarningAsync(new HtmlString(" ").Join( + H["There are no applicable payment providers for this site."], + H["Please make sure there is at least one enabled and properly configured."])); + } + } + + return viewModel; + } + + public async Task FinalModificationOfOrderAsync(ContentItem order, string? shoppingCartId, string? paymentProviderName) + { + await _orderEvents.AwaitEachAsync(orderEvent => + orderEvent.FinalizeAsync(order, shoppingCartId, paymentProviderName)); + + await _shoppingCartHelpers.UpdateAsync(shoppingCartId, cart => + { + cart.Items?.Clear(); + return Task.CompletedTask; + }); + + if (!string.IsNullOrEmpty(paymentProviderName)) + { + await _paymentProvidersLazy + .Value + .WhereName(paymentProviderName) + .AwaitEachAsync(provider => provider.FinalModificationOfOrderAsync(order, shoppingCartId)); + } + } + + public async Task CreatePendingOrderFromShoppingCartAsync(string? shoppingCartId, bool mustBeFree) + { + var cart = await _shoppingCartHelpers.RetrieveAsync(shoppingCartId); + var order = await _contentManager.NewAsync(Order); + + var errors = await UpdateOrderWithDriversAsync(order); + if (errors.Any()) + { + foreach (var error in errors) + { + await _notifier.ErrorAsync(new LocalizedHtmlString(error, error)); + } + + return null; + } + + var lineItems = await _shoppingCartHelpers.CreateOrderLineItemsAsync(cart); + + var cartViewModel = await _shoppingCartHelpers.CreateShoppingCartViewModelAsync(shoppingCartId, order); + + if (mustBeFree && cartViewModel.Totals.Any(total => total.Value > 0)) + { + await _notifier.ErrorAsync(H["Invalid attempt to check out non-free order as free."]); + return null; + } + + await order.AlterAsync(async orderPart => + { + orderPart.LineItems.SetItems(lineItems); + orderPart.Status.Text = OrderStatuses.Pending.HtmlClassify(); + + await _orderEvents.AwaitEachAsync(orderEvents => + orderEvents.CreatedFreeAsync(orderPart, cart, cartViewModel)); + }); + + await _contentManager.CreateAsync(order); + + return order; + } + + private async Task> UpdateOrderWithDriversAsync(ContentItem order) + { + await _contentItemDisplayManager.UpdateEditorAsync(order, _updateModelAccessor.ModelUpdater, isNew: false); + return _updateModelAccessor.ModelUpdater.GetModelErrorMessages()?.AsList() ?? Array.Empty(); + } + + public async Task UpdateOrderToOrderedAsync( + ContentItem order, + string? shoppingCartId, + Func?>? getCharges = null) + { + ArgumentNullException.ThrowIfNull(order); + + order.Alter(orderPart => + { + if (getCharges?.Invoke(orderPart)?.AsList() is { } newCharges) + { + orderPart.Charges.SetItems(newCharges); + } + + orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.Ordered.HtmlClassify() }; + }); + + await _orderEvents.AwaitEachAsync(orderEvent => orderEvent.OrderedAsync(order, shoppingCartId)); + await _contentManager.UpdateAsync(order); + } + + public async Task<(ContentItem Order, bool IsNew)> CreateOrUpdateOrderFromShoppingCartAsync( + IUpdateModelAccessor updateModelAccessor, + string? orderId, + string? shoppingCartId, + AlterOrderAsyncDelegate? alterOrderAsync = null) + { + var order = await _contentManager.GetAsync(orderId) ?? await _contentManager.NewAsync(Order); + var isNew = order.IsNew(); + var part = order.As(); + + var cart = await _shoppingCartHelpers.RetrieveAsync(shoppingCartId); + if (cart.Items.Any() && !order.As().LineItems.Any()) + { + await _contentItemDisplayManager.UpdateEditorAsync(order, updateModelAccessor.ModelUpdater, isNew: false); + + var errors = updateModelAccessor.ModelUpdater.GetModelErrorMessages().AsList(); + if (errors.Any()) + { + throw new FrontendException(new HtmlString("
").Join( + errors.Select(error => H["{0}", error]).ToArray())); + } + } + + // If there are line items in the Order, use data from Order instead of shopping cart. + var lineItems = part.LineItems.Any() + ? part.LineItems + : await _shoppingCartHelpers.CreateOrderLineItemsAsync(cart); + + var cartViewModel = await _shoppingCartHelpers.CreateShoppingCartViewModelAsync(shoppingCartId, part); + + // If there is no cart, use current Order's data. + var total = cartViewModel is null + ? part.LineItems.Select(item => item.LinePrice).Sum() + : cartViewModel.GetTotalsOrThrowIfEmpty().SingleOrDefault(); + + if (alterOrderAsync is not null) await alterOrderAsync(order, isNew, total, cartViewModel, lineItems); + + if (isNew) + { + await _contentManager.CreateAsync(order); + } + else + { + await _contentManager.UpdateAsync(order); + } + + return (order, isNew); + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/Startup.cs b/src/Modules/OrchardCore.Commerce.Payment/Startup.cs new file mode 100644 index 000000000..81fc7e961 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Startup.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Constants; +using OrchardCore.Commerce.Services; +using OrchardCore.Modules; +using OrchardCore.ResourceManagement; + +namespace OrchardCore.Commerce.Payment; + +public class Startup : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) + { + services.AddTransient, ResourceManagementOptionsConfiguration>(); + services.AddScoped(); + } +} + +[Feature(FeatureIds.DummyProvider)] +public class DummyProviderStartup : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) => + services.AddScoped(); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/ViewModels/CheckoutViewModel.cs b/src/Modules/OrchardCore.Commerce.Payment/ViewModels/CheckoutViewModel.cs new file mode 100644 index 000000000..5e0e4a60f --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/ViewModels/CheckoutViewModel.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.Payment.ViewModels; +using OrchardCore.DisplayManagement; +using System; +using System.Collections.Generic; + +namespace OrchardCore.Commerce.ViewModels; + +public class CheckoutViewModel : PaymentViewModel, ICheckoutViewModel +{ + public string? ShoppingCartId { get; init; } + + public Amount GrossTotal { get; init; } + + [BindNever] + public IEnumerable Regions { get; set; } = Array.Empty(); + + [BindNever] + public IDictionary> Provinces { get; } = + new Dictionary>(); + + public string? UserEmail { get; init; } + + public IEnumerable CheckoutShapes { get; init; } = Array.Empty(); + + public CheckoutViewModel(OrderPart orderPart, Amount singleCurrencyTotal, Amount netTotal) + : base(orderPart, singleCurrencyTotal, netTotal) => + Metadata.Type = "Checkout"; +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/ViewModels/PaymentViewModel.cs b/src/Modules/OrchardCore.Commerce.Payment/ViewModels/PaymentViewModel.cs new file mode 100644 index 000000000..f7731f7ef --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/ViewModels/PaymentViewModel.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.DisplayManagement.Views; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.ViewModels; + +public class PaymentViewModel : ShapeViewModel, IPaymentViewModel +{ + public Amount SingleCurrencyTotal { get; } + + public Amount NetTotal { get; } + + public OrderPart OrderPart { get; } + + [BindNever] + public IDictionary PaymentProviderData { get; } = new Dictionary(); + + public PaymentViewModel(OrderPart orderPart, Amount singleCurrencyTotal, Amount netTotal) + { + OrderPart = orderPart; + SingleCurrencyTotal = singleCurrencyTotal; + NetTotal = netTotal; + } + + public async Task WithProviderDataAsync(IEnumerable paymentProviders) + { + foreach (var provider in paymentProviders) + { + if (await provider.CreatePaymentProviderDataAsync(this) is { } data) + { + PaymentProviderData[provider.Name] = data; + } + } + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/Views/CheckoutDummy.cshtml b/src/Modules/OrchardCore.Commerce.Payment/Views/CheckoutDummy.cshtml new file mode 100644 index 000000000..b1243708c --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Views/CheckoutDummy.cshtml @@ -0,0 +1,13 @@ + + + diff --git a/src/Modules/OrchardCore.Commerce.Payment/Views/PayButton.cshtml b/src/Modules/OrchardCore.Commerce.Payment/Views/PayButton.cshtml new file mode 100644 index 000000000..e1f072edf --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Views/PayButton.cshtml @@ -0,0 +1,47 @@ +@model OrchardCore.DisplayManagement.IShape + +@using Microsoft.AspNetCore.Html +@using OrchardCore.Commerce.MoneyDataType +@using OrchardCore.DisplayManagement + +@{ + var className = Model.GetProperty("PayButtonClass")?.ToString() ?? + throw new InvalidOperationException("Missing \"PayButtonClass\" property."); + + IHtmlContent buttonLabel = Model.GetProperty("Title") is { } title ? T["Pay with {0}", title] : T["Pay"]; + + if (Model.GetProperty("Text") is { } textHtml) + { + buttonLabel = textHtml; + } + else if (Model.GetProperty("Text")?.ToString() is { } textString) + { + buttonLabel = T["{0}", textString]; + } + else if (Model.GetProperty("Total") is Amount total && + total.Currency.CurrencyIsoCode != Currency.UnspecifiedCurrency.CurrencyIsoCode) + { + // We use HtmlString to exclude this from the localization string. This way translators don't have to worry + // about ruining functional HTML by accident. + var totalHtml = new HtmlString( + "" + + $"{HtmlEncoder.Encode(total.ToString())}"); + + buttonLabel = T["Pay {0}", totalHtml]; + } + +} + +
+ +
diff --git a/src/Modules/OrchardCore.Commerce/Views/Payment/Index.cshtml b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Index.cshtml similarity index 64% rename from src/Modules/OrchardCore.Commerce/Views/Payment/Index.cshtml rename to src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Index.cshtml index 425408537..8adf4670d 100644 --- a/src/Modules/OrchardCore.Commerce/Views/Payment/Index.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Index.cshtml @@ -1,5 +1,3 @@ -@model OrchardCore.Commerce.ViewModels.CheckoutViewModel - diff --git a/src/Modules/OrchardCore.Commerce/Views/Payment/PaymentRequest.cshtml b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/PaymentRequest.cshtml similarity index 73% rename from src/Modules/OrchardCore.Commerce/Views/Payment/PaymentRequest.cshtml rename to src/Modules/OrchardCore.Commerce.Payment/Views/Payment/PaymentRequest.cshtml index e2b00d951..a2dae4da1 100644 --- a/src/Modules/OrchardCore.Commerce/Views/Payment/PaymentRequest.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/PaymentRequest.cshtml @@ -1,7 +1,9 @@ -@using OrchardCore.Commerce.Models; +@model OrchardCore.Commerce.Payment.ViewModels.PaymentViewModel + +@using OrchardCore.Mvc.Utilities @{ - var orderPart = (OrderPart)Model.OrderPart; + var orderPart = Model.OrderPart; var shippingAddress = orderPart.ShippingAddress.Address; var billingAddress = orderPart.BillingAddress.Address; } @@ -13,11 +15,18 @@
@Html.AntiForgeryToken() - +
+ @foreach (var (providerName, data) in Model.PaymentProviderData) + { + var shapeName = "Checkout" + providerName; +
+ +
+ } +
diff --git a/src/Modules/OrchardCore.Commerce/Views/Payment/Success.cshtml b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Success.cshtml similarity index 100% rename from src/Modules/OrchardCore.Commerce/Views/Payment/Success.cshtml rename to src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Success.cshtml diff --git a/src/Modules/OrchardCore.Commerce.Payment/Views/_ViewImports.cshtml b/src/Modules/OrchardCore.Commerce.Payment/Views/_ViewImports.cshtml new file mode 100644 index 000000000..0abb97613 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Views/_ViewImports.cshtml @@ -0,0 +1,14 @@ +@* + For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 +*@ + +@inherits OrchardCore.DisplayManagement.Razor.RazorPage +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, OrchardCore.Commerce.Abstractions +@addTagHelper *, OrchardCore.DisplayManagement +@addTagHelper *, OrchardCore.ResourceManagement + +@using OrchardCore.Commerce.Payment.Constants +@using OrchardCore.Commerce.ViewModels +@using OrchardCore.Entities +@using OrchardCore.Workflows.Helpers diff --git a/src/Modules/OrchardCore.Commerce.Payment/package.json b/src/Modules/OrchardCore.Commerce.Payment/package.json new file mode 100644 index 000000000..1c1c4367e --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "scripts": { + "build": "npm explore nodejs-extensions -- pnpm build:styles", + "lint": "npm explore nodejs-extensions -- pnpm lint", + "clean": "npm explore nodejs-extensions -- pnpm clean:styles", + "watch": "npm explore nodejs-extensions -- pnpm watch:styles" + }, + "devDependencies": { + "eslint": "8.47.0", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-import": "2.28.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-only-warn": "1.1.0", + "eslint-plugin-promise": "6.1.1" + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/pnpm-lock.yaml b/src/Modules/OrchardCore.Commerce.Payment/pnpm-lock.yaml new file mode 100644 index 000000000..79cab4ede --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/pnpm-lock.yaml @@ -0,0 +1,1439 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + eslint: + specifier: 8.47.0 + version: 8.47.0 + eslint-config-airbnb-base: + specifier: 15.0.0 + version: 15.0.0(eslint-plugin-import@2.28.1)(eslint@8.47.0) + eslint-plugin-import: + specifier: 2.28.1 + version: 2.28.1(eslint@8.47.0) + eslint-plugin-node: + specifier: 11.1.0 + version: 11.1.0(eslint@8.47.0) + eslint-plugin-only-warn: + specifier: 1.1.0 + version: 1.1.0 + eslint-plugin-promise: + specifier: 6.1.1 + version: 6.1.1(eslint@8.47.0) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.47.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.47.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.9.0: + resolution: {integrity: sha512-zJmuCWj2VLBt4c25CfBIbMZLGLyhkvs7LznyVX5HfpzeocThgIj5XQK4L+g3U36mMcx8bPMhGyPpwCATamC4jQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.2: + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.22.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.47.0: + resolution: {integrity: sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.11: + resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /acorn-jsx@5.3.2(acorn@8.10.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: true + + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: true + + /array.prototype.findlastindex@1.2.2: + resolution: {integrity: sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + dev: true + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + dev: true + + /confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: true + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.28.1)(eslint@8.47.0): + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + dependencies: + confusing-browser-globals: 1.0.11 + eslint: 8.47.0 + eslint-plugin-import: 2.28.1(eslint@8.47.0) + object.assign: 4.1.4 + object.entries: 1.1.7 + semver: 6.3.1 + dev: true + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7 + is-core-module: 2.13.0 + resolve: 1.22.6 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.8.0(eslint-import-resolver-node@0.3.9)(eslint@8.47.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + debug: 3.2.7 + eslint: 8.47.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-es@3.0.1(eslint@8.47.0): + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.47.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + + /eslint-plugin-import@2.28.1(eslint@8.47.0): + resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.2 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.47.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(eslint-import-resolver-node@0.3.9)(eslint@8.47.0) + has: 1.0.3 + is-core-module: 2.13.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.6 + object.groupby: 1.0.0 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-node@11.1.0(eslint@8.47.0): + resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=5.16.0' + dependencies: + eslint: 8.47.0 + eslint-plugin-es: 3.0.1(eslint@8.47.0) + eslint-utils: 2.1.0 + ignore: 5.2.4 + minimatch: 3.1.2 + resolve: 1.22.6 + semver: 6.3.1 + dev: true + + /eslint-plugin-only-warn@1.1.0: + resolution: {integrity: sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==} + engines: {node: '>=6'} + dev: true + + /eslint-plugin-promise@6.1.1(eslint@8.47.0): + resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.47.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + + /eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.47.0: + resolution: {integrity: sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.47.0) + '@eslint-community/regexpp': 4.9.0 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.47.0 + '@humanwhocodes/config-array': 0.11.11 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.22.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.1.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.1.0: + resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} + engines: {node: '>=12.0.0'} + dependencies: + flatted: 3.2.9 + keyv: 4.5.3 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: true + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@13.22.0: + resolution: {integrity: sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + + /keyv@4.5.3: + resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /object.fromentries@2.0.6: + resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /object.groupby@1.0.0: + resolution: {integrity: sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + dev: true + + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: true + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: true + + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IOrderLineItemService.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IOrderLineItemService.cs index 37af67db9..5d307dedd 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IOrderLineItemService.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IOrderLineItemService.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.ViewModels; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IPaymentProvider.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IPaymentProvider.cs deleted file mode 100644 index b52346e0a..000000000 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IPaymentProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using OrchardCore.Commerce.MoneyDataType; -using System.Collections.Generic; - -namespace OrchardCore.Commerce.Abstractions; - -/// -/// When implemented, this service creates and populates . -/// -public interface IPaymentProvider -{ - /// - /// Creates a new payment, if the payment is of the expected . - /// - /// This value indicates towards the provider if it's applicable. - /// A unique ID. - /// The monetary amount to charge through the payment method. - /// A collection of additional data specific to the payment provider. - /// A new instance of or if not applicable. - public IPayment CreateCharge(string kind, string transactionId, Amount amount, IDictionary data); - - /// - /// Converts the information from into so it can be used to create - /// further charges. - /// - public void AddData(IPayment charge, IDictionary data); -} diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IPaymentService.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IPaymentService.cs deleted file mode 100644 index f16e8249b..000000000 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IPaymentService.cs +++ /dev/null @@ -1,31 +0,0 @@ -using OrchardCore.Commerce.Controllers; -using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.ViewModels; -using OrchardCore.ContentManagement; -using System; -using System.Threading.Tasks; - -namespace OrchardCore.Commerce.Abstractions; - -/// -/// Services related to payment and . -/// -public interface IPaymentService -{ - /// - /// Creates and returns . - /// - Task CreateCheckoutViewModelAsync( - string shoppingCartId, - Action updateOrderPart = null); - - /// - /// When the order is payed this logic should be run to set properties that represents its state. - /// - Task FinalModificationOfOrderAsync(ContentItem order); - - /// - /// Creates an order content item without payment in the database based on the current content. - /// - Task CreateNoPaymentOrderFromShoppingCartAsync(); -} diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IPriceProvider.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IPriceProvider.cs index 65a30cf1d..c58fb0467 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IPriceProvider.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IPriceProvider.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using System.Collections.Generic; namespace OrchardCore.Commerce.Abstractions; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IPriceSelectionStrategy.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IPriceSelectionStrategy.cs index 7042e86c2..7d92339c4 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IPriceSelectionStrategy.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IPriceSelectionStrategy.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IPriceService.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IPriceService.cs index ed04b708c..761b0ba4c 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IPriceService.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IPriceService.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IProductAttributeProvider.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IProductAttributeProvider.cs index 776df45f7..47c023147 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IProductAttributeProvider.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IProductAttributeProvider.cs @@ -1,3 +1,4 @@ +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement.Metadata.Models; using System; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IProductEstimationContextUpdater.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IProductEstimationContextUpdater.cs index eca96b80d..9dd9e0476 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IProductEstimationContextUpdater.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IProductEstimationContextUpdater.cs @@ -1,4 +1,5 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Models; namespace OrchardCore.Commerce.Abstractions; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IProductInventoryProvider.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IProductInventoryProvider.cs index cd717c07c..caf2f607b 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IProductInventoryProvider.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IProductInventoryProvider.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IProductInventoryService.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IProductInventoryService.cs index 678dc320c..7ac7a0c5b 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IProductInventoryService.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IProductInventoryService.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IProductService.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IProductService.cs index 182990d49..29204b48a 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IProductService.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IProductService.cs @@ -1,3 +1,4 @@ +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Models; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartEvents.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartEvents.cs index f84e8274f..35f482fca 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartEvents.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartEvents.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Mvc.Localization; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.Controllers; using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.ViewModels; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartHelpers.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartHelpers.cs deleted file mode 100644 index 2408dffa0..000000000 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartHelpers.cs +++ /dev/null @@ -1,58 +0,0 @@ -using OrchardCore.Commerce.AddressDataType; -using OrchardCore.Commerce.Exceptions; -using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.MoneyDataType; -using OrchardCore.Commerce.ViewModels; -using OrchardCore.DisplayManagement; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace OrchardCore.Commerce.Abstractions; - -/// -/// A service to work with shopping cart data. -/// -public interface IShoppingCartHelpers -{ - /// - /// Creates a model from the current shopping cart. This includes everything except the - /// collection in . - /// - Task CreateShoppingCartViewModelAsync( - string shoppingCartId, - Address shipping = null, - Address billing = null); - - /// - /// Calculate the total value in the cart. All prices must be of a single currency. - /// - /// The total value of the items in the cart, or if the cart is empty. - Task CalculateSingleCurrencyTotalAsync(); - - /// - /// Groups the line items in the cart by currency and returns the value by currency code. - /// - Task> CalculateMultipleCurrencyTotalsAsync(); - - /// - /// Adds a new entry to the shopping cart, optionally saves the cart using if - /// is . - /// - /// - /// Thrown if the cart validation fails. Its can be displayed safely. - /// - Task AddToCartAsync( - string shoppingCartId, - ShoppingCartItem item, - bool storeIfOk = false); - - /// - /// Adds the product with the given to the shopping cart without saving, validates the cart - /// and calculates the display information for the added item. - /// - Task EstimateProductAsync( - string shoppingCartId, - string sku, - Address shipping = null, - Address billing = null); -} diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartPersistence.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartPersistence.cs index e1a563af2..9a858fa30 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartPersistence.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartPersistence.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Services; using System.Threading.Tasks; @@ -16,10 +16,22 @@ public interface IShoppingCartPersistence /// /// Returns a identified by . /// - Task RetrieveAsync(string shoppingCartId = null); + /// + /// The name used to identify the shopping cart. refers to the default shopping cart. + /// + Task RetrieveAsync(string shoppingCartId); /// /// Saves a shopping cart by a given ID. /// - Task StoreAsync(ShoppingCart items, string shoppingCartId = null); + Task StoreAsync(ShoppingCart items); +} + +public static class ShoppingCartPersistenceExtensions +{ + public static Task StoreAsync(this IShoppingCartPersistence service, ShoppingCart items, string shoppingCartId) + { + items.Id = shoppingCartId ?? items.Id; + return service.StoreAsync(items); + } } diff --git a/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartSerializer.cs b/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartSerializer.cs index cab5a4c77..dd300d035 100644 --- a/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartSerializer.cs +++ b/src/Modules/OrchardCore.Commerce/Abstractions/IShoppingCartSerializer.cs @@ -1,5 +1,7 @@ +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.ProductAttributeValues; using OrchardCore.Commerce.ViewModels; using OrchardCore.ContentManagement.Metadata.Models; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/AdminMenu.cs b/src/Modules/OrchardCore.Commerce/AdminMenu.cs index 5abee20d2..26691a4b1 100644 --- a/src/Modules/OrchardCore.Commerce/AdminMenu.cs +++ b/src/Modules/OrchardCore.Commerce/AdminMenu.cs @@ -4,14 +4,11 @@ using OrchardCore.Commerce.Drivers; using OrchardCore.Commerce.Settings; using OrchardCore.Navigation; -using static OrchardCore.Commerce.Constants.NavigationConstants; namespace OrchardCore.Commerce; -public class AdminMenu : NavigationProviderBase +public class AdminMenu : AdminMenuNavigationProviderBase { - protected override string NavigationName => "admin"; - public AdminMenu(IHttpContextAccessor hca, IStringLocalizer stringLocalizer) : base(hca, stringLocalizer) { } @@ -21,35 +18,15 @@ protected override void Build(NavigationBuilder builder) => .Add(T["Configuration"], configuration => configuration .Add(T["Commerce"], commerce => commerce .Add(T["Currency"], T["Currency"], entry => entry - .Action(ActionNames.Index, ControllerNames.Admin, new - { - area = $"{nameof(OrchardCore)}.{nameof(OrchardCore.Settings)}", - groupId = CurrencySettingsDisplayDriver.GroupId, - }) + .SiteSettings(CurrencySettingsDisplayDriver.GroupId) .Permission(Permissions.ManageCurrencySettings) .LocalNav()) .Add(T["Price Display"], T["Price Display"], entry => entry - .Action(ActionNames.Index, ControllerNames.Admin, new - { - area = $"{nameof(OrchardCore)}.{nameof(OrchardCore.Settings)}", - groupId = PriceDisplaySettingsDisplayDriver.GroupId, - }) + .SiteSettings(PriceDisplaySettingsDisplayDriver.GroupId) .Permission(Permissions.ManagePriceDisplaySettings) .LocalNav()) - .Add(T["Stripe API"], T["Stripe API"], stripeApi => stripeApi - .Action(ActionNames.Index, ControllerNames.Admin, new - { - area = $"{nameof(OrchardCore)}.{nameof(OrchardCore.Settings)}", - groupId = StripeApiSettingsDisplayDriver.GroupId, - }) - .Permission(Permissions.ManageStripeApiSettings) - .LocalNav()) .Add(T["Region"], T["Region"], region => region - .Action(ActionNames.Index, ControllerNames.Admin, new - { - area = $"{nameof(OrchardCore)}.{nameof(OrchardCore.Settings)}", - groupId = RegionSettingsDisplayDriver.GroupId, - }) + .SiteSettings(RegionSettingsDisplayDriver.GroupId) .Permission(Permissions.ManageRegionSettings) .LocalNav()))); } diff --git a/src/Modules/OrchardCore.Commerce/Constants/NavigationConstants.cs b/src/Modules/OrchardCore.Commerce/Constants/NavigationConstants.cs deleted file mode 100644 index 4785e3c43..000000000 --- a/src/Modules/OrchardCore.Commerce/Constants/NavigationConstants.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace OrchardCore.Commerce.Constants; - -public static class NavigationConstants -{ - public static class ActionNames - { - public const string Index = nameof(Index); - } - - public static class ControllerNames - { - public const string Admin = nameof(Admin); - } -} diff --git a/src/Modules/OrchardCore.Commerce/Constants/ResourceNames.cs b/src/Modules/OrchardCore.Commerce/Constants/ResourceNames.cs index 6e419a13e..a8f456b7c 100644 --- a/src/Modules/OrchardCore.Commerce/Constants/ResourceNames.cs +++ b/src/Modules/OrchardCore.Commerce/Constants/ResourceNames.cs @@ -2,12 +2,7 @@ namespace OrchardCore.Commerce.Constants; public static class ResourceNames { - public const string JQuery = "jQuery"; - - public const string PaymentForm = nameof(PaymentForm); public const string ShoppingCart = nameof(ShoppingCart); public const string ShoppingCartWidget = nameof(ShoppingCartWidget); - public const string CommerceRegions = nameof(CommerceRegions); - public const string StripePaymentForm = nameof(StripePaymentForm); public const string ToggleSecondAddress = nameof(ToggleSecondAddress); } diff --git a/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs b/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs index 2779b6734..2d60dbc91 100644 --- a/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs +++ b/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs @@ -3,8 +3,10 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Exceptions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Activities; -using OrchardCore.Commerce.Exceptions; using OrchardCore.Commerce.Inventory.Models; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.ViewModels; @@ -99,7 +101,7 @@ public async Task Index(string shoppingCartId = null) } [HttpGet] - [Route("cart-empty")] + [Route("cart/empty")] public async Task Empty() { var trackingConsentFeature = HttpContext.Features.Get(); diff --git a/src/Modules/OrchardCore.Commerce/Controllers/UserController.cs b/src/Modules/OrchardCore.Commerce/Controllers/UserController.cs index ec46c3087..503ff2409 100644 --- a/src/Modules/OrchardCore.Commerce/Controllers/UserController.cs +++ b/src/Modules/OrchardCore.Commerce/Controllers/UserController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; -using OrchardCore.Commerce.Extensions; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Display; using OrchardCore.DisplayManagement.ModelBinding; @@ -13,7 +12,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using YesSql; -using static OrchardCore.Commerce.Constants.ContentTypes; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; namespace OrchardCore.Commerce.Controllers; diff --git a/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs index 796b539e6..da5a412ad 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs @@ -1,6 +1,6 @@ using Lombiq.HelpfulLibraries.OrchardCore.Contents; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Exceptions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Exceptions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Promotion.Extensions; diff --git a/src/Modules/OrchardCore.Commerce/Drivers/OrderContentTypeDefinitionDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/OrderContentTypeDefinitionDisplayDriver.cs index a433de041..04e8e2d0b 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/OrderContentTypeDefinitionDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/OrderContentTypeDefinitionDisplayDriver.cs @@ -1,3 +1,4 @@ +using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Services; using OrchardCore.Commerce.ViewModels; using OrchardCore.ContentManagement; @@ -6,7 +7,7 @@ using OrchardCore.DisplayManagement.Views; using System.Collections.Generic; using System.Linq; -using static OrchardCore.Commerce.Constants.ContentTypes; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; namespace OrchardCore.Commerce.Drivers; @@ -15,8 +16,6 @@ public class OrderContentTypeDefinitionDisplayDriver : ContentTypeDefinitionDisp // The built-in fields are rendered from the Checkout shape. private static readonly string[] _excludedShapes = { - "Order_Checkout__StripePaymentPart__PaymentIntentId", - "Order_Checkout__StripePaymentPart__PaymentMethodId", "Order_Checkout__OrderPart__OrderId", "Order_Checkout__OrderPart__Status", "Order_Checkout__OrderPart__Email", @@ -28,13 +27,16 @@ public class OrderContentTypeDefinitionDisplayDriver : ContentTypeDefinitionDisp private readonly IContentManager _contentManager; private readonly IFieldsOnlyDisplayManager _fieldsOnlyDisplayManager; + private readonly IEnumerable _orderContentTypeDefinitionExclusionProviders; public OrderContentTypeDefinitionDisplayDriver( IContentManager contentManager, - IFieldsOnlyDisplayManager fieldsOnlyDisplayManager) + IFieldsOnlyDisplayManager fieldsOnlyDisplayManager, + IEnumerable orderContentTypeDefinitionExclusionProviders) { _contentManager = contentManager; _fieldsOnlyDisplayManager = fieldsOnlyDisplayManager; + _orderContentTypeDefinitionExclusionProviders = orderContentTypeDefinitionExclusionProviders; } public override IDisplayResult Edit(ContentTypeDefinition model) => @@ -43,11 +45,19 @@ public override IDisplayResult Edit(ContentTypeDefinition model) => "OrderPart_TemplateLinks", async viewModel => { - var templateLinks = await _fieldsOnlyDisplayManager - .GetFieldTemplateEditorUrlsAsync(await _contentManager.NewAsync(Order), "Checkout"); + var templateLinks = (await _fieldsOnlyDisplayManager + .GetFieldTemplateEditorUrlsAsync(await _contentManager.NewAsync(Order), "Checkout")) + .AsList(); + + var excludedShapes = new List(_excludedShapes); + + foreach (var provider in _orderContentTypeDefinitionExclusionProviders) + { + excludedShapes.AddRange(provider.GetExcludedShapes(templateLinks)); + } viewModel.TemplateLinks = templateLinks - .WhereNot(link => _excludedShapes.Contains(link.ShapeType)) + .WhereNot(link => excludedShapes.Contains(link.ShapeType)) .Select(link => { var displayText = link.Url diff --git a/src/Modules/OrchardCore.Commerce/Drivers/OrderPartDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/OrderPartDisplayDriver.cs index 7e3ddf012..7ea0fa480 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/OrderPartDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/OrderPartDisplayDriver.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Localization; using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.MoneyDataType.Abstractions; using OrchardCore.Commerce.ViewModels; diff --git a/src/Modules/OrchardCore.Commerce/Drivers/RegionSettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/RegionSettingsDisplayDriver.cs index 71c5e783c..9812c87ee 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/RegionSettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/RegionSettingsDisplayDriver.cs @@ -1,7 +1,7 @@ using Dapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Extensions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.ViewModels; diff --git a/src/Modules/OrchardCore.Commerce/Drivers/ShoppingCartWidgetPartDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/ShoppingCartWidgetPartDisplayDriver.cs index 5e1cf4a02..6211addbe 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/ShoppingCartWidgetPartDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/ShoppingCartWidgetPartDisplayDriver.cs @@ -23,7 +23,7 @@ public override IDisplayResult Display(ShoppingCartWidgetPart part, BuildPartDis private async ValueTask PopulateViewModelAsync(ShoppingCartWidgetPartViewModel model) { // Shopping cart ID is null by default currently. - var cart = await _shoppingCartPersistence.RetrieveAsync(); + var cart = await _shoppingCartPersistence.RetrieveAsync(shoppingCartId: null); model.ItemCount = cart?.ItemCount ?? 0; } diff --git a/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs index e858a45a2..ca5141ed7 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Http; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Exceptions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Exceptions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.Tax.Extensions; using OrchardCore.Commerce.Tax.Models; diff --git a/src/Modules/OrchardCore.Commerce/Events/InventoryOrderEvents.cs b/src/Modules/OrchardCore.Commerce/Events/InventoryOrderEvents.cs new file mode 100644 index 000000000..9997a7af8 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce/Events/InventoryOrderEvents.cs @@ -0,0 +1,28 @@ +using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.ContentManagement; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Events; + +public class InventoryOrderEvents : IOrderEvents +{ + private readonly IProductInventoryService _productInventoryService; + private readonly IShoppingCartHelpers _shoppingCartHelpers; + + public InventoryOrderEvents( + IProductInventoryService productInventoryService, + IShoppingCartHelpers shoppingCartHelpers) + { + _productInventoryService = productInventoryService; + _shoppingCartHelpers = shoppingCartHelpers; + } + + public async Task OrderedAsync(ContentItem order, string shoppingCartId) + { + var cart = await _shoppingCartHelpers.RetrieveAsync(shoppingCartId); + + // Decrease inventories of purchased items. + await _productInventoryService.UpdateInventoriesAsync(cart.Items); + } +} diff --git a/src/Modules/OrchardCore.Commerce/Events/InventoryShoppingCartEvents.cs b/src/Modules/OrchardCore.Commerce/Events/InventoryShoppingCartEvents.cs index 61be76db3..b2d1c8143 100644 --- a/src/Modules/OrchardCore.Commerce/Events/InventoryShoppingCartEvents.cs +++ b/src/Modules/OrchardCore.Commerce/Events/InventoryShoppingCartEvents.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc.Localization; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Inventory.Models; using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement; diff --git a/src/Modules/OrchardCore.Commerce/Events/PromotionOrderEvents.cs b/src/Modules/OrchardCore.Commerce/Events/PromotionOrderEvents.cs new file mode 100644 index 000000000..598f4369e --- /dev/null +++ b/src/Modules/OrchardCore.Commerce/Events/PromotionOrderEvents.cs @@ -0,0 +1,24 @@ +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; +using OrchardCore.Commerce.Promotion.Extensions; +using System.Linq; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Events; + +public class PromotionOrderEvents : IOrderEvents +{ + public Task CreatedFreeAsync(OrderPart orderPart, ShoppingCart cart, ShoppingCartViewModel viewModel) + { + // Store the current applicable discount info, so they will be available in the future. + orderPart.AdditionalData.SetDiscountsByProduct(viewModel + .Lines + .Where(line => line.AdditionalData.GetDiscounts().Any()) + .ToDictionary( + line => line.ProductSku, + line => line.AdditionalData.GetDiscounts())); + + return Task.CompletedTask; + } +} diff --git a/src/Modules/OrchardCore.Commerce/Events/PromotionShoppingCartEvents.cs b/src/Modules/OrchardCore.Commerce/Events/PromotionShoppingCartEvents.cs index e826a1125..05aa1a39c 100644 --- a/src/Modules/OrchardCore.Commerce/Events/PromotionShoppingCartEvents.cs +++ b/src/Modules/OrchardCore.Commerce/Events/PromotionShoppingCartEvents.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Mvc.Localization; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.Promotion.Extensions; using OrchardCore.Commerce.Tax.Extensions; -using OrchardCore.Commerce.ViewModels; using OrchardCore.Modules; using System.Collections.Generic; using System.Linq; diff --git a/src/Modules/OrchardCore.Commerce/Events/ShoppingCartEventsBase.cs b/src/Modules/OrchardCore.Commerce/Events/ShoppingCartEventsBase.cs index 553528511..49ca65953 100644 --- a/src/Modules/OrchardCore.Commerce/Events/ShoppingCartEventsBase.cs +++ b/src/Modules/OrchardCore.Commerce/Events/ShoppingCartEventsBase.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Mvc.Localization; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.ViewModels; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Events/TaxShoppingCartEvents.cs b/src/Modules/OrchardCore.Commerce/Events/TaxShoppingCartEvents.cs index f7050edf0..93956055a 100644 --- a/src/Modules/OrchardCore.Commerce/Events/TaxShoppingCartEvents.cs +++ b/src/Modules/OrchardCore.Commerce/Events/TaxShoppingCartEvents.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.Mvc.Localization; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.Tax.Extensions; -using OrchardCore.Commerce.ViewModels; using OrchardCore.Entities; using OrchardCore.Settings; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/Events/UserAddressFieldEvents.cs b/src/Modules/OrchardCore.Commerce/Events/UserAddressFieldEvents.cs new file mode 100644 index 000000000..d7415b704 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce/Events/UserAddressFieldEvents.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json.Linq; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Fields; +using OrchardCore.Commerce.ContentFields.Events; +using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.ViewModels; +using OrchardCore.ContentManagement.Display.Models; +using OrchardCore.DisplayManagement.ModelBinding; +using System; +using System.Threading.Tasks; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; + +namespace OrchardCore.Commerce.Events; + +public class UserAddressFieldEvents : IAddressFieldEvents +{ + private readonly IHttpContextAccessor _hca; + private readonly IUserService _userService; + public UserAddressFieldEvents(IHttpContextAccessor hca, IUserService userService) + { + _hca = hca; + _userService = userService; + } + + public async Task UpdatingAsync( + AddressFieldViewModel viewModel, + AddressField field, + IUpdateModel updater, + UpdateFieldEditorContext context) + { + if (!viewModel.ToBeSaved || + string.IsNullOrEmpty(viewModel.UserAddressToSave) || + await _userService.GetCurrentFullUserAsync(_hca) is not { } user) + { + return; + } + + await _userService.AlterUserSettingAsync(user, UserAddresses, contentItem => + { + var part = contentItem.GetJObject(nameof(UserAddressesPart)); + + if (part[viewModel.UserAddressToSave] is not JObject) + { + part[viewModel.UserAddressToSave] = JObject.FromObject(new AddressField()); + } + + if (part.GetJObject(viewModel.UserAddressToSave) is not { } userAddressToSave) + { + throw new InvalidOperationException( + $"The property {viewModel.UserAddressToSave} is missing from {nameof(UserAddressesPart)}."); + } + + userAddressToSave[nameof(AddressField.Address)] = JToken.FromObject(viewModel.Address); + return contentItem; + }); + } +} diff --git a/src/Modules/OrchardCore.Commerce/Events/UserSettingsCheckoutEvents.cs b/src/Modules/OrchardCore.Commerce/Events/UserSettingsCheckoutEvents.cs new file mode 100644 index 000000000..dd418e4c4 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce/Events/UserSettingsCheckoutEvents.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Events; + +public class UserSettingsCheckoutEvents : ICheckoutEvents +{ + private readonly IHttpContextAccessor _hca; + public UserSettingsCheckoutEvents(IHttpContextAccessor hca) => + _hca = hca; + + public async Task OrderCreatingAsync(OrderPart orderPart, string shoppingCartId) + { + if (await _hca.HttpContext.GetUserAddressAsync() is { } userAddresses) + { + orderPart.BillingAddress.Address = userAddresses.BillingAddress.Address; + orderPart.ShippingAddress.Address = userAddresses.ShippingAddress.Address; + orderPart.BillingAndShippingAddressesMatch.Value = userAddresses.BillingAndShippingAddressesMatch.Value; + } + + if (await _hca.HttpContext.GetUserDetailsAsync() is { } userDetails) + { + orderPart.Phone.Text = userDetails.PhoneNumber.Text; + orderPart.VatNumber.Text = userDetails.VatNumber.Text; + orderPart.IsCorporation.Value = userDetails.IsCorporation.Value; + } + } +} diff --git a/src/Modules/OrchardCore.Commerce/Events/UserSettingsOrderEvents.cs b/src/Modules/OrchardCore.Commerce/Events/UserSettingsOrderEvents.cs new file mode 100644 index 000000000..d31ebea47 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce/Events/UserSettingsOrderEvents.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json.Linq; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Models; +using OrchardCore.ContentManagement; +using System.Threading.Tasks; +using static OrchardCore.Commerce.ContentFields.Constants.ContentTypes; + +namespace OrchardCore.Commerce.Events; + +public class UserSettingsOrderEvents : IOrderEvents +{ + private readonly IHttpContextAccessor _hca; + private readonly IUserService _userService; + + public UserSettingsOrderEvents(IHttpContextAccessor hca, IUserService userService) + { + _hca = hca; + _userService = userService; + } + + public async Task FinalizeAsync(ContentItem order, string shoppingCartId, string paymentProviderName) + { + // Saving addresses. + var orderPart = order.As(); + + if (_hca.HttpContext != null && await _userService.GetFullUserAsync(_hca.HttpContext.User) is { } user) + { + var isSame = orderPart.BillingAndShippingAddressesMatch.Value; + + await _userService.AlterUserSettingAsync(user, UserAddresses, contentItem => + { + var part = contentItem.TryGetValue(nameof(UserAddressesPart), out var partJson) + ? partJson.ToObject()! + : new UserAddressesPart(); + + part.BillingAndShippingAddressesMatch.Value = isSame; + contentItem[nameof(UserAddressesPart)] = JToken.FromObject(part); + return contentItem; + }); + } + } +} diff --git a/src/Modules/OrchardCore.Commerce/Events/WorkflowOrderEvents.cs b/src/Modules/OrchardCore.Commerce/Events/WorkflowOrderEvents.cs new file mode 100644 index 000000000..02ad79d9e --- /dev/null +++ b/src/Modules/OrchardCore.Commerce/Events/WorkflowOrderEvents.cs @@ -0,0 +1,19 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Workflow; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Activities; +using OrchardCore.ContentManagement; +using OrchardCore.Workflows.Services; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Events; + +public class WorkflowOrderEvents : IOrderEvents +{ + private readonly IWorkflowManager _workflowManager; + + public WorkflowOrderEvents(IWorkflowManager workflowManager) => + _workflowManager = workflowManager; + + public Task OrderedAsync(ContentItem order, string shoppingCartId) => + _workflowManager.TriggerContentItemEventAsync(order); +} diff --git a/src/Modules/OrchardCore.Commerce/Events/WorkflowShoppingCartEvents.cs b/src/Modules/OrchardCore.Commerce/Events/WorkflowShoppingCartEvents.cs index f9c1b77e6..dcc067b99 100644 --- a/src/Modules/OrchardCore.Commerce/Events/WorkflowShoppingCartEvents.cs +++ b/src/Modules/OrchardCore.Commerce/Events/WorkflowShoppingCartEvents.cs @@ -2,9 +2,10 @@ using Microsoft.AspNetCore.Mvc.Localization; using Newtonsoft.Json; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.Activities; using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.ViewModels; using OrchardCore.Workflows.Models; using OrchardCore.Workflows.Services; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/Extensions/ContentItemDisplayManagerExtensions.cs b/src/Modules/OrchardCore.Commerce/Extensions/ContentItemDisplayManagerExtensions.cs deleted file mode 100644 index 66dedc1e0..000000000 --- a/src/Modules/OrchardCore.Commerce/Extensions/ContentItemDisplayManagerExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using OrchardCore.DisplayManagement; -using System.Threading.Tasks; -using static OrchardCore.Commerce.Constants.ContentTypes; - -namespace OrchardCore.ContentManagement.Display; - -public static class ContentItemDisplayManagerExtensions -{ - public static Task BuildMvcTitleAsync(this IContentItemDisplayManager manager, string text) - { - var header = new ContentItem { ContentType = MvcTitle, DisplayText = text }; - return manager.BuildDisplayAsync(header, updater: null); - } -} diff --git a/src/Modules/OrchardCore.Commerce/Extensions/LineItemExtensions.cs b/src/Modules/OrchardCore.Commerce/Extensions/LineItemExtensions.cs index 2939213a6..599b40f40 100644 --- a/src/Modules/OrchardCore.Commerce/Extensions/LineItemExtensions.cs +++ b/src/Modules/OrchardCore.Commerce/Extensions/LineItemExtensions.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.MoneyDataType.Extensions; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/Extensions/PriceProviderExtensions.cs b/src/Modules/OrchardCore.Commerce/Extensions/PriceProviderExtensions.cs index c8c8d4821..de81de696 100644 --- a/src/Modules/OrchardCore.Commerce/Extensions/PriceProviderExtensions.cs +++ b/src/Modules/OrchardCore.Commerce/Extensions/PriceProviderExtensions.cs @@ -1,4 +1,5 @@ using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Models; using System.Collections.Generic; using System.Linq; diff --git a/src/Modules/OrchardCore.Commerce/Extensions/ProductInventoryProviderExtensions.cs b/src/Modules/OrchardCore.Commerce/Extensions/ProductInventoryProviderExtensions.cs index 577ec1c11..4566707e9 100644 --- a/src/Modules/OrchardCore.Commerce/Extensions/ProductInventoryProviderExtensions.cs +++ b/src/Modules/OrchardCore.Commerce/Extensions/ProductInventoryProviderExtensions.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Extensions/ShoppingCartCheckoutExtensions.cs b/src/Modules/OrchardCore.Commerce/Extensions/ShoppingCartCheckoutExtensions.cs index 353cefc45..f25f83127 100644 --- a/src/Modules/OrchardCore.Commerce/Extensions/ShoppingCartCheckoutExtensions.cs +++ b/src/Modules/OrchardCore.Commerce/Extensions/ShoppingCartCheckoutExtensions.cs @@ -1,5 +1,5 @@ using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using System.Collections.Generic; using System.Linq; diff --git a/src/Modules/OrchardCore.Commerce/Extensions/UpdateModelExtensions.cs b/src/Modules/OrchardCore.Commerce/Extensions/UpdateModelExtensions.cs deleted file mode 100644 index b3ec51a1c..000000000 --- a/src/Modules/OrchardCore.Commerce/Extensions/UpdateModelExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding; -using OrchardCore.DisplayManagement.ModelBinding; -using System.Collections.Generic; -using System.Linq; - -namespace OrchardCore.Commerce.Extensions; - -public static class UpdateModelExtensions -{ - public static IEnumerable GetModelErrors(this IUpdateModel updateModel) => - updateModel - .ModelState - .Values - .SelectMany(entry => entry.Errors) - .Where(error => !string.IsNullOrWhiteSpace(error.ErrorMessage)); - - public static IEnumerable GetModelErrorMessages(this IUpdateModel updateModel) => - updateModel.GetModelErrors().Select(error => error.ErrorMessage); -} diff --git a/src/Modules/OrchardCore.Commerce/Extensions/UserManagerExtensions.cs b/src/Modules/OrchardCore.Commerce/Extensions/UserManagerExtensions.cs index 2af4a7508..c2c538963 100644 --- a/src/Modules/OrchardCore.Commerce/Extensions/UserManagerExtensions.cs +++ b/src/Modules/OrchardCore.Commerce/Extensions/UserManagerExtensions.cs @@ -5,7 +5,7 @@ using OrchardCore.Users.Models; using System.Security.Claims; using System.Threading.Tasks; -using static OrchardCore.Commerce.Constants.ContentTypes; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; namespace Microsoft.AspNetCore.Identity; diff --git a/src/Modules/OrchardCore.Commerce/Handlers/OrderPartHandler.cs b/src/Modules/OrchardCore.Commerce/Handlers/OrderPartHandler.cs index dc40af6b6..888baa35a 100644 --- a/src/Modules/OrchardCore.Commerce/Handlers/OrderPartHandler.cs +++ b/src/Modules/OrchardCore.Commerce/Handlers/OrderPartHandler.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Localization; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Handlers; using System; diff --git a/src/Modules/OrchardCore.Commerce/Handlers/OrderPermissionsAuthorizationHandler.cs b/src/Modules/OrchardCore.Commerce/Handlers/OrderPermissionsAuthorizationHandler.cs index 977562c7b..cd7d84bcb 100644 --- a/src/Modules/OrchardCore.Commerce/Handlers/OrderPermissionsAuthorizationHandler.cs +++ b/src/Modules/OrchardCore.Commerce/Handlers/OrderPermissionsAuthorizationHandler.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Authorization; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.ContentManagement; using OrchardCore.Security; using System; @@ -23,7 +23,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext // Regular users should only see their own Orders, while users with the ManageOrders permission should be // able to see all Orders. - if (!await _authorizationServiceLazy.Value.AuthorizeAsync(context.User, Permissions.ManageOrders) && + if (!await _authorizationServiceLazy.Value.AuthorizeAsync(context.User, Payment.Permissions.ManageOrders) && context.User.Identity?.Name != order.ContentItem.Author) { context.Fail(); diff --git a/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs b/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs index 97a2697e7..f54e89b57 100644 --- a/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs +++ b/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs @@ -1,10 +1,11 @@ +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement; using YesSql.Indexes; namespace OrchardCore.Commerce.Indexes; -public class ProductPartIndex : MapIndex +public class ProductPartIndex : MapIndex, ISkuHolder { public string ContentItemId { get; set; } public string Sku { get; set; } diff --git a/src/Modules/OrchardCore.Commerce/Liquid/AddressFieldEditorViewModelConverterFilter.cs b/src/Modules/OrchardCore.Commerce/Liquid/AddressFieldEditorViewModelConverterFilter.cs index 0683e82d8..f1fdd3dbf 100644 --- a/src/Modules/OrchardCore.Commerce/Liquid/AddressFieldEditorViewModelConverterFilter.cs +++ b/src/Modules/OrchardCore.Commerce/Liquid/AddressFieldEditorViewModelConverterFilter.cs @@ -1,6 +1,6 @@ using Fluid; using Fluid.Values; -using OrchardCore.Commerce.Fields; +using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.Commerce.ViewModels; using OrchardCore.Liquid; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Liquid/OrderLineItemViewModelsAndTaxRatesConverterFilter.cs b/src/Modules/OrchardCore.Commerce/Liquid/OrderLineItemViewModelsAndTaxRatesConverterFilter.cs index fe8d4a67f..1fec2948d 100644 --- a/src/Modules/OrchardCore.Commerce/Liquid/OrderLineItemViewModelsAndTaxRatesConverterFilter.cs +++ b/src/Modules/OrchardCore.Commerce/Liquid/OrderLineItemViewModelsAndTaxRatesConverterFilter.cs @@ -2,7 +2,7 @@ using Fluid.Values; using Newtonsoft.Json.Linq; using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Tax.Models; using OrchardCore.ContentManagement; using OrchardCore.Liquid; diff --git a/src/Modules/OrchardCore.Commerce/Liquid/ProductFilter.cs b/src/Modules/OrchardCore.Commerce/Liquid/ProductFilter.cs index a90482dc8..581bdad0f 100644 --- a/src/Modules/OrchardCore.Commerce/Liquid/ProductFilter.cs +++ b/src/Modules/OrchardCore.Commerce/Liquid/ProductFilter.cs @@ -2,7 +2,8 @@ using Fluid.Values; using Newtonsoft.Json.Linq; using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.ViewModels; using OrchardCore.Liquid; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/Manifest.cs b/src/Modules/OrchardCore.Commerce/Manifest.cs index 962ed47ef..5e4cc4d27 100644 --- a/src/Modules/OrchardCore.Commerce/Manifest.cs +++ b/src/Modules/OrchardCore.Commerce/Manifest.cs @@ -20,6 +20,7 @@ "OrchardCore.Workflows", "OrchardCore.Templates", "OrchardCore.Commerce.ContentFields", + "OrchardCore.Commerce.Payment", } )] diff --git a/src/Modules/OrchardCore.Commerce/Migrations/MvcTitleMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/MvcTitleMigrations.cs index 785644059..2c278b61c 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/MvcTitleMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/MvcTitleMigrations.cs @@ -1,7 +1,7 @@ using OrchardCore.ContentManagement.Metadata; using OrchardCore.ContentManagement.Metadata.Builders; using OrchardCore.Data.Migration; -using static OrchardCore.Commerce.Constants.ContentTypes; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; namespace OrchardCore.Commerce.Migrations; diff --git a/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs index 4ffc84c02..f1a9489b8 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs @@ -1,7 +1,5 @@ -using Lombiq.HelpfulLibraries.OrchardCore.Data; -using OrchardCore.Commerce.Fields; -using OrchardCore.Commerce.Indexes; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Fields; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Settings; using OrchardCore.ContentFields.Fields; using OrchardCore.ContentFields.Settings; @@ -12,9 +10,9 @@ using OrchardCore.Mvc.Utilities; using OrchardCore.Title.Models; using System.Collections.Generic; -using YesSql.Sql; -using static OrchardCore.Commerce.Constants.ContentTypes; -using static OrchardCore.Commerce.Constants.OrderStatuses; +using System.Diagnostics.CodeAnalysis; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; +using static OrchardCore.Commerce.Abstractions.Constants.OrderStatuses; namespace OrchardCore.Commerce.Migrations; @@ -105,11 +103,6 @@ public int Create() .WithDisplayName("Buyer is a corporation")) ); - SchemaBuilder - .CreateMapIndexTable(table => table - .Column(nameof(OrderPaymentIndex.OrderId), column => column.WithCommonUniqueIdLength()) - .Column(nameof(OrderPaymentIndex.PaymentIntentId))); - return 6; } @@ -210,15 +203,8 @@ public int UpdateFrom3() return 4; } - public int UpdateFrom4() - { - SchemaBuilder - .CreateMapIndexTable(table => table - .Column(nameof(OrderPaymentIndex.OrderId), column => column.WithCommonUniqueIdLength()) - .Column(nameof(OrderPaymentIndex.PaymentIntentId))); - - return 5; - } + [SuppressMessage("Minor Code Smell", "S3400:Methods should not return constants", Justification = "Special migration.")] + public int UpdateFrom4() => 5; // Moved into a separate module. public int UpdateFrom5() { diff --git a/src/Modules/OrchardCore.Commerce/Migrations/ShoppingCartWidgetMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/ShoppingCartWidgetMigrations.cs index dc1d9a3f8..a53aa17a4 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/ShoppingCartWidgetMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/ShoppingCartWidgetMigrations.cs @@ -2,7 +2,7 @@ using OrchardCore.ContentManagement.Metadata; using OrchardCore.ContentManagement.Metadata.Settings; using OrchardCore.Data.Migration; -using static OrchardCore.Commerce.Constants.ContentTypes; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; namespace OrchardCore.Commerce.Migrations; public class ShoppingCartWidgetMigrations : DataMigration diff --git a/src/Modules/OrchardCore.Commerce/Migrations/StripeMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/StripeMigrations.cs deleted file mode 100644 index 41659ccf8..000000000 --- a/src/Modules/OrchardCore.Commerce/Migrations/StripeMigrations.cs +++ /dev/null @@ -1,37 +0,0 @@ -using OrchardCore.Commerce.Models; -using OrchardCore.ContentManagement.Metadata; -using OrchardCore.ContentManagement.Metadata.Settings; -using OrchardCore.Data.Migration; - -namespace OrchardCore.Commerce.Migrations; - -public class StripeMigrations : DataMigration -{ - private readonly IContentDefinitionManager _contentDefinitionManager; - - public StripeMigrations(IContentDefinitionManager contentDefinitionManager) => - _contentDefinitionManager = contentDefinitionManager; - - public int Create() - { - _contentDefinitionManager - .AlterPartDefinition(builder => builder - .Configure(part => part.Attachable()) - .WithField(part => part.PaymentIntentId)); - - _contentDefinitionManager - .AlterTypeDefinition(Constants.ContentTypes.Order, builder => builder - .WithPart(nameof(StripePaymentPart))); - - return 2; - } - - public int UpdateFrom1() - { - _contentDefinitionManager.AlterPartDefinition( - nameof(StripePaymentPart), - builder => builder.RemoveField("PaymentIntentId")); - - return 2; - } -} diff --git a/src/Modules/OrchardCore.Commerce/Migrations/UserAddressesMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/UserAddressesMigrations.cs index 6a5a41e62..ccbca8a37 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/UserAddressesMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/UserAddressesMigrations.cs @@ -2,7 +2,7 @@ using OrchardCore.ContentManagement.Metadata; using OrchardCore.ContentManagement.Metadata.Settings; using OrchardCore.Data.Migration; -using static OrchardCore.Commerce.Constants.ContentTypes; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; namespace OrchardCore.Commerce.Migrations; diff --git a/src/Modules/OrchardCore.Commerce/Migrations/UserDetailsMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/UserDetailsMigrations.cs index e73bc9754..17280ccfd 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/UserDetailsMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/UserDetailsMigrations.cs @@ -2,7 +2,7 @@ using OrchardCore.ContentManagement.Metadata; using OrchardCore.ContentManagement.Metadata.Settings; using OrchardCore.Data.Migration; -using static OrchardCore.Commerce.Constants.ContentTypes; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; namespace OrchardCore.Commerce.Migrations; diff --git a/src/Modules/OrchardCore.Commerce/Models/Payment.cs b/src/Modules/OrchardCore.Commerce/Models/Payment.cs deleted file mode 100644 index c17eaf98e..000000000 --- a/src/Modules/OrchardCore.Commerce/Models/Payment.cs +++ /dev/null @@ -1,18 +0,0 @@ -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.MoneyDataType; -using System; - -namespace OrchardCore.Commerce.Models; - -public class Payment : IPayment -{ - public string Kind { get; set; } - - public string TransactionId { get; set; } - - public string ChargeText { get; set; } - - public Amount Amount { get; set; } - - public DateTime CreatedUtc { get; set; } -} diff --git a/src/Modules/OrchardCore.Commerce/Models/ProductEstimationContext.cs b/src/Modules/OrchardCore.Commerce/Models/ProductEstimationContext.cs index 475e477d6..541b0efea 100644 --- a/src/Modules/OrchardCore.Commerce/Models/ProductEstimationContext.cs +++ b/src/Modules/OrchardCore.Commerce/Models/ProductEstimationContext.cs @@ -1,4 +1,5 @@ -using OrchardCore.Commerce.AddressDataType; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.AddressDataType; namespace OrchardCore.Commerce.Models; diff --git a/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs b/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs index fa7582353..4275af296 100644 --- a/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs +++ b/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs @@ -1,3 +1,4 @@ +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Inventory.Models; using OrchardCore.ContentManagement; using OrchardCore.Media.Fields; @@ -9,11 +10,9 @@ namespace OrchardCore.Commerce.Models; /// The product part describes the most basic product attribute: a SKU. It also identifies any content item as a product, /// by its mere presence. /// -public class ProductPart : ContentPart +public class ProductPart : ContentPart, ISkuHolderContent { - /// - /// Gets or sets the product's SKU, which can also be used as an alias for the item. - /// + /// public string Sku { get; set; } /// diff --git a/src/Modules/OrchardCore.Commerce/Models/PromotionAndTaxProviderContext.cs b/src/Modules/OrchardCore.Commerce/Models/PromotionAndTaxProviderContext.cs index 25b1a1300..d68e09488 100644 --- a/src/Modules/OrchardCore.Commerce/Models/PromotionAndTaxProviderContext.cs +++ b/src/Modules/OrchardCore.Commerce/Models/PromotionAndTaxProviderContext.cs @@ -1,9 +1,9 @@ +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.AddressDataType; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.MoneyDataType.Extensions; using OrchardCore.Commerce.Promotion.Extensions; using OrchardCore.Commerce.Promotion.Models; -using OrchardCore.Commerce.ViewModels; using OrchardCore.ContentManagement; using System; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/Models/ShoppingCartCellViewModel.cs b/src/Modules/OrchardCore.Commerce/Models/ShoppingCartCellViewModel.cs index 57178ff06..fc00d6cc6 100644 --- a/src/Modules/OrchardCore.Commerce/Models/ShoppingCartCellViewModel.cs +++ b/src/Modules/OrchardCore.Commerce/Models/ShoppingCartCellViewModel.cs @@ -1,5 +1,5 @@ -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.ViewModels; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.ViewModels; using System; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/Models/ShoppingCartDisplayingEventContext.cs b/src/Modules/OrchardCore.Commerce/Models/ShoppingCartDisplayingEventContext.cs index 25b43b664..8f9b63b90 100644 --- a/src/Modules/OrchardCore.Commerce/Models/ShoppingCartDisplayingEventContext.cs +++ b/src/Modules/OrchardCore.Commerce/Models/ShoppingCartDisplayingEventContext.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc.Localization; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.AddressDataType; using OrchardCore.Commerce.MoneyDataType; -using OrchardCore.Commerce.ViewModels; using System.Collections.Generic; namespace OrchardCore.Commerce.Models; diff --git a/src/Modules/OrchardCore.Commerce/Models/UserAddressesPart.cs b/src/Modules/OrchardCore.Commerce/Models/UserAddressesPart.cs index 7c09253e9..773c52ca9 100644 --- a/src/Modules/OrchardCore.Commerce/Models/UserAddressesPart.cs +++ b/src/Modules/OrchardCore.Commerce/Models/UserAddressesPart.cs @@ -1,4 +1,4 @@ -using OrchardCore.Commerce.Fields; +using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.ContentFields.Fields; using OrchardCore.ContentManagement; diff --git a/src/Modules/OrchardCore.Commerce/OrchardCore.Commerce.csproj b/src/Modules/OrchardCore.Commerce/OrchardCore.Commerce.csproj index 975e5e69b..d8da6123b 100644 --- a/src/Modules/OrchardCore.Commerce/OrchardCore.Commerce.csproj +++ b/src/Modules/OrchardCore.Commerce/OrchardCore.Commerce.csproj @@ -36,13 +36,11 @@ - - - + @@ -54,16 +52,12 @@ - - ..\..\..\..\..\Utilities\Lombiq.NodeJs.Extensions\Lombiq.NodeJs.Extensions - - - + - - + + diff --git a/src/Modules/OrchardCore.Commerce/Permissions.cs b/src/Modules/OrchardCore.Commerce/Permissions.cs index 09437ab47..acbfaa832 100644 --- a/src/Modules/OrchardCore.Commerce/Permissions.cs +++ b/src/Modules/OrchardCore.Commerce/Permissions.cs @@ -1,44 +1,21 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Users; using OrchardCore.Security.Permissions; using System.Collections.Generic; -using System.Threading.Tasks; namespace OrchardCore.Commerce; -public class Permissions : IPermissionProvider +public class Permissions : AdminPermissionBase { public static readonly Permission ManageCurrencySettings = new(nameof(ManageCurrencySettings), "Manage Currency Settings"); - public static readonly Permission ManageStripeApiSettings = new(nameof(ManageStripeApiSettings), "Manage Stripe API Settings"); public static readonly Permission ManagePriceDisplaySettings = new(nameof(ManagePriceDisplaySettings), "Manage Price Display Settings"); public static readonly Permission ManageRegionSettings = new(nameof(ManageRegionSettings), "Manage Region Settings"); - public static readonly Permission ManageOrders = new(nameof(ManageOrders), "Manage Orders"); - public static readonly Permission Checkout = new(nameof(Checkout), "Ability to checkout"); - public Task> GetPermissionsAsync() => - Task.FromResult>(new[] - { - ManageCurrencySettings, - ManageStripeApiSettings, - Checkout, - ManageRegionSettings, - ManageOrders, - ManagePriceDisplaySettings, - }); + private static readonly IReadOnlyList _adminPermissions = new[] + { + ManageCurrencySettings, + ManageRegionSettings, + ManagePriceDisplaySettings, + }; - public IEnumerable GetDefaultStereotypes() => - new[] - { - new PermissionStereotype - { - Name = "Administrator", - Permissions = new[] - { - ManageCurrencySettings, - ManageStripeApiSettings, - Checkout, - ManageRegionSettings, - ManageOrders, - ManagePriceDisplaySettings, - }, - }, - }; + protected override IEnumerable AdminPermissions => _adminPermissions; } diff --git a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/BooleanProductAttributeValue.cs b/src/Modules/OrchardCore.Commerce/ProductAttributeValues/BooleanProductAttributeValue.cs index 3c79b3a92..a1a39adfa 100644 --- a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/BooleanProductAttributeValue.cs +++ b/src/Modules/OrchardCore.Commerce/ProductAttributeValues/BooleanProductAttributeValue.cs @@ -1,3 +1,4 @@ +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; using System.Globalization; namespace OrchardCore.Commerce.ProductAttributeValues; diff --git a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/NumericProductAttributeValue.cs b/src/Modules/OrchardCore.Commerce/ProductAttributeValues/NumericProductAttributeValue.cs index 03ce7c88e..01e59ed73 100644 --- a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/NumericProductAttributeValue.cs +++ b/src/Modules/OrchardCore.Commerce/ProductAttributeValues/NumericProductAttributeValue.cs @@ -1,4 +1,6 @@ -namespace OrchardCore.Commerce.ProductAttributeValues; +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; + +namespace OrchardCore.Commerce.ProductAttributeValues; public class NumericProductAttributeValue : BaseProductAttributeValue { diff --git a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/TextProductAttributeValue.cs b/src/Modules/OrchardCore.Commerce/ProductAttributeValues/TextProductAttributeValue.cs index 9227cc6ae..45e2b56a6 100644 --- a/src/Modules/OrchardCore.Commerce/ProductAttributeValues/TextProductAttributeValue.cs +++ b/src/Modules/OrchardCore.Commerce/ProductAttributeValues/TextProductAttributeValue.cs @@ -1,4 +1,5 @@ -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; using System.Collections.Generic; using System.Globalization; using System.Linq; diff --git a/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Development.Setup.recipe.json b/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Development.Setup.recipe.json index d483eaf71..32f189ff9 100644 --- a/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Development.Setup.recipe.json +++ b/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Development.Setup.recipe.json @@ -64,9 +64,9 @@ "OrchardCore.Commerce.CurrencySettingsSelector", "OrchardCore.Commerce.SessionCartStorage", - "OrchardCore.Commerce.ContentFields", + "OrchardCore.Commerce.Payment.DummyProvider", + "OrchardCore.Commerce.Payment.Stripe", "OrchardCore.Commerce.Promotion", - "OrchardCore.Commerce.Tax", "OrchardCore.Commerce.Inventory", // Themes diff --git a/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Development.Tests.Setup.recipe.json b/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Development.Tests.Setup.recipe.json index 2d29337e3..bec67bc85 100644 --- a/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Development.Tests.Setup.recipe.json +++ b/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Development.Tests.Setup.recipe.json @@ -32,6 +32,9 @@ "enable": [ // Needed for consistent, machine-independent locale setting. "OrchardCore.Localization" + ], + "disable": [ + "OrchardCore.Commerce.Payment.Stripe" ] }, { diff --git a/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Samples.Order.recipe.json b/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Samples.Order.recipe.json index d0bc90b92..ffea04931 100644 --- a/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Samples.Order.recipe.json +++ b/src/Modules/OrchardCore.Commerce/Recipes/OrchardCore.Commerce.Samples.Order.recipe.json @@ -52,7 +52,19 @@ } ], "AdditionalCosts": [], - "Charges": [], + "Charges": [ + { + "$type": "OrchardCore.Commerce.Models.Payment, OrchardCore.Commerce.Payment", + "Kind": "Dummy Payment", + "TransactionId": "43p7j4274jq7r3s860gadwv8mf:0", + "ChargeText": "Dummy transaction of US Dollar.", + "Amount": { + "value": 5.0, + "currency": "USD" + }, + "CreatedUtc": "2023-11-05T01:05:36.6982061Z" + } + ], "Email": { "Text": "admin@admin.com" }, diff --git a/src/Modules/OrchardCore.Commerce/ResourceManagementOptionsConfiguration.cs b/src/Modules/OrchardCore.Commerce/ResourceManagementOptionsConfiguration.cs index 233bca2a9..0acce82a6 100644 --- a/src/Modules/OrchardCore.Commerce/ResourceManagementOptionsConfiguration.cs +++ b/src/Modules/OrchardCore.Commerce/ResourceManagementOptionsConfiguration.cs @@ -15,13 +15,6 @@ public class ResourceManagementOptionsConfiguration : IConfigureOptions _userManager; - private readonly IRegionService _regionService; - private readonly Lazy _userServiceLazy; - private readonly IPaymentIntentPersistence _paymentIntentPersistence; - private readonly IShoppingCartPersistence _shoppingCartPersistence; - private readonly IHttpContextAccessor _hca; - private readonly IUpdateModelAccessor _updateModelAccessor; - private readonly IContentItemDisplayManager _contentItemDisplayManager; - - // We need all of them. -#pragma warning disable S107 // Methods should not have too many parameters - public PaymentService( - IStripePaymentService stripePaymentService, - IFieldsOnlyDisplayManager fieldsOnlyDisplayManager, - IOrchardServices services, - IShoppingCartHelpers shoppingCartHelpers, - IRegionService regionService, - Lazy userServiceLazy, - IPaymentIntentPersistence paymentIntentPersistence, - IShoppingCartPersistence shoppingCartPersistence, - IUpdateModelAccessor updateModelAccessor, - IContentItemDisplayManager contentItemDisplayManager) -#pragma warning restore S107 // Methods should not have too many parameters - { - _stripePaymentService = stripePaymentService; - _fieldsOnlyDisplayManager = fieldsOnlyDisplayManager; - _contentManager = services.ContentManager.Value; - _shoppingCartHelpers = shoppingCartHelpers; - _siteService = services.SiteService.Value; - _userManager = services.UserManager.Value; - _regionService = regionService; - _userServiceLazy = userServiceLazy; - _updateModelAccessor = updateModelAccessor; - _contentItemDisplayManager = contentItemDisplayManager; - _paymentIntentPersistence = paymentIntentPersistence; - _shoppingCartPersistence = shoppingCartPersistence; - _hca = services.HttpContextAccessor.Value; - } - - public async Task CreateCheckoutViewModelAsync( - string shoppingCartId, - Action updateOrderPart = null) - { - var orderPart = new OrderPart(); - - if (await _hca.HttpContext.GetUserAddressAsync() is { } userAddresses) - { - orderPart.BillingAddress.Address = userAddresses.BillingAddress.Address; - orderPart.ShippingAddress.Address = userAddresses.ShippingAddress.Address; - orderPart.BillingAndShippingAddressesMatch.Value = userAddresses.BillingAndShippingAddressesMatch.Value; - } - - if (await _hca.HttpContext.GetUserDetailsAsync() is { } userDetails) - { - orderPart.Phone.Text = userDetails.PhoneNumber.Text; - orderPart.VatNumber.Text = userDetails.VatNumber.Text; - orderPart.IsCorporation.Value = userDetails.IsCorporation.Value; - } - - var email = _hca.HttpContext?.User is { Identity.IsAuthenticated: true } user - ? await _userManager.GetEmailAsync(await _userManager.GetUserAsync(user)) - : string.Empty; - - orderPart.Email.Text = email; - orderPart.ShippingAddress.UserAddressToSave = nameof(orderPart.ShippingAddress); - orderPart.BillingAddress.UserAddressToSave = nameof(orderPart.BillingAddress); - - updateOrderPart?.Invoke(orderPart); - - var cart = await _shoppingCartHelpers.CreateShoppingCartViewModelAsync( - shoppingCartId, - orderPart.ShippingAddress.Address, - orderPart.BillingAddress.Address); - if (cart?.Totals.Single() is not { } total) return null; - - var checkoutShapes = (await _fieldsOnlyDisplayManager.DisplayFieldsAsync( - await _contentManager.NewAsync(Order), - "Checkout")) - .ToList(); - - var stripeApiSettings = (await _siteService.GetSiteSettingsAsync()).As(); - var initPaymentIntent = new PaymentIntent(); - if (!string.IsNullOrEmpty(stripeApiSettings.PublishableKey) && - !string.IsNullOrEmpty(stripeApiSettings.SecretKey) && - total.Value > 0) - { - var paymentIntentId = _paymentIntentPersistence.Retrieve(); - initPaymentIntent = await _stripePaymentService.InitializePaymentIntentAsync(paymentIntentId); - } - - var currency = total.Currency; - var netTotal = new Amount(0, currency); - var grossTotal = new Amount(0, currency); - - var lines = cart.Lines; - foreach (var line in lines) - { - var additionalData = line.AdditionalData; - var grossAmount = additionalData.GetGrossPrice(); - if (grossAmount.Value > 0) - { - grossTotal += grossAmount * line.Quantity; - } - - var netAmount = additionalData.GetNetPrice(); - var netPrice = netAmount.Value > 0 ? netAmount : line.UnitPrice; - netTotal += netPrice * line.Quantity; - } - - return new CheckoutViewModel - { - ShoppingCartId = shoppingCartId, - Regions = (await _regionService.GetAvailableRegionsAsync()).CreateSelectListOptions(), - OrderPart = orderPart, - SingleCurrencyTotal = total, - NetTotal = netTotal, - GrossTotal = grossTotal, - StripePublishableKey = (await _siteService.GetSiteSettingsAsync()).As().PublishableKey, - UserEmail = email, - CheckoutShapes = checkoutShapes, - PaymentIntentClientSecret = initPaymentIntent?.ClientSecret, - }; - } - - public async Task FinalModificationOfOrderAsync(ContentItem order) - { - // Saving addresses. - var userService = _userServiceLazy.Value; - var orderPart = order.As(); - - if (_hca.HttpContext != null && await userService.GetFullUserAsync(_hca.HttpContext.User) is { } user) - { - var isSame = orderPart.BillingAndShippingAddressesMatch.Value; - - await userService.AlterUserSettingAsync(user, UserAddresses, contentItem => - { - var part = contentItem.TryGetValue(nameof(UserAddressesPart), out var partJson) - ? partJson.ToObject()! - : new UserAddressesPart(); - - part.BillingAndShippingAddressesMatch.Value = isSame; - contentItem[nameof(UserAddressesPart)] = JToken.FromObject(part); - return contentItem; - }); - } - - var currentShoppingCart = await _shoppingCartPersistence.RetrieveAsync(); - currentShoppingCart?.Items?.Clear(); - - // Shopping cart ID is null by default currently. - await _shoppingCartPersistence.StoreAsync(currentShoppingCart); - - // Set back to default, because a new payment intent should be created on the next checkout. - _paymentIntentPersistence.Store(paymentIntentId: string.Empty); - } - - public async Task CreateNoPaymentOrderFromShoppingCartAsync() - { - var currentShoppingCart = await _shoppingCartPersistence.RetrieveAsync(); - - var order = await _contentManager.NewAsync(Order); - if (await UpdateOrderWithDriversAsync(order)) - { - return null; - } - - var lineItems = await _stripePaymentService.CreateOrderLineItemsAsync(currentShoppingCart); - - var cartViewModel = await _shoppingCartHelpers.CreateShoppingCartViewModelAsync( - shoppingCartId: null, - order.As().ShippingAddress.Address, - order.As().BillingAddress.Address); - - if (!cartViewModel.Totals.Any() || cartViewModel.Totals.Any(total => total.Value > 0)) - { - return null; - } - - order.Alter(orderPart => - { - // Shopping cart - orderPart.LineItems.SetItems(lineItems); - - orderPart.Status.Text = OrderStatuses.Pending.HtmlClassify(); - - // Store the current applicable discount info, so they will be available in the future. - orderPart.AdditionalData.SetDiscountsByProduct(cartViewModel - .Lines - .Where(line => line.AdditionalData.GetDiscounts().Any()) - .ToDictionary( - line => line.ProductSku, - line => line.AdditionalData.GetDiscounts())); - }); - - await _contentManager.CreateAsync(order); - - return order; - } - - private async Task UpdateOrderWithDriversAsync(ContentItem order) - { - await _contentItemDisplayManager.UpdateEditorAsync(order, _updateModelAccessor.ModelUpdater, isNew: false); - return _updateModelAccessor.ModelUpdater.GetModelErrorMessages().Any(); - } -} diff --git a/src/Modules/OrchardCore.Commerce/Services/PriceProvider.cs b/src/Modules/OrchardCore.Commerce/Services/PriceProvider.cs index 63d32c19b..83668941e 100644 --- a/src/Modules/OrchardCore.Commerce/Services/PriceProvider.cs +++ b/src/Modules/OrchardCore.Commerce/Services/PriceProvider.cs @@ -1,4 +1,5 @@ using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Extensions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.MoneyDataType.Abstractions; diff --git a/src/Modules/OrchardCore.Commerce/Services/PriceService.cs b/src/Modules/OrchardCore.Commerce/Services/PriceService.cs index c3eefeac5..4f22e5215 100644 --- a/src/Modules/OrchardCore.Commerce/Services/PriceService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/PriceService.cs @@ -1,5 +1,5 @@ using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using System.Collections.Generic; using System.Linq; diff --git a/src/Modules/OrchardCore.Commerce/Services/PriceVariantProvider.cs b/src/Modules/OrchardCore.Commerce/Services/PriceVariantProvider.cs index 6901937f4..505119b76 100644 --- a/src/Modules/OrchardCore.Commerce/Services/PriceVariantProvider.cs +++ b/src/Modules/OrchardCore.Commerce/Services/PriceVariantProvider.cs @@ -1,4 +1,5 @@ using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Extensions; using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement; diff --git a/src/Modules/OrchardCore.Commerce/Services/ProductAttributeProvider.cs b/src/Modules/OrchardCore.Commerce/Services/ProductAttributeProvider.cs index 6d937ec23..69b66e5ad 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ProductAttributeProvider.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ProductAttributeProvider.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Fields; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.ProductAttributeValues; diff --git a/src/Modules/OrchardCore.Commerce/Services/ProductInventoryService.cs b/src/Modules/OrchardCore.Commerce/Services/ProductInventoryService.cs index 9b92a65ed..248da132e 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ProductInventoryService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ProductInventoryService.cs @@ -1,5 +1,5 @@ using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Services/ProductService.cs b/src/Modules/OrchardCore.Commerce/Services/ProductService.cs index 4d4595f57..0979f08a1 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ProductService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ProductService.cs @@ -1,4 +1,5 @@ using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Indexes; using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement; diff --git a/src/Modules/OrchardCore.Commerce/Services/RegionService.cs b/src/Modules/OrchardCore.Commerce/Services/RegionService.cs index 04f0f02ce..1102c8d49 100644 --- a/src/Modules/OrchardCore.Commerce/Services/RegionService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/RegionService.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Localization; -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.AddressDataType; using OrchardCore.Commerce.Models; using OrchardCore.Entities; diff --git a/src/Modules/OrchardCore.Commerce/Services/SessionShoppingCartPersistence.cs b/src/Modules/OrchardCore.Commerce/Services/SessionShoppingCartPersistence.cs index 037e243a6..4631dd035 100644 --- a/src/Modules/OrchardCore.Commerce/Services/SessionShoppingCartPersistence.cs +++ b/src/Modules/OrchardCore.Commerce/Services/SessionShoppingCartPersistence.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Http; using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs index 12f5fd88e..8822a6967 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs @@ -1,14 +1,16 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Localization; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Exceptions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.AddressDataType; -using OrchardCore.Commerce.Exceptions; using OrchardCore.Commerce.Extensions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.MoneyDataType.Extensions; -using OrchardCore.Commerce.ProductAttributeValues; -using OrchardCore.Commerce.ViewModels; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -19,7 +21,9 @@ namespace OrchardCore.Commerce.Services; public class ShoppingCartHelpers : IShoppingCartHelpers { private readonly IHttpContextAccessor _hca; + private readonly IPriceSelectionStrategy _priceSelectionStrategy; private readonly IPriceService _priceService; + private readonly IEnumerable _productAttributeProviders; private readonly IEnumerable _productEstimationContextUpdaters; private readonly IProductService _productService; private readonly IEnumerable _shoppingCartEvents; @@ -33,7 +37,9 @@ public class ShoppingCartHelpers : IShoppingCartHelpers Justification = "This service ties together many cart-related features.")] public ShoppingCartHelpers( IHttpContextAccessor hca, + IPriceSelectionStrategy priceSelectionStrategy, IPriceService priceService, + IEnumerable productAttributeProviders, IEnumerable productEstimationContextUpdaters, IProductService productService, IEnumerable shoppingCartEvents, @@ -42,7 +48,9 @@ public ShoppingCartHelpers( IHtmlLocalizer localizer) { _hca = hca; + _priceSelectionStrategy = priceSelectionStrategy; _priceService = priceService; + _productAttributeProviders = productAttributeProviders; _productEstimationContextUpdaters = productEstimationContextUpdaters; _productService = productService; _shoppingCartEvents = shoppingCartEvents; @@ -74,7 +82,7 @@ private async Task CreateShoppingCartViewModelAsync( var product = products[item.ProductSku]; var price = _priceService.SelectPrice(item.Prices); - var attributes = item.Attributes.Any(attribute => attribute is RawProductAttributeValue) + var attributes = item.HasRawAttributes() ? _shoppingCartSerializer.PostProcessAttributes(item.Attributes, product) : item.Attributes; @@ -101,7 +109,7 @@ private async Task CreateShoppingCartViewModelAsync( H["Price"], H["Action"], }; - IList totals = (await CalculateMultipleCurrencyTotalsAsync()).Values.ToList(); + IList totals = (await CalculateMultipleCurrencyTotalsAsync(cart)).Values.ToList(); (shipping, billing) = await _hca.GetUserAddressIfNullAsync(shipping, billing); @@ -127,22 +135,27 @@ private async Task CreateShoppingCartViewModelAsync( return model; } - public async Task CalculateSingleCurrencyTotalAsync() + public Task RetrieveAsync(string shoppingCartId) => + _shoppingCartPersistence.RetrieveAsync(shoppingCartId); + + public async Task UpdateAsync(string shoppingCartId, Func updateTask) { - var totals = await CalculateMultipleCurrencyTotalsAsync(); - return totals.Count > 0 ? totals.Single().Value : null; + var cart = await RetrieveAsync(shoppingCartId); + await updateTask(cart); + await _shoppingCartPersistence.StoreAsync(cart, shoppingCartId); } - public async Task> CalculateMultipleCurrencyTotalsAsync() + public async Task CalculateSingleCurrencyTotalAsync(ShoppingCart cart) { - // Shopping cart ID is null by default currently. - var currentShoppingCart = await _shoppingCartPersistence.RetrieveAsync(); - if (currentShoppingCart.Count == 0) return new Dictionary(); - - var totals = await currentShoppingCart.CalculateTotalsAsync(_priceService); - return totals.ToDictionary(total => total.Currency.CurrencyIsoCode); + var totals = await CalculateMultipleCurrencyTotalsAsync(cart); + return totals.Count > 0 ? totals.Single().Value : null; } + public async Task> CalculateMultipleCurrencyTotalsAsync(ShoppingCart cart) => + cart.Count == 0 + ? new Dictionary() + : (await cart.CalculateTotalsAsync(_priceService)).ToDictionary(total => total.Currency.CurrencyIsoCode); + public async Task AddToCartAsync( string shoppingCartId, ShoppingCartItem item, @@ -168,7 +181,7 @@ public async Task AddToCartAsync( } } - if (await ShoppingCartItem.GetErrorAsync(parsedLine.ProductSku, parsedLine, H, _priceService) is { } error) + if (await GetErrorAsync(parsedLine.ProductSku, parsedLine) is { } error) { throw new FrontendException(error); } @@ -203,4 +216,48 @@ private async Task EstimateProductAsync(ProductEstima .Lines .FirstOrDefault(line => line.ProductSku == context.ShoppingCartItem.ProductSku); } + + private async Task GetErrorAsync(string sku, ShoppingCartItem item) + { + if (item is null) + { + return H["Product with SKU {0} not found.", sku]; + } + + item = (await _priceService.AddPricesAsync(new[] { item })).Single(); + + return item.Prices.Any() + ? null + : H["Can't add product {0} because it doesn't have a price, or its currency doesn't match the current display currency.", sku]; + } + + public async Task> CreateOrderLineItemsAsync(ShoppingCart shoppingCart) + { + static string TrimSku(ShoppingCartItem item) => item.ProductSku.Split('-')[0]; + + var contentItems = (await _productService.GetProductsAsync(shoppingCart.Items.Select(TrimSku))) + .ToDictionary(item => item.Sku, item => item.ContentItem); + + return await shoppingCart.Items.AwaitEachAsync(async item => + { + item = await _priceService.AddPriceAsync(item); + var price = _priceSelectionStrategy.SelectPrice(item.Prices); + var fullSku = _productService.GetOrderFullSku(item, await _productService.GetProductAsync(item.ProductSku)); + + var selectedAttributes = _productAttributeProviders + .Select(provider => provider.GetSelectedAttributes(item.Attributes)) + .Where(attributesByType => attributesByType.Any()) + .ToDictionary(attributesByType => attributesByType.Keys.First(), attributesByType => attributesByType.Values.First()); + + return new OrderLineItem( + item.Quantity, + item.ProductSku, + fullSku, + price, + item.Quantity * price, + contentItems[TrimSku(item)].ContentItemVersionId, + item.Attributes, + selectedAttributes); + }); + } } diff --git a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartPersistenceBase.cs b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartPersistenceBase.cs index f86d1c6a1..3c60272d1 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartPersistenceBase.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartPersistenceBase.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -19,7 +19,7 @@ public abstract class ShoppingCartPersistenceBase : IShoppingCartPersistence protected ShoppingCartPersistenceBase(IEnumerable shoppingCartEvents) => _shoppingCartEvents = shoppingCartEvents; - public async Task RetrieveAsync(string shoppingCartId = null) + public async Task RetrieveAsync(string shoppingCartId) { var key = GetCacheId(shoppingCartId); @@ -39,9 +39,9 @@ public async Task RetrieveAsync(string shoppingCartId = null) return cart; } - public async Task StoreAsync(ShoppingCart items, string shoppingCartId = null) + public async Task StoreAsync(ShoppingCart items) { - var key = GetCacheId(shoppingCartId); + var key = GetCacheId(items.Id); if (await StoreInnerAsync(key, items)) { diff --git a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartSerializer.cs b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartSerializer.cs index f16d76918..39c305084 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartSerializer.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartSerializer.cs @@ -1,7 +1,9 @@ using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.ProductAttributeValues; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.MoneyDataType.Abstractions; -using OrchardCore.Commerce.ProductAttributeValues; using OrchardCore.Commerce.ViewModels; using OrchardCore.ContentManagement.Metadata; using OrchardCore.ContentManagement.Metadata.Models; @@ -115,11 +117,12 @@ public ISet PostProcessAttributes(IEnumerable>() .SelectWhere(attribute => - attribute is RawProductAttributeValue rawAttribute && - type.GetFieldDefinition(rawAttribute.AttributeName) is ({ } partDefinition, { } fieldDefinition) + attribute.IsRaw() && + type.GetFieldDefinition(attribute.AttributeName) is ({ } partDefinition, { } fieldDefinition) ? _attributeProviders - .SelectWhere(provider => provider.CreateFromValue(partDefinition, fieldDefinition, rawAttribute.Value)) + .SelectWhere(provider => provider.CreateFromValue(partDefinition, fieldDefinition, attribute.Value)) .FirstOrDefault() : attribute) .ToHashSet(); diff --git a/src/Modules/OrchardCore.Commerce/Services/SimplePriceStrategy.cs b/src/Modules/OrchardCore.Commerce/Services/SimplePriceStrategy.cs index f2dcfbbeb..ef0204fba 100644 --- a/src/Modules/OrchardCore.Commerce/Services/SimplePriceStrategy.cs +++ b/src/Modules/OrchardCore.Commerce/Services/SimplePriceStrategy.cs @@ -1,5 +1,5 @@ using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using System.Collections.Generic; using System.Linq; diff --git a/src/Modules/OrchardCore.Commerce/Services/StripePaymentService.cs b/src/Modules/OrchardCore.Commerce/Services/StripePaymentService.cs deleted file mode 100644 index ca2efe88a..000000000 --- a/src/Modules/OrchardCore.Commerce/Services/StripePaymentService.cs +++ /dev/null @@ -1,416 +0,0 @@ -using Lombiq.HelpfulLibraries.OrchardCore.Workflow; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Logging; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Activities; -using OrchardCore.Commerce.Constants; -using OrchardCore.Commerce.Extensions; -using OrchardCore.Commerce.Indexes; -using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.MoneyDataType; -using OrchardCore.Commerce.MoneyDataType.Abstractions; -using OrchardCore.Commerce.MoneyDataType.Extensions; -using OrchardCore.Commerce.Promotion.Extensions; -using OrchardCore.Commerce.ViewModels; -using OrchardCore.ContentFields.Fields; -using OrchardCore.ContentManagement; -using OrchardCore.ContentManagement.Display; -using OrchardCore.DisplayManagement.ModelBinding; -using OrchardCore.Entities; -using OrchardCore.Mvc.Utilities; -using OrchardCore.Settings; -using OrchardCore.Workflows.Services; -using Stripe; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using YesSql; - -namespace OrchardCore.Commerce.Services; - -public class StripePaymentService : IStripePaymentService -{ - private readonly IShoppingCartHelpers _shoppingCartHelpers; - private readonly IShoppingCartPersistence _shoppingCartPersistence; - private readonly PaymentIntentService _paymentIntentService; - private readonly IContentManager _contentManager; - private readonly IStringLocalizer T; - private readonly ISession _session; - private readonly IPaymentIntentPersistence _paymentIntentPersistence; - private readonly RequestOptions _requestOptions; - private readonly string _siteName; - private readonly IContentItemDisplayManager _contentItemDisplayManager; - private readonly IProductInventoryService _productInventoryService; - private readonly IEnumerable _workflowManagers; - private readonly IPriceSelectionStrategy _priceSelectionStrategy; - private readonly IPriceService _priceService; - private readonly IProductService _productService; - private readonly IMoneyService _moneyService; - private readonly IEnumerable _productAttributeProviders; - - // We need to use that many, this cannot be avoided. -#pragma warning disable S107 // Methods should not have too many parameters - public StripePaymentService( - IShoppingCartHelpers shoppingCartHelpers, - IShoppingCartPersistence shoppingCartPersistence, - IContentManager contentManager, - ISiteService siteService, - IDataProtectionProvider dataProtectionProvider, - ILogger logger, - IStringLocalizer stringLocalizer, - ISession session, - IPaymentIntentPersistence paymentIntentPersistence, - IContentItemDisplayManager contentItemDisplayManager, - IProductInventoryService productInventoryService, - IEnumerable workflowManagers, - IPriceSelectionStrategy priceSelectionStrategy, - IPriceService priceService, - IProductService productService, - IMoneyService moneyService, - IEnumerable productAttributeProviders) -#pragma warning restore S107 // Methods should not have too many parameters - { - _paymentIntentService = new PaymentIntentService(); - _shoppingCartHelpers = shoppingCartHelpers; - _shoppingCartPersistence = shoppingCartPersistence; - _contentManager = contentManager; - _session = session; - _paymentIntentPersistence = paymentIntentPersistence; - _productInventoryService = productInventoryService; - _priceSelectionStrategy = priceSelectionStrategy; - _priceService = priceService; - _productService = productService; - _moneyService = moneyService; - _productAttributeProviders = productAttributeProviders; - T = stringLocalizer; - _contentItemDisplayManager = contentItemDisplayManager; - _workflowManagers = workflowManagers; - - var siteSettings = siteService.GetSiteSettingsAsync() - .GetAwaiter() - .GetResult(); - _siteName = siteSettings.SiteName; - _requestOptions = - new RequestOptions - { - ApiKey = siteSettings - .As() - .SecretKey - .DecryptStripeApiKey(dataProtectionProvider, logger), - }; - } - - public async Task InitializePaymentIntentAsync(string paymentIntentId) - { - var orderPart = (await GetOrderByPaymentIntentIdAsync(paymentIntentId))?.As(); - var totals = CheckTotals( - await _shoppingCartHelpers.CreateShoppingCartViewModelAsync( - shoppingCartId: null, - orderPart?.ShippingAddress.Address, - orderPart?.BillingAddress.Address)); - - // Same here as on the checkout page: Later we have to figure out what to do if there are multiple - // totals i.e., multiple currencies. https://github.com/OrchardCMS/OrchardCore.Commerce/issues/132 - var defaultTotal = totals.SingleOrDefault(); - long amountForPayment = GetPaymentAmount(defaultTotal); - - return string.IsNullOrEmpty(paymentIntentId) - ? await CreatePaymentIntentAsync(amountForPayment, defaultTotal) - : await GetOrUpdatePaymentIntentAsync(paymentIntentId, amountForPayment, defaultTotal); - } - - public Task GetPaymentIntentAsync(string paymentIntentId) - { - var paymentIntentGetOptions = new PaymentIntentGetOptions(); - paymentIntentGetOptions.AddExpansions(); - return _paymentIntentService.GetAsync( - paymentIntentId, - paymentIntentGetOptions, - _requestOptions.SetIdempotencyKey()); - } - - public async Task UpdateOrderToOrderedAsync(PaymentIntent paymentIntent = null, ContentItem orderItem = null) - { - var order = paymentIntent != null - ? await GetOrderByPaymentIntentIdAsync(paymentIntent.Id) - : orderItem; - - order.Alter(orderPart => - { - if (paymentIntent != null) - { - // Same here as on the checkout page: Later we have to figure out what to do if there are multiple - // totals i.e., multiple currencies. https://github.com/OrchardCMS/OrchardCore.Commerce/issues/132 - var orderPartCharge = orderPart.Charges.SingleOrDefault(); - var amount = orderPartCharge!.Amount; - - var payment = new Payment - { - Kind = paymentIntent.PaymentMethod.GetFormattedPaymentType(), - ChargeText = paymentIntent.Description, - TransactionId = paymentIntent.Id, - Amount = amount, - CreatedUtc = paymentIntent.Created, - }; - - orderPart.Charges.Clear(); - orderPart.Charges.Add(payment); - } - - orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.Ordered.HtmlClassify() }; - }); - - await _workflowManagers.TriggerContentItemEventAsync(order); - - var currentShoppingCart = await _shoppingCartPersistence.RetrieveAsync(); - - // Decrease inventories of purchased items. - await _productInventoryService.UpdateInventoriesAsync(currentShoppingCart.Items); - - await _contentManager.UpdateAsync(order); - } - - public async Task UpdateOrderToPaymentFailedAsync(PaymentIntent paymentIntent) - { - var order = await GetOrderByPaymentIntentIdAsync(paymentIntent.Id); - order.Alter(orderPart => - orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.PaymentFailed.HtmlClassify() }); - - await _contentManager.UpdateAsync(order); - } - - public Task GetOrderPaymentByPaymentIntentIdAsync(string paymentIntentId) => - _session - .Query(index => index.PaymentIntentId == paymentIntentId) - .FirstOrDefaultAsync(); - - public async Task CreateOrUpdateOrderFromShoppingCartAsync( - PaymentIntent paymentIntent, - IUpdateModelAccessor updateModelAccessor) - { - var currentShoppingCart = await _shoppingCartPersistence.RetrieveAsync(); - var orderId = (await GetOrderPaymentByPaymentIntentIdAsync(paymentIntent.Id))?.OrderId; - - if (!string.IsNullOrEmpty(orderId) && await _contentManager.GetAsync(orderId) is { } order) - { - // If there are line items in the Order, use data from Order instead of shopping cart. - if (!order.As().LineItems.Any() && - currentShoppingCart.Items.Any() && - await UpdateOrderWithDriversAsync(order, updateModelAccessor)) - { - return null; - } - } - else - { - order = await _contentManager.NewAsync(Constants.ContentTypes.Order); - if (await UpdateOrderWithDriversAsync(order, updateModelAccessor)) - { - return null; - } - - _session.Save(new OrderPayment - { - OrderId = order.ContentItemId, - PaymentIntentId = paymentIntent.Id, - }); - } - - var orderPart = order.As(); - - var lineItems = await CreateOrderLineItemsAsync(currentShoppingCart); - if (!lineItems.Any()) - { - lineItems = orderPart.LineItems; - } - - var cartViewModel = await _shoppingCartHelpers.CreateShoppingCartViewModelAsync( - shoppingCartId: null, - orderPart.ShippingAddress.Address, - orderPart.BillingAddress.Address); - - var currency = orderPart.LineItems.Any() ? orderPart.LineItems[0].LinePrice.Currency : _moneyService.DefaultCurrency; - var orderTotals = new Amount(0, currency); - - if (cartViewModel is null) - { - orderTotals = orderPart.LineItems.Select(item => item.LinePrice).Sum(); - } - - // If there is no cart, use current Order's data. - var defaultTotal = cartViewModel is null ? orderTotals : CheckTotals(cartViewModel).SingleOrDefault(); - AlterOrder(order, paymentIntent, defaultTotal, cartViewModel, lineItems); - - if (string.IsNullOrEmpty(orderId)) - { - await _contentManager.CreateAsync(order); - } - else - { - await _contentManager.UpdateAsync(order); - } - - return order; - } - - public long GetPaymentAmount(Amount total) - { - if (CurrencyCollectionConstants.ZeroDecimalCurrencies.Contains(total.Currency.CurrencyIsoCode)) - { - return (long)Math.Round(total.Value); - } - - return CurrencyCollectionConstants.SpecialCases.Contains(total.Currency.CurrencyIsoCode) - ? (long)Math.Round(total.Value / 100m) * 10000 - : (long)Math.Round(total.Value * 100); - } - - private async Task UpdateOrderWithDriversAsync(ContentItem order, IUpdateModelAccessor updateModelAccessor) - { - await _contentItemDisplayManager.UpdateEditorAsync(order, updateModelAccessor.ModelUpdater, isNew: false); - - return updateModelAccessor.ModelUpdater.GetModelErrorMessages().Any(); - } - - public async Task CreatePaymentIntentAsync(long amountForPayment, Amount total) - { - var paymentIntentOptions = new PaymentIntentCreateOptions - { - Amount = amountForPayment, - Currency = total.Currency.CurrencyIsoCode, - Description = T["User checkout on {0}", _siteName].Value, - AutomaticPaymentMethods = new PaymentIntentAutomaticPaymentMethodsOptions { Enabled = true, }, - }; - - var paymentIntent = await _paymentIntentService.CreateAsync( - paymentIntentOptions, - _requestOptions.SetIdempotencyKey()); - - _paymentIntentPersistence.Store(paymentIntent.Id); - - return paymentIntent; - } - - public async Task> CreateOrderLineItemsAsync(ShoppingCart shoppingCart) - { - var lineItems = new List(); - - // This needs to be done separately because it's async: "Avoid using async lambda when delegate type returns - // void." - foreach (var item in shoppingCart.Items) - { - var trimmedSku = item.ProductSku.Split('-')[0]; - - var contentItemId = (await _session - .QueryIndex(productPartIndex => productPartIndex.Sku == trimmedSku) - .ListAsync()) - .Select(index => index.ContentItemId) - .First(); - - var contentItemVersion = (await _contentManager.GetAsync(contentItemId)).ContentItemVersionId; - - var selectedAttributes = _productAttributeProviders - .Select(provider => provider.GetSelectedAttributes(item.Attributes)) - .Where(attributesByType => attributesByType.Any()) - .ToDictionary(attributesByType => attributesByType.Keys.First(), attributesByType => attributesByType.Values.First()); - - lineItems.Add(await item.CreateOrderLineFromShoppingCartItemAsync( - _priceSelectionStrategy, - _priceService, - _productService, - contentItemVersion, - selectedAttributes)); - } - - return lineItems; - } - - private static void AlterOrder( - ContentItem order, - PaymentIntent paymentIntent, - Amount defaultTotal, - ShoppingCartViewModel cartViewModel, - IEnumerable lineItems) - { - order.Alter(orderPart => - { - orderPart.Charges.Clear(); - orderPart.Charges.Add( - new Payment - { - ChargeText = paymentIntent.Description, - TransactionId = paymentIntent.Id, - Amount = defaultTotal, - CreatedUtc = paymentIntent.Created, - }); - - if (cartViewModel is not null) - { - // Shopping cart - orderPart.LineItems.SetItems(lineItems); - orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.Pending.HtmlClassify() }; - - // Store the current applicable discount info so they will be available in the future. - orderPart.AdditionalData.SetDiscountsByProduct(cartViewModel - .Lines - .Where(line => line.AdditionalData.GetDiscounts().Any()) - .ToDictionary( - line => line.ProductSku, - line => line.AdditionalData.GetDiscounts())); - } - }); - - order.Alter(part => part.PaymentIntentId = new TextField { ContentItem = order, Text = paymentIntent.Id }); - } - - private async Task GetOrUpdatePaymentIntentAsync( - string paymentIntentId, - long amountForPayment, - Amount defaultTotal) - { - var paymentIntent = await GetPaymentIntentAsync(paymentIntentId); - - if (paymentIntent?.Status is PaymentIntentStatuses.Succeeded or PaymentIntentStatuses.Processing) - { - return paymentIntent; - } - - return await UpdatePaymentIntentAsync(paymentIntentId, amountForPayment, defaultTotal); - } - - private Task UpdatePaymentIntentAsync( - string paymentIntentId, - long amountForPayment, - Amount defaultTotal) - { - var updateOptions = new PaymentIntentUpdateOptions - { - Amount = amountForPayment, - Currency = defaultTotal.Currency.CurrencyIsoCode, - }; - - updateOptions.AddExpansions(); - return _paymentIntentService.UpdateAsync( - paymentIntentId, - updateOptions, - _requestOptions.SetIdempotencyKey()); - } - - private static IList CheckTotals(ShoppingCartViewModel viewModel) - { - if (!viewModel.Totals.Any()) - { - throw new InvalidOperationException("Cannot create a payment without shopping cart total(s)!"); - } - - return viewModel.Totals; - } - - private async Task GetOrderByPaymentIntentIdAsync(string paymentIntentId) - { - var orderId = (await GetOrderPaymentByPaymentIntentIdAsync(paymentIntentId))?.OrderId; - return await _contentManager.GetAsync(orderId); - } -} diff --git a/src/Modules/OrchardCore.Commerce/Services/TextProductAttributeDeserializer.cs b/src/Modules/OrchardCore.Commerce/Services/TextProductAttributeDeserializer.cs new file mode 100644 index 000000000..dd1318e5d --- /dev/null +++ b/src/Modules/OrchardCore.Commerce/Services/TextProductAttributeDeserializer.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json.Linq; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.ProductAttributeValues; +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace OrchardCore.Commerce.Services; + +public class TextProductAttributeDeserializer : IProductAttributeDeserializer +{ + public string AttributeTypeName => nameof(TextProductAttributeValue); + + public IProductAttributeValue Deserialize(string attributeName, JObject attribute) => + new TextProductAttributeValue(attributeName, attribute.Get>("value")); + + public IProductAttributeValue Deserialize(string attributeName, JsonObject attribute) => + new TextProductAttributeValue(attributeName, attribute["value"].GetValue>()); +} + +public class BooleanProductAttributeDeserializer : IProductAttributeDeserializer +{ + public string AttributeTypeName => nameof(BooleanProductAttributeValue); + + public IProductAttributeValue Deserialize(string attributeName, JObject attribute) => + new BooleanProductAttributeValue(attributeName, attribute.Get("value")); + + public IProductAttributeValue Deserialize(string attributeName, JsonObject attribute) => + new BooleanProductAttributeValue(attributeName, attribute["value"].GetValue()); +} + +public class NumericProductAttributeDeserializer : IProductAttributeDeserializer +{ + public string AttributeTypeName => nameof(NumericProductAttributeValue); + + public IProductAttributeValue Deserialize(string attributeName, JObject attribute) => + new NumericProductAttributeValue(attributeName, attribute.Get("value")); + + public IProductAttributeValue Deserialize(string attributeName, JsonObject attribute) => + new NumericProductAttributeValue(attributeName, attribute["value"].GetValue()); +} diff --git a/src/Modules/OrchardCore.Commerce/Services/TextProductAttributeProvider.cs b/src/Modules/OrchardCore.Commerce/Services/TextProductAttributeProvider.cs index 17626f1b3..8e7826948 100644 --- a/src/Modules/OrchardCore.Commerce/Services/TextProductAttributeProvider.cs +++ b/src/Modules/OrchardCore.Commerce/Services/TextProductAttributeProvider.cs @@ -1,4 +1,5 @@ using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.ProductAttributeValues; using OrchardCore.Commerce.Settings; diff --git a/src/Modules/OrchardCore.Commerce/Services/TieredPriceProvider.cs b/src/Modules/OrchardCore.Commerce/Services/TieredPriceProvider.cs index c2c400c73..08eaecb13 100644 --- a/src/Modules/OrchardCore.Commerce/Services/TieredPriceProvider.cs +++ b/src/Modules/OrchardCore.Commerce/Services/TieredPriceProvider.cs @@ -1,4 +1,5 @@ using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Extensions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.MoneyDataType.Abstractions; diff --git a/src/Modules/OrchardCore.Commerce/Services/UserService.cs b/src/Modules/OrchardCore.Commerce/Services/UserService.cs index 8e9d9be2b..e24fea571 100644 --- a/src/Modules/OrchardCore.Commerce/Services/UserService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/UserService.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; using Newtonsoft.Json.Linq; -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.ContentManagement; using OrchardCore.Users; using OrchardCore.Users.Models; diff --git a/src/Modules/OrchardCore.Commerce/Startup.cs b/src/Modules/OrchardCore.Commerce/Startup.cs index ee4ef9c4f..1482fe9d1 100644 --- a/src/Modules/OrchardCore.Commerce/Startup.cs +++ b/src/Modules/OrchardCore.Commerce/Startup.cs @@ -7,9 +7,15 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Fields; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Abstractions.TagHelpers; +using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.Activities; using OrchardCore.Commerce.AddressDataType; using OrchardCore.Commerce.AddressDataType.Abstractions; +using OrchardCore.Commerce.ContentFields.Events; using OrchardCore.Commerce.Controllers; using OrchardCore.Commerce.Drivers; using OrchardCore.Commerce.Events; @@ -25,7 +31,6 @@ using OrchardCore.Commerce.Promotion.Models; using OrchardCore.Commerce.Services; using OrchardCore.Commerce.Settings; -using OrchardCore.Commerce.TagHelpers; using OrchardCore.Commerce.Tax.Constants; using OrchardCore.Commerce.Tax.Models; using OrchardCore.Commerce.ViewModels; @@ -61,14 +66,12 @@ public override void ConfigureServices(IServiceCollection services) services.AddTagHelpers(); services.AddTransient, ResourceManagementOptionsConfiguration>(); services.AddScoped(); - services.AddScoped(); // Product services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddContentPart() @@ -137,9 +140,7 @@ public override void ConfigureServices(IServiceCollection services) .UseDisplayDriver() .WithMigration(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); - services.AddScoped(); // Orders services.AddContentPart() @@ -148,19 +149,12 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); - services.AddContentField() - .UseDisplayDriver(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - // Checkout - services.AddScoped(); - // Region services.AddScoped(); @@ -169,25 +163,13 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped, CurrencySettingsDisplayDriver>(); services.AddScoped(); services.AddTransient, CurrencySettingsConfiguration>(); - services.AddScoped, StripeApiSettingsDisplayDriver>(); services.AddScoped, PriceDisplaySettingsDisplayDriver>(); - services.AddTransient, StripeApiSettingsConfiguration>(); services.AddScoped, RegionSettingsDisplayDriver>(); services.AddTransient, RegionSettingsConfiguration>(); // Page services.AddScoped(); - // Promotion - services.AddScoped(); - - // Stripe payments - services.AddContentPart(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); - // Exposing models to liquid templates services.Configure(option => { @@ -221,6 +203,10 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddContentPart() .UseDisplayDriver(); + IProductAttributeDeserializer.AddSerializers( + new TextProductAttributeDeserializer(), + new BooleanProductAttributeDeserializer(), + new NumericProductAttributeDeserializer()); } } @@ -235,6 +221,9 @@ public override void ConfigureServices(IServiceCollection services) services.AddActivity(); services.AddActivity(); services.AddActivity(); + + services.AddScoped(); + services.AddScoped(); } } @@ -308,6 +297,10 @@ public class PromotionStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services .AddContentPart() .AddHandler() @@ -350,7 +343,10 @@ public override void ConfigureServices(IServiceCollection services) .AddContentPart() .WithMigration(); + services.AddScoped(); services.AddScoped, UserAddressesUserDisplayDriver>(); + services.AddScoped(); + services.AddScoped(); } public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) @@ -367,8 +363,12 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro [RequireFeatures(Inventory.Constants.FeatureIds.Inventory)] public class InventoryStartup : StartupBase { - public override void ConfigureServices(IServiceCollection services) => + public override void ConfigureServices(IServiceCollection services) + { services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } } [RequireFeatures("OrchardCore.ContentLocalization")] diff --git a/src/Modules/OrchardCore.Commerce/ViewModels/CheckoutViewModel.cs b/src/Modules/OrchardCore.Commerce/ViewModels/CheckoutViewModel.cs deleted file mode 100644 index 3bc81ef63..000000000 --- a/src/Modules/OrchardCore.Commerce/ViewModels/CheckoutViewModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.Rendering; -using OrchardCore.Commerce.Models; -using OrchardCore.Commerce.MoneyDataType; -using OrchardCore.DisplayManagement; -using OrchardCore.DisplayManagement.Views; -using System.Collections.Generic; - -namespace OrchardCore.Commerce.ViewModels; - -public class CheckoutViewModel : ShapeViewModel -{ - public string ShoppingCartId { get; init; } - public Amount SingleCurrencyTotal { get; init; } - public Amount NetTotal { get; init; } - public Amount GrossTotal { get; init; } - public OrderPart OrderPart { get; init; } - public string PaymentIntentClientSecret { get; init; } - - [BindNever] - public IEnumerable Regions { get; set; } - - [BindNever] - public IDictionary> Provinces { get; } = - new Dictionary>(); - public string StripePublishableKey { get; init; } - public string UserEmail { get; init; } - public IEnumerable CheckoutShapes { get; init; } - - public CheckoutViewModel() => Metadata.Type = "Checkout"; -} diff --git a/src/Modules/OrchardCore.Commerce/ViewModels/OrderLineItemViewModel.cs b/src/Modules/OrchardCore.Commerce/ViewModels/OrderLineItemViewModel.cs index d53e85b00..47bb5ae93 100644 --- a/src/Modules/OrchardCore.Commerce/ViewModels/OrderLineItemViewModel.cs +++ b/src/Modules/OrchardCore.Commerce/ViewModels/OrderLineItemViewModel.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; -using OrchardCore.Commerce.Abstractions; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Settings; diff --git a/src/Modules/OrchardCore.Commerce/ViewModels/OrderPartViewModel.cs b/src/Modules/OrchardCore.Commerce/ViewModels/OrderPartViewModel.cs index c5abfa139..36d171d7e 100644 --- a/src/Modules/OrchardCore.Commerce/ViewModels/OrderPartViewModel.cs +++ b/src/Modules/OrchardCore.Commerce/ViewModels/OrderPartViewModel.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; -using OrchardCore.Commerce.Abstractions; -using OrchardCore.Commerce.Models; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.ContentManagement; using System.Collections.Generic; diff --git a/src/Modules/OrchardCore.Commerce/ViewModels/ProductPartViewModel.cs b/src/Modules/OrchardCore.Commerce/ViewModels/ProductPartViewModel.cs index 50821d9f6..c37fd12f7 100644 --- a/src/Modules/OrchardCore.Commerce/ViewModels/ProductPartViewModel.cs +++ b/src/Modules/OrchardCore.Commerce/ViewModels/ProductPartViewModel.cs @@ -1,11 +1,12 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; +using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement; using System.Collections.Generic; namespace OrchardCore.Commerce.ViewModels; -public class ProductPartViewModel +public class ProductPartViewModel : ISkuHolderContent { public string Sku { get; set; } diff --git a/src/Modules/OrchardCore.Commerce/Views/Checkout.cshtml b/src/Modules/OrchardCore.Commerce/Views/Checkout.cshtml index e5972cb28..7bf96b7c0 100644 --- a/src/Modules/OrchardCore.Commerce/Views/Checkout.cshtml +++ b/src/Modules/OrchardCore.Commerce/Views/Checkout.cshtml @@ -1,4 +1,8 @@ @using OrchardCore.Commerce.Controllers; +@using OrchardCore +@using OrchardCore.Mvc.Utilities +@using static OrchardCore.Commerce.Constants.ResourceNames +@using static OrchardCore.Commerce.Payment.Constants.ResourceNames @model CheckoutViewModel @@ -7,11 +11,14 @@ var regions = Model.Regions.AsList(); var provinces = Model.Provinces; + + var actionUrl = Orchard.Action(controller => + controller.CheckoutWithoutPayment(Model.ShoppingCartId)); }
-
+ @Html.AntiForgeryToken()
@@ -42,26 +49,26 @@

@T["Billing Address"]

@await DisplayAsync( - new AddressFieldEditorViewModel - { - AddressField = orderPart.BillingAddress, - CityName = Html.NameFor(model => model.OrderPart.BillingAddress.Address.City), - Regions = regions, - Provinces = provinces, - }) + new AddressFieldEditorViewModel + { + AddressField = orderPart.BillingAddress, + CityName = Html.NameFor(model => model.OrderPart.BillingAddress.Address.City), + Regions = regions, + Provinces = provinces, + })

@T["Shipping Address"]

@await DisplayAsync( - new AddressFieldEditorViewModel - { - AddressField = orderPart.ShippingAddress, - CityName = Html.NameFor(model => model.OrderPart.ShippingAddress.Address.City), - Regions = regions, - Provinces = provinces, - }) + new AddressFieldEditorViewModel + { + AddressField = orderPart.ShippingAddress, + CityName = Html.NameFor(model => model.OrderPart.ShippingAddress.Address.City), + Regions = regions, + Provinces = provinces, + })
@@ -92,12 +99,19 @@ @* If totals are zero, there's no need for payment. *@ @if (Model.SingleCurrencyTotal.Value > 0) { - +
+ @foreach (var (providerName, data) in Model.PaymentProviderData) + { + var shapeName = "Checkout" + providerName; +
+ +
+ } +
} else { @@ -113,10 +127,10 @@
- + - - +