From 546baa1a5ded49d9fbe72ba8a013931bb09f6f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 20 Jan 2024 20:49:35 +0100 Subject: [PATCH 01/90] Add OrchardCore.Commerce.Payment.Exactly project. --- OrchardCore.Commerce.sln | 7 ++++ .../License.md | 21 ++++++++++ ...rchardCore.Commerce.Payment.Exactly.csproj | 39 ++++++++++++++++++ .../OrchardCoreIcon.png | Bin 0 -> 2785 bytes .../Readme.md | 7 ++++ 5 files changed, 74 insertions(+) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/License.md create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCoreIcon.png create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Readme.md diff --git a/OrchardCore.Commerce.sln b/OrchardCore.Commerce.sln index 59e893909..fb171a3d4 100644 --- a/OrchardCore.Commerce.sln +++ b/OrchardCore.Commerce.sln @@ -109,6 +109,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Commerce.Paymen 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Commerce.Payment.Exactly", "src\Modules\OrchardCore.Commerce.Payment.Exactly\OrchardCore.Commerce.Payment.Exactly.csproj", "{73925C09-BF96-4727-91D8-57A88AD1601F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -171,6 +173,10 @@ Global {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 + {73925C09-BF96-4727-91D8-57A88AD1601F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73925C09-BF96-4727-91D8-57A88AD1601F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73925C09-BF96-4727-91D8-57A88AD1601F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73925C09-BF96-4727-91D8-57A88AD1601F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -199,6 +205,7 @@ Global {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} + {73925C09-BF96-4727-91D8-57A88AD1601F} = {E6C02BDF-EEB0-4ABD-ADEC-9932F60923AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {456CBC78-579D-483F-A4C3-AF5C12AB3324} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/License.md b/src/Modules/OrchardCore.Commerce.Payment.Exactly/License.md new file mode 100644 index 000000000..798b9cdec --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/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.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj b/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj new file mode 100644 index 000000000..aac228693 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj @@ -0,0 +1,39 @@ + + + + net6.0 + true + + + + Orchard Core Commerce - Payment - Exactly + Bertrand Le Roy + Copyright © 2018 .NET Foundation + Exactly payment provider for Orchard Core Commerce + OrchardCore;OrchardCore.Commerce;Commerce;e-Commerce;Payment;Exactly + https://github.com/OrchardCMS/OrchardCore.Commerce + https://github.com/OrchardCMS/OrchardCore.Commerce/blob/main/src/Modules/OrchardCore.Commerce.Payment.Exactly/Readme.md + License.md + OrchardCoreIcon.png + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCoreIcon.png b/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCoreIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..c72b029aca6c9f96150cfedb46289ca64b99e24e GIT binary patch literal 2785 zcmV<73Lf=|P)Px#1ZP1_K>z@;j|==^1poj54^T{0MF0Q*K(lZ_wQxbUa6z_kK;PY*00004bW%=J z06>7hqg34t00009a7bBm001(;001(;0Vrfz+W-IxOG!jQRCr$1oza5oDhx%BdjJ2M zFW6n%LK1c$(9U|AQz&GSfR1{cf3tbLUhnrO@!ya1k9XqrLYKeD>vcAgNtS<3k?}h5 zcMM-|kJpdS;{{X35Nj9jH^zA$gJv6_NBTvWZ9L5{&TQjB_1SlVG?AEy^~tT1ll zNjsqQg6s&lNVN>37h%Ud1Wb5fW%PzulyIA%mLc>;?J$f^bVZ(M>y6Fb+HVup5>30I zeb;iERFMZQy#c$1+uXEmOtWhYJUwk2bE0UBx*%;EGwkd!SCr%rdwX=)*=~zUa>owF zzs|T(X0cneommMhOA?rN&Lh(t>jt;R^FUW6S#C>@k~3B@dF*hDM8t0zomh7KiW{Tc z=oS-~QIxm3nRaAh8e8{fH`9(QOnrdc-G+lRiV{8U#3(DGFpazKdJMQQ6rY~B<t9gu@Wrq$5>U|G5C=b*l)6M`o)u8U`IqpduBiFB2*L+==%DyO9L6Nf~qvSe2n=A!X4WtB6t^zKXpj89C zTJ+fA)+<$EDyc&&=t^AkxcuRNe?bnYDoiDH*XgJgsA1*Y$kG-zJ8R)LARn zuFASDarw%&g80W3yL2w#4i{y32TUPQb!o`aRmrV&1@5guyG^k+@`B`+vGXK@TU=S#|~87E3dA#89hXuDeO3u+)I`A5mlIIXoLPcxO(Lka9Dl` zI#bZl2K{$%^@<`VZ3ml})bXPfcZzHKmgSu1Nhah>;RjhO*H@UhcZZiNA{1}@XwH7R z3X_RZq{#VfxjsAG3;67>mUFE8!FBI+%ZUs7(GJlr_}G42gN&8?FI29_?lKjwG2jlc zLat-mT~dH+47dZVkn6DfLvkI5%6TqaV!$0>jhx@^oH|@$z#U+boZIeC$+_)Lli>;j z?f}c=ymo&}&S`h5%jgGjalAn}WiAcel<1ZdmoxOa=P>%RQ&f|1o<(R`VXII&ak+k&u$K{Y*&~f?0 z*7M7u`;O|cL@r_Ion|BJ61{hD^-9j}^b_TWnm)H&xodFw%38UE;)m7*SC`b5`zqk) zD--GxNM9$8rUWO4&O646t=e=aN-T*)x0qqp4RP1l5x zL)RU-DlNIWb!pJ9%Q7_MJXwCCG-#)es>(gR@><yoLb~)X3&Q4g3#*auImU5CDkG*sjK>F zbRHuT+tE_5RaHh>rJ(tM?kE#ckq2M9a>#m}(NH2J1T7_0>H^aS^0e1M>!NhzLyM0% zQ)1J$k?ru~#x~U%G?Yl`(9VPA!$Ex7zNC52A=M$fHd1Hmq7KQL^BeZOo0PcT)XgZ# zox@Iu7%dgDoSHhN`3K>6X(>YY$VRg#d^DFHzRe*WBEKgaXxo^7mQ$)yXR~? z`3pY{w|3$ZdaTN0`IO`-q56ZnVl2)wJMDW~jUu5EXNIaSPpqC6=(w9XhHhuvf|AMYI&J2)zC$m>9iZx(}T3C$>(O5fH~HHp7lNC z=CJ&R+b|c(Ua8Kb#r&3Td&$s=XTYu_eINS^$nEi*MVYY&w1Y`&5AFo08nv}7YSRx<_k=DsT4xB$h^ge%p=<|8M7_UU@ zjR@~Sc_PTJpH^WHxV7!(>NVd#8gesUFxr`CN4zzLp1m}Vs>~!k;;7}etpQ;-LyvhB z8Fovwt0P>GQ5)QEJB(eV9`)oj$c?lM(j$(MOqd&2CW-OC%0{ZiZUfRT%{s8lS+ig_ zhh49q=cBd3ZZ5ktyK{QihlWEQ0!lpdryWlUnx9{nX#FPHi7R zx#PF{^8HT|`InrX%WptFrnx0LLyBTjN$zweFNT?E`x5w`fRo8-+qfxt)6=%$dD$+C zC28AOZnsV?vs7 Date: Sat, 20 Jan 2024 22:09:35 +0100 Subject: [PATCH 02/90] Add some basic services. --- .../Controllers/ExactlyController.cs | 17 +++++++++ .../Models/ExactlySetings.cs | 14 ++++++++ ...rchardCore.Commerce.Payment.Exactly.csproj | 7 ++++ .../Services/ExactlyApiHandler.cs | 21 +++++++++++ .../Services/ExactlyPaymentProvider.cs | 26 ++++++++++++++ .../Services/ExactlyService.cs | 6 ++++ .../Services/IExactlyApi.cs | 20 +++++++++++ .../Startup.cs | 35 +++++++++++++++++++ .../OrchardCore.Commerce.csproj | 1 + 9 files changed, 147 insertions(+) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyApiHandler.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs new file mode 100644 index 000000000..ee4b9ce88 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Build.Utilities; + +namespace OrchardCore.Commerce.Payment.Exactly.Controllers; + +public class ExactlyController : Controller +{ + public async Task CreateTransaction() + { + + } + + public async Task GetRedirectUrl() + { + + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs new file mode 100644 index 000000000..afdd6f41d --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +public class ExactlySettings +{ + [Required] + public string BaseAddress { get; set; } + + [Required] + [PasswordPropertyText(password: true)] + public string ApiKey { get; set; } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj b/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj index aac228693..f0374bbb8 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj @@ -36,4 +36,11 @@ + + + + + + + diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyApiHandler.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyApiHandler.cs new file mode 100644 index 000000000..c5d311d62 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyApiHandler.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Commerce.Payment.Exactly.Models; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Exactly.Services; + +public class ExactlyApiHandler : DelegatingHandler +{ + private readonly ExactlySettings _settings; + + public ExactlyApiHandler(IOptionsSnapshot settings) => _settings = settings.Value; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Headers.Add("Authorization", "Api-Key " + _settings.ApiKey); + + return base.SendAsync(request, cancellationToken); + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs new file mode 100644 index 000000000..999a1560c --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.ContentManagement; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Exactly.Services; + +public class ExactlyPaymentProvider : IPaymentProvider +{ + public const string ProviderName = "Exactly"; + + public string Name => ProviderName; + + public ExactlyPaymentProvider() + { + } + + public Task CreatePaymentProviderDataAsync(IPaymentViewModel model) => + throw new System.NotImplementedException(); + public Task UpdateAndRedirectToFinishedOrderAsync( + Controller controller, + ContentItem order, + string shoppingCartId) => + throw new System.NotImplementedException(); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs new file mode 100644 index 000000000..d9dc7fbe2 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Commerce.Payment.Exactly.Services; + +public class ExactlyService +{ + +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs new file mode 100644 index 000000000..f5147dc0a --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs @@ -0,0 +1,20 @@ +using Refit; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Exactly.Services; + +/// +/// Wrapper for the Exactly API. See here. All method documentation is +/// copied from this source. The authorization API key is provided by an +/// +[Headers("Content-Type: application/vnd.api+json")] +public interface IExactlyApi +{ + /// + /// The endpoint creates new transaction. Created transaction is processed in asynchronous manner, so there won't be + /// any completed transaction in response to a request to this endpoint. Get transaction details endpoint or + /// callback/webhook must be used to retrieve status of the transaction when it's completed. + /// + [Post("/api/v1/transactions")] + Task<> CreateTransactionAsync([Body] data); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs new file mode 100644 index 000000000..6b6cd6423 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Exactly.Models; +using OrchardCore.Commerce.Payment.Exactly.Services; +using OrchardCore.Modules; +using Refit; +using System; + +namespace OrchardCore.Commerce.Payment.Exactly; + +public class Startup : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + services.AddTransient(); + services.AddRefitClient() + .ConfigureHttpClient((provider, client) => + { + var settings = provider + .GetRequiredService() + .HttpContext! + .RequestServices + .GetRequiredService>() + .Value; + + client.BaseAddress = new Uri(settings.BaseAddress); + }) + .AddHttpMessageHandler(); + } +} diff --git a/src/Modules/OrchardCore.Commerce/OrchardCore.Commerce.csproj b/src/Modules/OrchardCore.Commerce/OrchardCore.Commerce.csproj index d8da6123b..31c5428b4 100644 --- a/src/Modules/OrchardCore.Commerce/OrchardCore.Commerce.csproj +++ b/src/Modules/OrchardCore.Commerce/OrchardCore.Commerce.csproj @@ -40,6 +40,7 @@ + From f8d37a4b91e75c6f5ed9ac9de92c551c852d4912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 20 Jan 2024 22:52:52 +0100 Subject: [PATCH 03/90] Add configuration, permission, admin things. --- .../AdminMenu.cs | 24 +++++++++ .../Drivers/ExactlySettingsDisplayDriver.cs | 54 +++++++++++++++++++ .../Models/ExactlySetings.cs | 9 +++- .../Permissions.cs | 16 ++++++ .../Services/ExactlySettingsConfiguration.cs | 24 +++++++++ .../Startup.cs | 19 +++++++ .../Views/ExactlySettings.Edit.cshtml | 4 ++ .../Views/_ViewImports.cshtml | 10 ++++ 8 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/AdminMenu.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Permissions.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlySettingsConfiguration.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/_ViewImports.cshtml diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/AdminMenu.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/AdminMenu.cs new file mode 100644 index 000000000..215a992d1 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/AdminMenu.cs @@ -0,0 +1,24 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Navigation; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Localization; +using OrchardCore.Commerce.Payment.Exactly.Drivers; +using OrchardCore.Navigation; + +namespace OrchardCore.Commerce.Payment.Exactly; + +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["Exactly API"], T["Exactly API"], entry => entry + .SiteSettings(ExactlySettingsDisplayDriver.EditorGroupId) + .Permission(Permissions.ManageExactlySettings) + .LocalNav()) + )); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs new file mode 100644 index 000000000..caacf4dd8 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using OrchardCore.Commerce.Payment.Exactly.Models; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Settings; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Exactly.Drivers; + +public class ExactlySettingsDisplayDriver : SectionDisplayDriver +{ + public const string EditorGroupId = "Exactly"; + + private readonly IAuthorizationService _authorizationService; + private readonly IHttpContextAccessor _hca; + private readonly ExactlySettings _ssoSettings; + + public ExactlySettingsDisplayDriver( + IAuthorizationService authorizationService, + IHttpContextAccessor hca, + IOptionsSnapshot ssoSettings) + { + _authorizationService = authorizationService; + _hca = hca; + _ssoSettings = ssoSettings.Value; + } + + public override async Task EditAsync(ExactlySettings section, BuildEditorContext context) => + await AuthorizeAsync() + ? Initialize($"{nameof(ExactlySettings)}_Edit", _ssoSettings.CopyTo) + .PlaceInContent() + .OnGroup(EditorGroupId) + : null; + + public override async Task UpdateAsync(ExactlySettings section, BuildEditorContext context) + { + var viewModel = new ExactlySettings(); + + if (context.GroupId == EditorGroupId && + await AuthorizeAsync() && + await context.Updater.TryUpdateModelAsync(viewModel, Prefix)) + { + viewModel.CopyTo(section); + } + + return await EditAsync(section, context); + } + + private Task AuthorizeAsync() => + _authorizationService.AuthorizeCurrentUserAsync(_hca.HttpContext, Permissions.ManageExactlySettings); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs index afdd6f41d..9d2479c87 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace OrchardCore.Commerce.Payment.Exactly.Models; @@ -11,4 +12,10 @@ public class ExactlySettings [Required] [PasswordPropertyText(password: true)] public string ApiKey { get; set; } + + public void CopyTo(ExactlySettings target) + { + if (Uri.TryCreate(BaseAddress, UriKind.Absolute, out var _)) target.BaseAddress = BaseAddress; + if (!string.IsNullOrWhiteSpace(ApiKey)) target.ApiKey = ApiKey; + } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Permissions.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Permissions.cs new file mode 100644 index 000000000..a173605a9 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Permissions.cs @@ -0,0 +1,16 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Users; +using OrchardCore.Security.Permissions; +using System.Collections.Generic; + +namespace OrchardCore.Commerce.Payment.Exactly; + +public class Permissions : AdminPermissionBase +{ + public static readonly Permission ManageExactlySettings = + new(nameof(ManageExactlySettings), "Manage Exactly settings."); + + protected override IEnumerable AdminPermissions => new[] + { + ManageExactlySettings, + }; +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlySettingsConfiguration.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlySettingsConfiguration.cs new file mode 100644 index 000000000..2e350fcea --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlySettingsConfiguration.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Commerce.Payment.Exactly.Models; +using OrchardCore.Entities; +using OrchardCore.Settings; + +namespace OrchardCore.Commerce.Payment.Exactly.Services; + +public class ExactlySettingsConfiguration : IConfigureOptions +{ + private readonly ISiteService _siteService; + + public ExactlySettingsConfiguration(ISiteService siteService) => _siteService = siteService; + + public void Configure(ExactlySettings options) + { + var siteSettings = _siteService + .GetSiteSettingsAsync() + .GetAwaiter() + .GetResult() + .As(); + + siteSettings.CopyTo(options); + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs index 6b6cd6423..a52b02da7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs @@ -2,9 +2,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Exactly.Drivers; using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.Commerce.Payment.Exactly.Services; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Environment.Shell.Configuration; using OrchardCore.Modules; +using OrchardCore.Navigation; +using OrchardCore.Security.Permissions; +using OrchardCore.Settings; using Refit; using System; @@ -12,11 +18,24 @@ namespace OrchardCore.Commerce.Payment.Exactly; public class Startup : StartupBase { + private readonly IShellConfiguration _shellConfiguration; + + public Startup(IShellConfiguration shellConfiguration) => _shellConfiguration = shellConfiguration; + public override void ConfigureServices(IServiceCollection services) { + // Payment services services.AddScoped(); services.AddScoped(); + // Configuration, permission, admin things + services.Configure(_shellConfiguration.GetSection("OrchardCoreCommerce_Payment_Exactly")); + services.AddTransient, ExactlySettingsConfiguration>(); + services.AddScoped, ExactlySettingsDisplayDriver>(); + services.AddScoped(); + services.AddScoped(); + + // API client services.AddTransient(); services.AddRefitClient() .ConfigureHttpClient((provider, client) => diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml new file mode 100644 index 000000000..9ff6daf7b --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -0,0 +1,4 @@ +@model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings + +
+
diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/_ViewImports.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/_ViewImports.cshtml new file mode 100644 index 000000000..b253153fc --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/_ViewImports.cshtml @@ -0,0 +1,10 @@ +@* + 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 +@addTagHelper *, Lombiq.HelpfulLibraries.OrchardCore From 2280e832819ad3b61c5389528af33ccfa0bc20a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sun, 21 Jan 2024 00:37:34 +0100 Subject: [PATCH 04/90] Add ChargeRequest. --- .../Controllers/ExactlyController.cs | 8 ++- .../Models/ChargeRequest.cs | 70 +++++++++++++++++++ .../Models/ExactlyRequest.cs | 8 +++ .../Models/ExactlySetings.cs | 6 +- .../Models/IExactlyRequestAttributes.cs | 12 ++++ .../Services/IExactlyApi.cs | 5 +- .../Views/ExactlySettings.Edit.cshtml | 5 +- 7 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyRequest.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyRequestAttributes.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index ee4b9ce88..5d52797fc 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.Build.Utilities; +using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Controllers; @@ -14,4 +14,10 @@ public async Task GetRedirectUrl() { } + + [HttpGet("checkout/middleware/Exactly")] + public async Task Middleware() + { + + } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs new file mode 100644 index 000000000..b02c9a401 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.MoneyDataType.Extensions; +using OrchardCore.Commerce.Payment.Exactly.Controllers; +using OrchardCore.Users.Models; +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +public class ChargeRequest : IExactlyRequestAttributes +{ + public string Type => "charge"; + + public string ProjectId { get; set; } + public string PaymentMethod { get; set; } = "card"; + public string Amount { get; set; } + public string Currency { get; set; } + public string ReferenceId { get; set; } + public string CustomerDescription { get; set; } + public string ReturnUrl { get; set; } + public string CustomerId { get; set; } + public string Email { get; set; } + public bool TokenizeSource { get; set; } = true; + public int Lifetime { get; set; } = 3600; + public object Meta { get; set; } + + public ChargeRequest() + { + } + + public ChargeRequest(OrderPart orderPart, User user, string projectId, string tenantId, Uri returnUrl) + { + var total = orderPart.Charges.Select(payment => payment.Amount).Sum(); + + if (!returnUrl.IsAbsoluteUri) throw new ArgumentException("The return URL must be absolute.", nameof(returnUrl)); + + ProjectId = projectId; + ReferenceId = $"{tenantId}-{orderPart.OrderId.Text}"; + CustomerDescription = string.Join(", ", orderPart.Charges.Select(payment => $"{payment.Amount} × {payment.ChargeText}")); + ReturnUrl = returnUrl.AbsoluteUri; + CustomerId = user.UserId; + Email = user.Email; + Amount = total.Value.ToString("0.00", CultureInfo.InvariantCulture); + Currency = total.Currency.CurrencyIsoCode; + Meta = orderPart; + } + + public static implicit operator ExactlyRequest(ChargeRequest attributes) => + new() { Attributes = attributes }; + + public static async Task CreateUserAsync(OrderPart orderPart, HttpContext context) + { + var provider = context.RequestServices; + var returnurl = context.ActionTask(controller => controller.Middleware()); + + return new ChargeRequest( + orderPart, + await provider.GetRequiredService().GetFullUserAsync(context.User), + provider.GetRequiredService>().Value.ProjectId, + context.Request.Host.Host, + new Uri(new Uri(context.Request.GetDisplayUrl()), returnurl)); + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyRequest.cs new file mode 100644 index 000000000..60e21987b --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyRequest.cs @@ -0,0 +1,8 @@ +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +public class ExactlyRequest + where T : IExactlyRequestAttributes +{ + public string Type => Attributes?.Type; + public T Attributes { get; set; } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs index 9d2479c87..18572ae53 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace OrchardCore.Commerce.Payment.Exactly.Models; @@ -9,13 +8,14 @@ public class ExactlySettings [Required] public string BaseAddress { get; set; } - [Required] - [PasswordPropertyText(password: true)] public string ApiKey { get; set; } + public string ProjectId { get; set; } + public void CopyTo(ExactlySettings target) { if (Uri.TryCreate(BaseAddress, UriKind.Absolute, out var _)) target.BaseAddress = BaseAddress; if (!string.IsNullOrWhiteSpace(ApiKey)) target.ApiKey = ApiKey; + if (!string.IsNullOrWhiteSpace(ProjectId)) target.ProjectId = ProjectId; } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyRequestAttributes.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyRequestAttributes.cs new file mode 100644 index 000000000..399412938 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyRequestAttributes.cs @@ -0,0 +1,12 @@ +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +/// +/// The data payload for requests using . +/// +public interface IExactlyRequestAttributes +{ + /// + /// Gets the type name of the request. This is used by . + /// + public string Type { get; } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs index f5147dc0a..799fec763 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs @@ -1,4 +1,5 @@ -using Refit; +using OrchardCore.Commerce.Payment.Exactly.Models; +using Refit; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Services; @@ -16,5 +17,5 @@ public interface IExactlyApi /// callback/webhook must be used to retrieve status of the transaction when it's completed. /// [Post("/api/v1/transactions")] - Task<> CreateTransactionAsync([Body] data); + Task<> CreateTransactionAsync([Body] ExactlyRequest data); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 9ff6daf7b..778e2478d 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -1,4 +1,5 @@ @model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings -
-
+
+
+
From 5be431d21654a21aa1dedda61edf90beb85928a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 23 Jan 2024 14:12:10 +0100 Subject: [PATCH 05/90] Add response models. --- .../Models/ChargeRequest.cs | 10 +- .../Models/ChargeResponse.cs | 95 +++++++++++++++++++ .../Models/ExactlyError.cs | 9 ++ .../Models/ExactlyException.cs | 33 +++++++ .../Models/ExactlyResponse.cs | 21 ++++ .../Models/IExactlyAmount.cs | 36 +++++++ .../Models/IExactlyResponseData.cs | 12 +++ .../Models/PaymentMethod.cs | 6 ++ .../Services/IExactlyApi.cs | 10 +- 9 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyError.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyException.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyResponseData.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/PaymentMethod.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index b02c9a401..91ef7c9c5 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -10,12 +10,14 @@ using System; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Models; -public class ChargeRequest : IExactlyRequestAttributes +public class ChargeRequest : IExactlyRequestAttributes, IExactlyAmount { + [JsonIgnore] public string Type => "charge"; public string ProjectId { get; set; } @@ -37,8 +39,6 @@ public ChargeRequest() public ChargeRequest(OrderPart orderPart, User user, string projectId, string tenantId, Uri returnUrl) { - var total = orderPart.Charges.Select(payment => payment.Amount).Sum(); - if (!returnUrl.IsAbsoluteUri) throw new ArgumentException("The return URL must be absolute.", nameof(returnUrl)); ProjectId = projectId; @@ -47,9 +47,9 @@ public ChargeRequest(OrderPart orderPart, User user, string projectId, string te ReturnUrl = returnUrl.AbsoluteUri; CustomerId = user.UserId; Email = user.Email; - Amount = total.Value.ToString("0.00", CultureInfo.InvariantCulture); - Currency = total.Currency.CurrencyIsoCode; Meta = orderPart; + + this.SetAmount(orderPart.Charges.Select(payment => payment.Amount).Sum()); } public static implicit operator ExactlyRequest(ChargeRequest attributes) => diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs new file mode 100644 index 000000000..5c33275e8 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +public class ChargeResponse : IExactlyResponseData +{ + private static Dictionary StatusMap = new() + { + ["action-required"] = ChargeResponseStatus.ActionRequired, + ["processing"] = ChargeResponseStatus.Processing, + ["processed"] = ChargeResponseStatus.Processed, + ["failed"] = ChargeResponseStatus.Failed, + }; + + public string Type => "charge"; + public string Id { get; set; } + + public enum ChargeResponseStatus + { + /// + /// Additional data or action (e.g., redirect) is required to proceed with transactions; required action details + /// are available in . + /// + ActionRequired, + + /// + /// Transaction is being processed. + /// + Processing, + + /// + /// Processing of the transaction successfully completed; additional result details might be available in . + /// + Processed, + + /// + /// Processing of transaction failed, reason of failure should be available in . + /// + Failed, + } + + public class ChargeProcessing : IExactlyAmount + { + public PaymentMethod PaymentMethod { get; set; } + public string Amount { get; set; } + public string Currency { get; set; } + public DateTime ProcessedAt { get; set; } + public string ResultCode { get; set; } + } + + public class ChargeAction + { + public string Action { get; set; } + public object RequiredAttributes { get; set; } + } + + public class ChargeAttributes + { + public string ProjectId { get; set; } + + [JsonPropertyName("status")] + private string StringStatus { get; set; } + + [JsonIgnore] + public ChargeResponseStatus Status + { + get => StatusMap.GetMaybe(StringStatus); + set => StringStatus = StatusMap.Single(pair => pair.Value == value).Key; + } + + [JsonPropertyName("environmentMode")] + private string StringEnvironmentMode { get; set; } + + [JsonIgnore] + public bool IsLive + { + get => StringEnvironmentMode == "live"; + set => StringEnvironmentMode = value ? "live" : "sandbox"; + } + + public DateTime CreatedAt { get; set; } + + public ChargeProcessing Processing { get; set; } + public IList StatusHistory { get; set; } + public object Meta { get; set; } + public string ReferenceId { get; set; } + public DateTime ExpireAt { get; set; } + public IList Actions { get; set; } + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyError.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyError.cs new file mode 100644 index 000000000..aba8710b4 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyError.cs @@ -0,0 +1,9 @@ +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +public class ExactlyError +{ + public string Code { get; set; } + public string Title { get; set; } + public string Details { get; set; } + public object Meta { get; set; } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyException.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyException.cs new file mode 100644 index 000000000..b392a9949 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyException.cs @@ -0,0 +1,33 @@ +using System; +using System.Runtime.Serialization; + +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +[Serializable] +public class ExactlyException : Exception +{ + public ExactlyError Error { get; } + + public ExactlyException(ExactlyError error) + : base($"{error.Code}: {error.Title} ({error.Details})") => + Error = error; + + public ExactlyException(string message) + : base(message) + { + } + + public ExactlyException() + { + } + + public ExactlyException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected ExactlyException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs new file mode 100644 index 000000000..fa62a3a9d --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +public class ExactlyResponse + where T : IExactlyResponseData +{ + public IExactlyResponseData Data { get; set; } + public IList Errors { get; set; } + + public void ThrowIfHasErrors() + { + if (Errors?.Any() != true) return; + + if (Errors.Count == 1) throw new ExactlyException(Errors.Single()); + + throw new AggregateException(Errors.Select(error => new ExactlyException(error))); + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs new file mode 100644 index 000000000..17644f383 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs @@ -0,0 +1,36 @@ +using OrchardCore.Commerce.MoneyDataType; +using System.Globalization; + +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +/// +/// Interface for types that represent an amount, and so may be initialized from an . +/// +public interface IExactlyAmount +{ + /// + /// Gets the decimal amount represented as string. Point (".") must be used as decimal separator. Decimal separator + /// must always be present for currencies with minor units. Max len is 37 chars: 18 digits before decimal separator, + /// decimal separator, 18 digits after + /// + public string Amount { get; set; } + + /// + /// Gets the currency code, can be ISO 4217 alpha code or unofficial, e.g. XBT for Bitcoin. + /// + public string Currency { get; set; } +} + +public static class ExactlyAmountExtensions +{ + public static void SetAmount(this IExactlyAmount target, Amount source) + { + target.Amount = source.Value.ToString("0.00", CultureInfo.InvariantCulture); + target.Currency = source.Currency.CurrencyIsoCode; + } + + public static Amount GetAmount(this IExactlyAmount source) => + new( + decimal.Parse(source.Amount, CultureInfo.InvariantCulture), + Currency.FromIsoCurrencyCode(source.Currency)); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyResponseData.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyResponseData.cs new file mode 100644 index 000000000..ba5cf3897 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyResponseData.cs @@ -0,0 +1,12 @@ +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +/// +/// The data payload for responses using . +/// +public interface IExactlyResponseData +{ + /// + /// Gets the type name of the response. + /// + public string Type { get; } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/PaymentMethod.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/PaymentMethod.cs new file mode 100644 index 000000000..2c37d7fa1 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/PaymentMethod.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +public class PaymentMethod +{ + public string Type { get; set; } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs index 799fec763..e7a1e06f4 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs @@ -17,5 +17,13 @@ public interface IExactlyApi /// callback/webhook must be used to retrieve status of the transaction when it's completed. /// [Post("/api/v1/transactions")] - Task<> CreateTransactionAsync([Body] ExactlyRequest data); + Task>> CreateTransactionAsync([Body] ExactlyRequest data); + + /// + /// The endpoint creates new transaction. Created transaction is processed in asynchronous manner, so there won't be + /// any completed transaction in response to a request to this endpoint. Get transaction details endpoint or + /// callback/webhook must be used to retrieve status of the transaction when it's completed. + /// + [Get("/api/v1/transactions/{transactionId}")] + Task>> CreateTransactionAsync(string transactionId); } From a7b0c5c31c4a56de6f7221db0d8c6c3f07027ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 23 Jan 2024 15:20:43 +0100 Subject: [PATCH 06/90] Add FrontendException.ThrowIfAny. --- .../Exceptions/FrontendException.cs | 28 ++++++++++++++++ .../Models/ExactlyException.cs | 33 ------------------- .../Models/ExactlyResponse.cs | 7 ++-- .../Services/PaymentService.cs | 6 +--- .../Services/ShoppingCartHelpers.cs | 5 ++- 5 files changed, 36 insertions(+), 43 deletions(-) delete mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyException.cs diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs index 193a06902..565b8c0dd 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs @@ -1,5 +1,9 @@ +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Localization; using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.Serialization; namespace OrchardCore.Commerce.Abstractions.Exceptions; @@ -30,4 +34,28 @@ protected FrontendException(SerializationInfo info, StreamingContext context) : base(info, context) { } + + /// + /// If the provided collection of is not empty, throws an exception with the included texts. + /// + /// The possible collection of error texts. + public static void ThrowIfAny([AllowNull] ICollection errors) + { + if (errors?.Any() != true) return; + + if (errors.Count == 1) throw new FrontendException(errors.Single()); + + throw new FrontendException(new HtmlString("
").Join( + errors.Select(error => new LocalizedHtmlString(error, error)).ToArray())); + } + + /// + public static void ThrowIfAny([AllowNull] ICollection errors) + { + if (errors?.Any() != true) return; + + if (errors.Count == 1) throw new FrontendException(errors.Single()); + + throw new FrontendException(new HtmlString("
").Join(errors.ToArray())); + } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyException.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyException.cs deleted file mode 100644 index b392a9949..000000000 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyException.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace OrchardCore.Commerce.Payment.Exactly.Models; - -[Serializable] -public class ExactlyException : Exception -{ - public ExactlyError Error { get; } - - public ExactlyException(ExactlyError error) - : base($"{error.Code}: {error.Title} ({error.Details})") => - Error = error; - - public ExactlyException(string message) - : base(message) - { - } - - public ExactlyException() - { - } - - public ExactlyException(string message, Exception innerException) - : base(message, innerException) - { - } - - protected ExactlyException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } -} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs index fa62a3a9d..f27c9a6be 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs @@ -1,4 +1,4 @@ -using System; +using OrchardCore.Commerce.Abstractions.Exceptions; using System.Collections.Generic; using System.Linq; @@ -14,8 +14,7 @@ public void ThrowIfHasErrors() { if (Errors?.Any() != true) return; - if (Errors.Count == 1) throw new ExactlyException(Errors.Single()); - - throw new AggregateException(Errors.Select(error => new ExactlyException(error))); + var errors = Errors.Select(error => $"{error.Code}: {error.Title} ({error.Details})").ToList(); + FrontendException.ThrowIfAny(errors); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs index 7b5186bc3..7959af32b 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs @@ -266,11 +266,7 @@ public async Task UpdateOrderToOrderedAsync( 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())); - } + FrontendException.ThrowIfAny(errors); } // If there are line items in the Order, use data from Order instead of shopping cart. diff --git a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs index 1b391bc90..085446d43 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs @@ -173,14 +173,17 @@ public async Task AddToCartAsync( var cart = await _shoppingCartPersistence.RetrieveAsync(shoppingCartId); var parsedLine = cart.AddItem(item); + var errors = new List(); foreach (var shoppingCartEvent in _shoppingCartEvents.OrderBy(provider => provider.Order)) { if (await shoppingCartEvent.VerifyingItemAsync(parsedLine) is { } errorMessage) { - throw new FrontendException(errorMessage); + errors.Add(errorMessage); } } + FrontendException.ThrowIfAny(errors); + if (await GetErrorAsync(parsedLine.ProductSku, parsedLine) is { } error) { throw new FrontendException(error); From d4f2f8a2aa9417b7a971f11080667b0aeb628654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 23 Jan 2024 15:44:21 +0100 Subject: [PATCH 07/90] controller stuff --- .../Controllers/ExactlyController.cs | 71 +++++++++++++++++-- .../Models/ChargeResponse.cs | 1 + .../Models/ExactlyResponse.cs | 2 +- .../Services/IExactlyApi.cs | 6 +- .../Abstractions/IPaymentService.cs | 6 +- .../Services/PaymentService.cs | 35 ++++++--- 6 files changed, 101 insertions(+), 20 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 5d52797fc..f0e212cf3 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,23 +1,84 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Localization; +using OrchardCore.Commerce.Abstractions.Exceptions; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Exactly.Models; +using OrchardCore.Commerce.Payment.Exactly.Services; +using OrchardCore.ContentManagement; +using Refit; +using System; +using System.Linq; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Controllers; public class ExactlyController : Controller { - public async Task CreateTransaction() - { + private readonly IExactlyApi _api; + private readonly IPaymentService _paymentService; + private readonly IStringLocalizer H; + public ExactlyController(IExactlyApi api, IPaymentService paymentService, IStringLocalizer stringLocalizer) + { + _api = api; + _paymentService = paymentService; + H = stringLocalizer; } - public async Task GetRedirectUrl() - { + public async Task CreateTransaction(string shoppingCartId) => + await this.SafeJsonAsync(async () => + { + try + { + var order = await _paymentService.CreatePendingOrderFromShoppingCartAsync( + shoppingCartId, + notifyOnError: false, + throwOnError: true); + var request = await ChargeRequest.CreateUserAsync(order!.As(), HttpContext); - } + return ThrowIfError(await _api.CreateTransactionAsync(request)); + } + catch (FrontendException exception) + { + return new { error = exception.Message, html = exception.HtmlMessage.Html() }; + } + }); + + public async Task GetRedirectUrl(string transactionId) => + await this.SafeJsonAsync(async () => + { + try + { + var content = ThrowIfError(await _api.GetTransactionDetailsAsync(transactionId)); + return content.Attributes.Actions.First(action => action.Action != null).Action; + } + catch (FrontendException exception) + { + return new { error = exception.Message, html = exception.HtmlMessage.Html() }; + } + }); [HttpGet("checkout/middleware/Exactly")] public async Task Middleware() { + throw new NotSupportedException("TODO"); + } + + private T ThrowIfError(ApiResponse> response) + where T : IExactlyResponseData + { + using (response) + { + response.Content?.ThrowIfHasErrors(); + if (response.Error is { } error) throw new FrontendException(error.Message); + if (response.Content is not { } content) + { + throw new FrontendException(H["There was an unknown error while contacting with the payment service, please try again."]); + } + return content.Data; + } } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs index 5c33275e8..0059a4cf6 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs @@ -17,6 +17,7 @@ public class ChargeResponse : IExactlyResponseData public string Type => "charge"; public string Id { get; set; } + public ChargeAttributes Attributes { get; set; } public enum ChargeResponseStatus { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs index f27c9a6be..1655ddcc1 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs @@ -7,7 +7,7 @@ namespace OrchardCore.Commerce.Payment.Exactly.Models; public class ExactlyResponse where T : IExactlyResponseData { - public IExactlyResponseData Data { get; set; } + public T Data { get; set; } public IList Errors { get; set; } public void ThrowIfHasErrors() diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs index e7a1e06f4..9444ac613 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs @@ -20,10 +20,8 @@ public interface IExactlyApi Task>> CreateTransactionAsync([Body] ExactlyRequest data); /// - /// The endpoint creates new transaction. Created transaction is processed in asynchronous manner, so there won't be - /// any completed transaction in response to a request to this endpoint. Get transaction details endpoint or - /// callback/webhook must be used to retrieve status of the transaction when it's completed. + /// Returns details of a transaction. /// [Get("/api/v1/transactions/{transactionId}")] - Task>> CreateTransactionAsync(string transactionId); + Task>> GetTransactionDetailsAsync(string transactionId); } diff --git a/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs index 1992d33b4..eb8c28776 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs @@ -40,7 +40,11 @@ public interface IPaymentService /// 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); + Task CreatePendingOrderFromShoppingCartAsync( + string? shoppingCartId, + bool mustBeFree = false, + bool notifyOnError = true, + bool throwOnError = false); /// /// Updates the 's status to . diff --git a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs index 7959af32b..e3491dbfc 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs @@ -167,24 +167,21 @@ await _paymentProvidersLazy } } - public async Task CreatePendingOrderFromShoppingCartAsync(string? shoppingCartId, bool mustBeFree) + public async Task CreatePendingOrderFromShoppingCartAsync( + string? shoppingCartId, + bool mustBeFree = false, + bool notifyOnError = true, + bool throwOnError = false) { var cart = await _shoppingCartHelpers.RetrieveAsync(shoppingCartId); var order = await _contentManager.NewAsync(Order); - var errors = await UpdateOrderWithDriversAsync(order); - if (errors.Any()) + if (!await HandleErrorsAsync(await UpdateOrderWithDriversAsync(order), notifyOnError, throwOnError)) { - 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)) @@ -223,6 +220,26 @@ await _orderEvents.AwaitEachAsync(orderEvents => return order; } + private async Task HandleErrorsAsync(IList errors, bool notifyOnError, bool throwOnError) + { + if (!errors.Any()) return true; + + if (notifyOnError) + { + foreach (var error in errors) + { + await _notifier.ErrorAsync(new LocalizedHtmlString(error, error)); + } + } + + if (throwOnError) + { + FrontendException.ThrowIfAny(errors); + } + + return false; + } + private async Task> UpdateOrderWithDriversAsync(ContentItem order) { await _contentItemDisplayManager.UpdateEditorAsync(order, _updateModelAccessor.ModelUpdater, isNew: false); From 35a04a32268045332afcad83944d8ee7be781f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 23 Jan 2024 16:17:04 +0100 Subject: [PATCH 08/90] Frontend button. --- .../Controllers/ExactlyController.cs | 1 + .../Views/CheckoutExactly.cshtml | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index f0e212cf3..f5ff2e792 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -27,6 +27,7 @@ public ExactlyController(IExactlyApi api, IPaymentService paymentService, IStrin H = stringLocalizer; } + [HttpPost] public async Task CreateTransaction(string shoppingCartId) => await this.SafeJsonAsync(async () => { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml new file mode 100644 index 000000000..ff5c68e37 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml @@ -0,0 +1,55 @@ +@using Microsoft.AspNetCore.Mvc.Localization +@using OrchardCore +@using OrchardCore.Commerce.Payment.Exactly.Controllers + +@{ + var actionUrl = Orchard.Action(controller => controller.CreateTransaction(null)); + var statusUrl = Orchard.Action(controller => controller.GetRedirectUrl("TRANSACTION_ID")); + var defaultErrorMessage = + T["An error has occurred while trying to connect to the payment service. Please try again later."]; +} + + + + \ No newline at end of file From d3eb9f92851addc4c865d72c3fba0b0fd09e7249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 29 Jan 2024 12:59:06 +0100 Subject: [PATCH 09/90] not yet needed --- src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs index a52b02da7..8be4a186f 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs @@ -26,7 +26,7 @@ public override void ConfigureServices(IServiceCollection services) { // Payment services services.AddScoped(); - services.AddScoped(); + //services.AddScoped(); // Configuration, permission, admin things services.Configure(_shellConfiguration.GetSection("OrchardCoreCommerce_Payment_Exactly")); From 5476f61447edacd537bd9c02f57d5b673782766e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 19 Mar 2024 21:54:21 +0100 Subject: [PATCH 10/90] Post merge fixup. --- Directory.Packages.props | 1 + .../Exceptions/FrontendException.cs | 10 ++++++---- .../Models/ChargeRequest.cs | 3 +-- .../OrchardCore.Commerce.Payment.Exactly.csproj | 6 +++--- .../Controllers/PaymentController.cs | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index faa05c667..c90730d55 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs index f0339a4dc..9821407b8 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs @@ -29,12 +29,13 @@ public FrontendException(string message, Exception innerException) } /// - /// If the provided collection of is not empty, throws an exception with the included texts. + /// If the provided collection of is not empty, it throws an exception with the included + /// texts. If there are multiple, they are merged with a HTML line break. /// /// The possible collection of error texts. public static void ThrowIfAny([AllowNull] ICollection errors) { - if (errors?.Any() != true) return; + if (errors == null || errors.Count == 0) return; if (errors.Count == 1) throw new FrontendException(errors.Single()); @@ -45,10 +46,11 @@ public static void ThrowIfAny([AllowNull] ICollection errors) /// public static void ThrowIfAny([AllowNull] ICollection errors) { - if (errors?.Any() != true) return; + if (errors == null || errors.Count == 0) return; if (errors.Count == 1) throw new FrontendException(errors.Single()); - throw new FrontendException(new HtmlString("
").Join(errors.ToArray())); + var errorsArray = errors.ToArray(); + throw new FrontendException(new HtmlString("
").Join(errorsArray)); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index 91ef7c9c5..1320db0bd 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -8,14 +8,13 @@ using OrchardCore.Commerce.Payment.Exactly.Controllers; using OrchardCore.Users.Models; using System; -using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Models; -public class ChargeRequest : IExactlyRequestAttributes, IExactlyAmount +public class ChargeRequest : IExactlyRequestAttributes, IExactlyAmount { [JsonIgnore] public string Type => "charge"; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj b/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj index f0374bbb8..079eb3c33 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 true @@ -28,7 +28,7 @@
- + @@ -40,7 +40,7 @@ - + diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index 60ee6e42b..1ebecb600 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -240,7 +240,7 @@ public async Task Callback(string paymentProviderName, string? or if (string.IsNullOrWhiteSpace(paymentProviderName)) return NotFound(); var order = string.IsNullOrEmpty(orderId) - ? await _paymentService.CreatePendingOrderFromShoppingCartAsync(shoppingCartId, mustBeFree: false) + ? await _paymentService.CreatePendingOrderFromShoppingCartAsync(shoppingCartId) : await _contentManager.GetAsync(orderId); if (order is null) return NotFound(); From 7b35da897351448f09e7cc9ba2a5d1617471ee39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 20 Mar 2024 14:20:52 +0100 Subject: [PATCH 11/90] Add feature to recipes. --- .../Constants/FeatureIds.cs | 6 ++++++ .../OrchardCore.Commerce.Development.Setup.recipe.json | 1 + ...OrchardCore.Commerce.Development.Tests.Setup.recipe.json | 1 + 3 files changed, 8 insertions(+) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Constants/FeatureIds.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Constants/FeatureIds.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Constants/FeatureIds.cs new file mode 100644 index 000000000..04572c49d --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Constants/FeatureIds.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Commerce.Payment.Exactly.Constants; + +public static class FeatureIds +{ + public const string Area = "OrchardCore.Commerce.Payment.Exactly"; +} 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 32f189ff9..48d5831f9 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 @@ -65,6 +65,7 @@ "OrchardCore.Commerce.SessionCartStorage", "OrchardCore.Commerce.Payment.DummyProvider", + "OrchardCore.Commerce.Payment.Exactly", "OrchardCore.Commerce.Payment.Stripe", "OrchardCore.Commerce.Promotion", "OrchardCore.Commerce.Inventory", 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 bec67bc85..19a71b246 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 @@ -34,6 +34,7 @@ "OrchardCore.Localization" ], "disable": [ + "OrchardCore.Commerce.Payment.Exactly", "OrchardCore.Commerce.Payment.Stripe" ] }, From 50e37537b539f9c4dbf0835d45ee73fd7aa71d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 20 Mar 2024 14:59:42 +0100 Subject: [PATCH 12/90] Organize Exactly settings. --- .../Drivers/ExactlySettingsDisplayDriver.cs | 14 +++++++++- .../Models/ExactlySetings.cs | 11 ++++---- .../Views/ExactlySettings.Edit.cshtml | 27 ++++++++++++++++--- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs index caacf4dd8..0c913a6e4 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs @@ -5,6 +5,7 @@ using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; +using OrchardCore.Environment.Shell; using OrchardCore.Settings; using System.Threading.Tasks; @@ -16,21 +17,29 @@ public class ExactlySettingsDisplayDriver : SectionDisplayDriver ssoSettings) { _authorizationService = authorizationService; _hca = hca; + _shellHost = shellHost; + _shellSettings = shellSettings; _ssoSettings = ssoSettings.Value; } public override async Task EditAsync(ExactlySettings section, BuildEditorContext context) => await AuthorizeAsync() - ? Initialize($"{nameof(ExactlySettings)}_Edit", _ssoSettings.CopyTo) + ? Initialize( + $"{nameof(ExactlySettings)}_Edit", + settings => _ssoSettings.CopyTo(settings, copyPassword: false)) .PlaceInContent() .OnGroup(EditorGroupId) : null; @@ -44,6 +53,9 @@ await AuthorizeAsync() && await context.Updater.TryUpdateModelAsync(viewModel, Prefix)) { viewModel.CopyTo(section); + + // Release the tenant to apply settings. + await _shellHost.ReleaseShellContextAsync(_shellSettings); } return await EditAsync(section, context); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs index 18572ae53..b2f4cc62d 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs @@ -6,16 +6,17 @@ namespace OrchardCore.Commerce.Payment.Exactly.Models; public class ExactlySettings { [Required] - public string BaseAddress { get; set; } - - public string ApiKey { get; set; } + public string BaseAddress { get; set; } = "https://api.exactly.com/"; + [Required] public string ProjectId { get; set; } - public void CopyTo(ExactlySettings target) + public string ApiKey { get; set; } + + public void CopyTo(ExactlySettings target, bool copyPassword = true) { if (Uri.TryCreate(BaseAddress, UriKind.Absolute, out var _)) target.BaseAddress = BaseAddress; - if (!string.IsNullOrWhiteSpace(ApiKey)) target.ApiKey = ApiKey; if (!string.IsNullOrWhiteSpace(ProjectId)) target.ProjectId = ProjectId; + if (copyPassword && !string.IsNullOrWhiteSpace(ApiKey)) target.ApiKey = ApiKey; } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 778e2478d..d7fb1a65b 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -1,5 +1,26 @@ +@using Microsoft.AspNetCore.Html +@using Microsoft.AspNetCore.Mvc.Localization @model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings -
-
-
+

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

+ +
+ +
+ +
+ +
+ + @T["You can find your project information at https://dashboard.exactly.com/projects/."] + +
From 208950a6a7dac85e7476f87936fb45e8be90bd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 20 Mar 2024 15:06:47 +0100 Subject: [PATCH 13/90] Harmonize Stripe settings view with new changes. --- .../Views/StripeApiSettings.Edit.cshtml | 41 +++++++++---------- .../Views/_ViewImports.cshtml | 1 + 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml index faa386cd7..9d1451eeb 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml @@ -1,31 +1,28 @@ +@using Lombiq.HelpfulLibraries.OrchardCore.TagHelpers +@using Microsoft.AspNetCore.Html +@using Microsoft.AspNetCore.Mvc.Localization + @model OrchardCore.Commerce.Payment.Stripe.ViewModels.StripeApiSettingsViewModel +@{ + var emptyAfterSaving = T["The field will be empty after saving it, for security reasons."]; +} +

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

-
- - - - @T["Your Stripe's Publishable API key."] -
+
-
- - - - - @T["Your Stripe Secret API key. The field will be empty after saving it, for security reasons."] - -
+
-
- - - - - @T["Your Webhook Signing Secret key. The field will be empty after saving it, for security reasons."] - -
+
diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/_ViewImports.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/_ViewImports.cshtml index d5e5ec391..ad41f7b1b 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/_ViewImports.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/_ViewImports.cshtml @@ -6,6 +6,7 @@ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, OrchardCore.DisplayManagement @addTagHelper *, OrchardCore.ResourceManagement +@addTagHelper *, Lombiq.HelpfulLibraries.OrchardCore @using OrchardCore.Commerce.Payment.Stripe.Constants @using OrchardCore.Entities From 3c187037977cb754e38e2df5f62caaecef8dfb1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 20 Mar 2024 18:01:02 +0100 Subject: [PATCH 14/90] Clean up ChargeRequest initialization. --- .../Models/ChargeRequest.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index 1320db0bd..0e03a31f8 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -36,12 +36,12 @@ public ChargeRequest() { } - public ChargeRequest(OrderPart orderPart, User user, string projectId, string tenantId, Uri returnUrl) + public ChargeRequest(OrderPart orderPart, User user, string projectId, Uri returnUrl) { if (!returnUrl.IsAbsoluteUri) throw new ArgumentException("The return URL must be absolute.", nameof(returnUrl)); ProjectId = projectId; - ReferenceId = $"{tenantId}-{orderPart.OrderId.Text}"; + ReferenceId = orderPart.OrderId.Text; CustomerDescription = string.Join(", ", orderPart.Charges.Select(payment => $"{payment.Amount} × {payment.ChargeText}")); ReturnUrl = returnUrl.AbsoluteUri; CustomerId = user.UserId; @@ -63,7 +63,6 @@ public static async Task CreateUserAsync(OrderPart orderPart, Htt orderPart, await provider.GetRequiredService().GetFullUserAsync(context.User), provider.GetRequiredService>().Value.ProjectId, - context.Request.Host.Host, new Uri(new Uri(context.Request.GetDisplayUrl()), returnurl)); } } From 4e49e3418df78c8bfd84018a7faa5fc9ea63629b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 20 Mar 2024 18:01:35 +0100 Subject: [PATCH 15/90] Reference OrchardCore.Settings. --- Directory.Packages.props | 4 ++-- .../OrchardCore.Commerce.Payment.Exactly.csproj | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c90730d55..56d51664c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,7 +3,6 @@ true false - @@ -27,6 +26,7 @@ + @@ -36,4 +36,4 @@ - + \ No newline at end of file diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj b/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj index 079eb3c33..bb2552fea 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/OrchardCore.Commerce.Payment.Exactly.csproj @@ -29,6 +29,7 @@ + From 08e6225a1fbc9895456a1c4332f386f67383b910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 20 Mar 2024 18:02:11 +0100 Subject: [PATCH 16/90] Add button to verify API. --- .../Controllers/ExactlyController.cs | 76 ++++++++++++++++++- .../Drivers/ExactlySettingsDisplayDriver.cs | 11 ++- .../Models/ExactlySetings.cs | 4 +- .../Views/ExactlySettings.Edit.cshtml | 7 ++ 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index f5ff2e792..f31da16e7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,30 +1,49 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; using OrchardCore.Commerce.Abstractions.Exceptions; using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Exactly.Drivers; using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.Commerce.Payment.Exactly.Services; using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Mvc.Core.Utilities; using Refit; using System; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; +using AdminController=OrchardCore.Settings.Controllers.AdminController; namespace OrchardCore.Commerce.Payment.Exactly.Controllers; public class ExactlyController : Controller { private readonly IExactlyApi _api; + private readonly ILogger _logger; + private readonly INotifier _notifier; private readonly IPaymentService _paymentService; - private readonly IStringLocalizer H; + private readonly IHtmlLocalizer H; + private readonly IStringLocalizer S; - public ExactlyController(IExactlyApi api, IPaymentService paymentService, IStringLocalizer stringLocalizer) + public ExactlyController( + IExactlyApi api, + ILogger logger, + INotifier notifier, + IPaymentService paymentService, + IHtmlLocalizer htmlLocalizer, + IStringLocalizer stringLocalizer) { _api = api; + _logger = logger; + _notifier = notifier; _paymentService = paymentService; - H = stringLocalizer; + H = htmlLocalizer; + S = stringLocalizer; } [HttpPost] @@ -67,6 +86,55 @@ public async Task Middleware() throw new NotSupportedException("TODO"); } + public async Task VerifyApi() + { + try + { + var orderPart = new OrderPart + { + OrderId = + { + Text = Guid.NewGuid().ToString("D"), + }, + Charges = + { + new Payment.Models.Payment( + "card", + Guid.NewGuid().ToString("D"), + "Test Transaction", + new Amount(0, Currency.Euro), DateTime.UtcNow), + }, + }; + + var charge = await ChargeRequest.CreateUserAsync(orderPart, HttpContext); + using var result = await _api.CreateTransactionAsync(new ExactlyRequest { Attributes = charge }); + await result.EnsureSuccessStatusCodeAsync(); + result.Content!.ThrowIfHasErrors(); + + await _notifier.SuccessAsync(H["The Exactly API access works correctly ({0}).", JsonSerializer.Serialize(result.Content.Data)]); + } + catch (ApiException exception) + { + _logger.LogError(exception, "An API error was encountered."); + await _notifier.ErrorAsync(H["An API error was encountered: {0}", exception.Message]); + } + catch (FrontendException exception) + { + _logger.LogError(exception, "A front-end readable error was encountered."); + await _notifier.ErrorAsync(exception.HtmlMessage); + } + catch (Exception exception) + { + _logger.LogError(exception, "An unknown error was encountered."); + await _notifier.ErrorAsync(H["An unknown error was encountered: {0}", exception.ToString()]); + } + + return RedirectToAction( + nameof(AdminController.Index), + typeof(AdminController).ControllerName(), + new { area = "OrchardCore.Settings", groupId = ExactlySettingsDisplayDriver.EditorGroupId }); + } + private T ThrowIfError(ApiResponse> response) where T : IExactlyResponseData { @@ -76,7 +144,7 @@ private T ThrowIfError(ApiResponse> response) if (response.Error is { } error) throw new FrontendException(error.Message); if (response.Content is not { } content) { - throw new FrontendException(H["There was an unknown error while contacting with the payment service, please try again."]); + throw new FrontendException(S["There was an unknown error while contacting with the payment service, please try again."]); } return content.Data; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs index 0c913a6e4..bc1f902b0 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Authorization; +using Lombiq.HelpfulLibraries.OrchardCore.Contents; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using OrchardCore.Commerce.Payment.Exactly.Models; @@ -37,9 +38,11 @@ public ExactlySettingsDisplayDriver( public override async Task EditAsync(ExactlySettings section, BuildEditorContext context) => await AuthorizeAsync() - ? Initialize( - $"{nameof(ExactlySettings)}_Edit", - settings => _ssoSettings.CopyTo(settings, copyPassword: false)) + ? Initialize($"{nameof(ExactlySettings)}_Edit", settings => + { + _ssoSettings.CopyTo(settings); + settings.ApiKey = string.Empty; + }) .PlaceInContent() .OnGroup(EditorGroupId) : null; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs index b2f4cc62d..9eec41550 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs @@ -13,10 +13,10 @@ public class ExactlySettings public string ApiKey { get; set; } - public void CopyTo(ExactlySettings target, bool copyPassword = true) + public void CopyTo(ExactlySettings target) { if (Uri.TryCreate(BaseAddress, UriKind.Absolute, out var _)) target.BaseAddress = BaseAddress; if (!string.IsNullOrWhiteSpace(ProjectId)) target.ProjectId = ProjectId; - if (copyPassword && !string.IsNullOrWhiteSpace(ApiKey)) target.ApiKey = ApiKey; + if (!string.IsNullOrWhiteSpace(ApiKey)) target.ApiKey = ApiKey; } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index d7fb1a65b..b36263202 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -1,5 +1,7 @@ @using Microsoft.AspNetCore.Html @using Microsoft.AspNetCore.Mvc.Localization +@using OrchardCore +@using OrchardCore.Commerce.Payment.Exactly.Controllers @model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings

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

@@ -24,3 +26,8 @@ @T["You can find your project information at https://dashboard.exactly.com/projects/."]
+ + + @T["Verify currently saved API configuration"] + \ No newline at end of file From 90859e4cf656b55ff888ea36b484cb59f876d95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 20 Mar 2024 19:13:07 +0100 Subject: [PATCH 17/90] Wrap API actions in ExactlyService. --- .../Controllers/ExactlyController.cs | 57 +++++++++---------- .../Models/ExactlyDataWrapper.cs | 3 + .../Services/ExactlyService.cs | 43 +++++++++++++- .../Services/IExactlyApi.cs | 3 +- .../Services/IExactlyService.cs | 21 +++++++ .../Startup.cs | 2 +- .../Views/ExactlySettings.Edit.cshtml | 2 +- 7 files changed, 94 insertions(+), 37 deletions(-) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyDataWrapper.cs create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index f31da16e7..918214574 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; @@ -7,9 +8,9 @@ using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.Exactly.Drivers; -using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.Commerce.Payment.Exactly.Services; using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.Html; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Mvc.Core.Utilities; using Refit; @@ -23,7 +24,7 @@ namespace OrchardCore.Commerce.Payment.Exactly.Controllers; public class ExactlyController : Controller { - private readonly IExactlyApi _api; + private readonly IExactlyService _exactlyService; private readonly ILogger _logger; private readonly INotifier _notifier; private readonly IPaymentService _paymentService; @@ -31,14 +32,14 @@ public class ExactlyController : Controller private readonly IStringLocalizer S; public ExactlyController( - IExactlyApi api, + IExactlyService exactlyService, ILogger logger, INotifier notifier, IPaymentService paymentService, IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer) { - _api = api; + _exactlyService = exactlyService; _logger = logger; _notifier = notifier; _paymentService = paymentService; @@ -48,7 +49,7 @@ public ExactlyController( [HttpPost] public async Task CreateTransaction(string shoppingCartId) => - await this.SafeJsonAsync(async () => + await this.SafeJsonAsync(async () => { try { @@ -56,13 +57,11 @@ await this.SafeJsonAsync(async () => shoppingCartId, notifyOnError: false, throwOnError: true); - var request = await ChargeRequest.CreateUserAsync(order!.As(), HttpContext); - - return ThrowIfError(await _api.CreateTransactionAsync(request)); + return await _exactlyService.CreateTransactionAsync(order.As()); } - catch (FrontendException exception) + catch (Exception exception) { - return new { error = exception.Message, html = exception.HtmlMessage.Html() }; + return Error(exception); } }); @@ -71,8 +70,8 @@ await this.SafeJsonAsync(async () => { try { - var content = ThrowIfError(await _api.GetTransactionDetailsAsync(transactionId)); - return content.Attributes.Actions.First(action => action.Action != null).Action; + var result = await _exactlyService.GetTransactionDetailsAsync(transactionId); + return result.Attributes.Actions.First(action => action.Action != null).Action; } catch (FrontendException exception) { @@ -102,16 +101,13 @@ public async Task VerifyApi() "card", Guid.NewGuid().ToString("D"), "Test Transaction", - new Amount(0, Currency.Euro), DateTime.UtcNow), + new Amount(1, Currency.Euro), + DateTime.UtcNow), }, }; - var charge = await ChargeRequest.CreateUserAsync(orderPart, HttpContext); - using var result = await _api.CreateTransactionAsync(new ExactlyRequest { Attributes = charge }); - await result.EnsureSuccessStatusCodeAsync(); - result.Content!.ThrowIfHasErrors(); - - await _notifier.SuccessAsync(H["The Exactly API access works correctly ({0}).", JsonSerializer.Serialize(result.Content.Data)]); + var result = await _exactlyService.CreateTransactionAsync(orderPart); + await _notifier.SuccessAsync(H["The Exactly API access works correctly ({0}).", JsonSerializer.Serialize(result)]); } catch (ApiException exception) { @@ -135,19 +131,18 @@ public async Task VerifyApi() new { area = "OrchardCore.Settings", groupId = ExactlySettingsDisplayDriver.EditorGroupId }); } - private T ThrowIfError(ApiResponse> response) - where T : IExactlyResponseData + private object Error(Exception exception) { - using (response) + if (exception is FrontendException frontendException) { - response.Content?.ThrowIfHasErrors(); - if (response.Error is { } error) throw new FrontendException(error.Message); - if (response.Content is not { } content) - { - throw new FrontendException(S["There was an unknown error while contacting with the payment service, please try again."]); - } - - return content.Data; + return Error(exception.Message, frontendException.HtmlMessage.Html()); } + + return Error(HttpContext.IsDevelopmentAndLocalhost() + ? exception.Message + : S["There was an unknown error while contacting with the payment service, please try again."]); } + + private object Error(string text, string html = null) => + new { error = text, html = html ?? new HtmlContentString(text).Html() }; } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyDataWrapper.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyDataWrapper.cs new file mode 100644 index 000000000..b0a2ce7f9 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyDataWrapper.cs @@ -0,0 +1,3 @@ +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +public record ExactlyDataWrapper(T Data); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index d9dc7fbe2..c66e67087 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -1,6 +1,43 @@ -namespace OrchardCore.Commerce.Payment.Exactly.Services; +using Microsoft.AspNetCore.Http; +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Payment.Exactly.Models; +using Refit; +using System.Threading.Tasks; -public class ExactlyService +namespace OrchardCore.Commerce.Payment.Exactly.Services; + +public class ExactlyService : IExactlyService { - + private readonly IExactlyApi _api; + private readonly IHttpContextAccessor _hca; + + public ExactlyService(IExactlyApi api, IHttpContextAccessor hca) + { + _api = api; + _hca = hca; + } + + public async Task CreateTransactionAsync(OrderPart orderPart) + { + var charge = await ChargeRequest.CreateUserAsync(orderPart, _hca.HttpContext); + var request = new ExactlyDataWrapper>( + new ExactlyRequest { Attributes = charge }); + + using var result = await _api.CreateTransactionAsync(request); + return await EvaluateResultAsync(result); + } + + public async Task GetTransactionDetailsAsync(string transactionId) + { + using var result = await _api.GetTransactionDetailsAsync(transactionId); + return await EvaluateResultAsync(result); + } + + private async Task EvaluateResultAsync(ApiResponse> result) + { + await result.EnsureSuccessStatusCodeAsync(); + result.Content!.ThrowIfHasErrors(); + + return result.Content.Data; + } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs index 9444ac613..6c7904974 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs @@ -17,7 +17,8 @@ public interface IExactlyApi /// callback/webhook must be used to retrieve status of the transaction when it's completed. /// [Post("/api/v1/transactions")] - Task>> CreateTransactionAsync([Body] ExactlyRequest data); + Task>> CreateTransactionAsync( + [Body(buffered: true)] ExactlyDataWrapper> data); /// /// Returns details of a transaction. diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs new file mode 100644 index 000000000..552a5b185 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs @@ -0,0 +1,21 @@ +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.Payment.Exactly.Models; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.Payment.Exactly.Services; + +/// +/// A service for accessing the Exactly API. +/// +public interface IExactlyService +{ + /// + /// Creates a new transaction for the current user based on the provided order. + /// + Task CreateTransactionAsync(OrderPart orderPart); + + /// + /// Returns details of a transaction. + /// + Task GetTransactionDetailsAsync(string transactionId); +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs index 8be4a186f..a52b02da7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Startup.cs @@ -26,7 +26,7 @@ public override void ConfigureServices(IServiceCollection services) { // Payment services services.AddScoped(); - //services.AddScoped(); + services.AddScoped(); // Configuration, permission, admin things services.Configure(_shellConfiguration.GetSection("OrchardCoreCommerce_Payment_Exactly")); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index b36263202..6161e42ec 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -28,6 +28,6 @@ + class="btn btn-sm btn-primary mb-3"> @T["Verify currently saved API configuration"] \ No newline at end of file From 821189f168ccdb0528a472a2fc989ccef4cb1cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 23 Mar 2024 21:40:00 +0100 Subject: [PATCH 18/90] Better error handling. --- .../Controllers/ExactlyController.cs | 33 +++++++++---------- .../Models/ChargeRequest.cs | 2 +- .../Services/ExactlyService.cs | 26 ++++++++++++--- .../Views/ExactlySettings.Edit.cshtml | 19 +++++++---- 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 918214574..11d026d50 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -18,12 +18,14 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using AdminController=OrchardCore.Settings.Controllers.AdminController; +using AdminController = OrchardCore.Settings.Controllers.AdminController; +using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; namespace OrchardCore.Commerce.Payment.Exactly.Controllers; public class ExactlyController : Controller { + private readonly IContentManager _contentManager; private readonly IExactlyService _exactlyService; private readonly ILogger _logger; private readonly INotifier _notifier; @@ -32,6 +34,7 @@ public class ExactlyController : Controller private readonly IStringLocalizer S; public ExactlyController( + IContentManager contentManager, IExactlyService exactlyService, ILogger logger, INotifier notifier, @@ -39,6 +42,7 @@ public ExactlyController( IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer) { + _contentManager = contentManager; _exactlyService = exactlyService; _logger = logger; _notifier = notifier; @@ -89,24 +93,19 @@ public async Task VerifyApi() { try { - var orderPart = new OrderPart + var order = await _contentManager.NewAsync(Order); + order.Alter(part => { - OrderId = - { - Text = Guid.NewGuid().ToString("D"), - }, - Charges = - { - new Payment.Models.Payment( - "card", - Guid.NewGuid().ToString("D"), - "Test Transaction", - new Amount(1, Currency.Euro), - DateTime.UtcNow), - }, - }; + part.OrderId.Text = Guid.NewGuid().ToString("D"); + part.Charges.Add(new Payment.Models.Payment( + "card", + Guid.NewGuid().ToString("D"), + "Test Transaction", + new Amount(1, Currency.Euro), + DateTime.UtcNow)); + }); - var result = await _exactlyService.CreateTransactionAsync(orderPart); + var result = await _exactlyService.CreateTransactionAsync(order.As()); await _notifier.SuccessAsync(H["The Exactly API access works correctly ({0}).", JsonSerializer.Serialize(result)]); } catch (ApiException exception) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index 0e03a31f8..fb08d39a9 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -46,7 +46,7 @@ public ChargeRequest(OrderPart orderPart, User user, string projectId, Uri retur ReturnUrl = returnUrl.AbsoluteUri; CustomerId = user.UserId; Email = user.Email; - Meta = orderPart; + Meta = orderPart.ContentItem.ContentItemId; this.SetAmount(orderPart.Charges.Select(payment => payment.Amount).Sum()); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index c66e67087..558b562d5 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Payment.Exactly.Models; using Refit; +using System; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Services; @@ -24,18 +26,34 @@ public async Task CreateTransactionAsync(OrderPart orderPart) new ExactlyRequest { Attributes = charge }); using var result = await _api.CreateTransactionAsync(request); - return await EvaluateResultAsync(result); + return EvaluateResult(result); } public async Task GetTransactionDetailsAsync(string transactionId) { using var result = await _api.GetTransactionDetailsAsync(transactionId); - return await EvaluateResultAsync(result); + return EvaluateResult(result); } - private async Task EvaluateResultAsync(ApiResponse> result) + private T EvaluateResult(IApiResponse> result) + where T : IExactlyResponseData { - await result.EnsureSuccessStatusCodeAsync(); + // If the request is not successful, try to parse the response error and throw a more specific FrontendException + // instead of the ApiException. + if (result.Error?.Content is { } error && error.StartsWith('{')) + { + try + { + var content = JsonConvert.DeserializeObject>(error); + content.ThrowIfHasErrors(); + } + catch + { + throw result.Error; + } + } + + // In the unlikely case that the HTTP response is success but there was still an error somehow. result.Content!.ThrowIfHasErrors(); return result.Content.Data; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 6161e42ec..dfa9244ef 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -4,6 +4,18 @@ @using OrchardCore.Commerce.Payment.Exactly.Controllers @model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings +
+ + + @T["Warning: pressing this button won't save any edits you made on this page. Save before pressing it."] + +
+

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

@T["You can find your project information at https://dashboard.exactly.com/projects/."] - - - - @T["Verify currently saved API configuration"] - \ No newline at end of file + \ No newline at end of file From 6c8929a319c6fef8916308ac0e6c593703d961e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 23 Mar 2024 21:53:25 +0100 Subject: [PATCH 19/90] Fix error output. --- .../Models/ExactlyResponse.cs | 5 ++++- .../Services/ExactlyService.cs | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs index 1655ddcc1..8d7b9ccfa 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs @@ -14,7 +14,10 @@ public void ThrowIfHasErrors() { if (Errors?.Any() != true) return; - var errors = Errors.Select(error => $"{error.Code}: {error.Title} ({error.Details})").ToList(); + var errors = Errors + .Select(error => $"{error.Code}: {error.Title} ({error.Details?.Trim()})".Replace(" ()", string.Empty)) + .ToList(); + FrontendException.ThrowIfAny(errors); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index 558b562d5..18812c125 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; +using OrchardCore.Commerce.Abstractions.Exceptions; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Payment.Exactly.Models; using Refit; -using System; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Services; @@ -47,6 +47,10 @@ private T EvaluateResult(IApiResponse> result) var content = JsonConvert.DeserializeObject>(error); content.ThrowIfHasErrors(); } + catch (FrontendException frontendException) + { + throw; + } catch { throw result.Error; From 4321a76bb4a5065e0c071d56c2d28bfdb1c02441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 23 Mar 2024 22:02:06 +0100 Subject: [PATCH 20/90] Remove TokenizeSource since it doesn't work anyway. --- .../OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index fb08d39a9..4057907ab 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -28,7 +28,6 @@ public class ChargeRequest : IExactlyRequestAttributes, IExactlyAmount public string ReturnUrl { get; set; } public string CustomerId { get; set; } public string Email { get; set; } - public bool TokenizeSource { get; set; } = true; public int Lifetime { get; set; } = 3600; public object Meta { get; set; } From 709e3acd072eacec2f0dacee2e219f472b5d045b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 25 Mar 2024 06:10:06 +0100 Subject: [PATCH 21/90] even better error reporting --- .../Controllers/ExactlyController.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 11d026d50..793082efd 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; @@ -121,7 +122,10 @@ public async Task VerifyApi() catch (Exception exception) { _logger.LogError(exception, "An unknown error was encountered."); - await _notifier.ErrorAsync(H["An unknown error was encountered: {0}", exception.ToString()]); + + var error = exception.ToString(); + var html = $"{H["An unknown error was encountered:"].Html()}
{error.Replace("\n", "
")}"; + await _notifier.ErrorAsync(new LocalizedHtmlString(html, html)); } return RedirectToAction( From ceca7a3c844c3952a6ffe7c13934cf12dcfe75df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 25 Mar 2024 06:15:49 +0100 Subject: [PATCH 22/90] bug fix. --- .../Services/ExactlyService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index 18812c125..002ff103a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -57,6 +57,9 @@ private T EvaluateResult(IApiResponse> result) } } + // Handle any other non-specific ApiExceptions. + if (result.Error is { } apiException) throw apiException; + // In the unlikely case that the HTTP response is success but there was still an error somehow. result.Content!.ThrowIfHasErrors(); From c7f59324180d4844d1c38ec43145edc9326a62eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 30 Mar 2024 20:27:29 +0100 Subject: [PATCH 23/90] Code cleanup. --- .../Controllers/ExactlyController.cs | 5 +---- .../Services/ExactlyService.cs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 793082efd..ba18e4a63 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -85,10 +85,7 @@ await this.SafeJsonAsync(async () => }); [HttpGet("checkout/middleware/Exactly")] - public async Task Middleware() - { - throw new NotSupportedException("TODO"); - } + public Task Middleware() => throw new NotSupportedException("TODO"); public async Task VerifyApi() { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index 002ff103a..7c57403af 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -47,7 +47,7 @@ private T EvaluateResult(IApiResponse> result) var content = JsonConvert.DeserializeObject>(error); content.ThrowIfHasErrors(); } - catch (FrontendException frontendException) + catch (FrontendException) { throw; } From 53d87795b3e534bfe26818281ebfc451b8df068c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 30 Mar 2024 22:53:14 +0100 Subject: [PATCH 24/90] Return a dummy result in case the server checks it. --- .../Controllers/ExactlyController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index ba18e4a63..bb95e9349 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -85,7 +85,8 @@ await this.SafeJsonAsync(async () => }); [HttpGet("checkout/middleware/Exactly")] - public Task Middleware() => throw new NotSupportedException("TODO"); + public Task Middleware() => + Task.FromResult(Json(new { Request = Request.ToString() })); public async Task VerifyApi() { From 807c4ad6cac9ac0b8a61d628ca3dee8566b1a2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 3 Apr 2024 12:22:10 +0200 Subject: [PATCH 25/90] Fix actions and add content-type workaround. --- .../Controllers/ExactlyController.cs | 24 ++++++++++++++++--- .../Models/ChargeAction.cs | 19 +++++++++++++++ .../Models/ChargeResponse.cs | 6 ----- .../Services/ExactlyApiHandler.cs | 12 ++++++++++ .../Services/ExactlyService.cs | 24 ++++++++++++++++--- .../Services/IExactlyApi.cs | 1 - .../Services/IExactlyService.cs | 6 ++++- 7 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index bb95e9349..0002739aa 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -9,6 +9,7 @@ using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.Exactly.Drivers; +using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.Commerce.Payment.Exactly.Services; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.Html; @@ -75,8 +76,7 @@ await this.SafeJsonAsync(async () => { try { - var result = await _exactlyService.GetTransactionDetailsAsync(transactionId); - return result.Attributes.Actions.First(action => action.Action != null).Action; + return await GetActionRedirectRequested(transactionId); } catch (FrontendException exception) { @@ -105,7 +105,11 @@ public async Task VerifyApi() }); var result = await _exactlyService.CreateTransactionAsync(order.As()); - await _notifier.SuccessAsync(H["The Exactly API access works correctly ({0}).", JsonSerializer.Serialize(result)]); + var action = await GetActionRedirectRequested(result.Id); + + await _notifier.SuccessAsync( + H["The Exactly API access works correctly. You can test the redirection by clicking here", + action.Url]); } catch (ApiException exception) { @@ -132,6 +136,20 @@ public async Task VerifyApi() new { area = "OrchardCore.Settings", groupId = ExactlySettingsDisplayDriver.EditorGroupId }); } + private async Task GetActionRedirectRequested(string transactionId) + { + + var result = await _exactlyService.GetTransactionDetailsAsync( + transactionId, + ChargeResponse.ChargeResponseStatus.ActionRequired, + HttpContext.RequestAborted); + return result + .Attributes + .Actions + .Select(action => action.Attributes) + .First(action => action.Action == "redirect-required" && action.HttpMethod == "GET"); + } + private object Error(Exception exception) { if (exception is FrontendException frontendException) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs new file mode 100644 index 000000000..74f892892 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace OrchardCore.Commerce.Payment.Exactly.Models; + +public class ChargeAction : IExactlyResponseData +{ + public string Type => "action"; + + public ChargeActionAttributes Attributes { get; set; } + + public class ChargeActionAttributes + { + public string Action { get; set; } + public Uri Url { get; set; } + public IList Parameters { get; set; } + public string HttpMethod { get; set; } + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs index 0059a4cf6..888a649cd 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs @@ -54,12 +54,6 @@ public class ChargeProcessing : IExactlyAmount public string ResultCode { get; set; } } - public class ChargeAction - { - public string Action { get; set; } - public object RequiredAttributes { get; set; } - } - public class ChargeAttributes { public string ProjectId { get; set; } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyApiHandler.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyApiHandler.cs index c5d311d62..9d82ef8b7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyApiHandler.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyApiHandler.cs @@ -1,6 +1,9 @@ using Microsoft.Extensions.Options; using OrchardCore.Commerce.Payment.Exactly.Models; +using System; using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -16,6 +19,15 @@ protected override Task SendAsync(HttpRequestMessage reques { request.Headers.Add("Authorization", "Api-Key " + _settings.ApiKey); + // Has to be applied to every request. Even to GET requests, which is not permitted by .NET so it has to be + // hacked in. +#pragma warning disable S3011 + const BindingFlags privateFieldFlags = BindingFlags.Instance | BindingFlags.NonPublic; + var allowedHeaderTypes = typeof(HttpHeaders).GetField("_allowedHeaderTypes", privateFieldFlags); + allowedHeaderTypes!.SetValue(request.Headers, Enum.Parse(allowedHeaderTypes.FieldType, "All")); + request.Headers.Add("Content-Type", "application/vnd.api+json"); +#pragma warning restore S3011 + return base.SendAsync(request, cancellationToken); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index 7c57403af..5c6006232 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -4,6 +4,8 @@ using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Payment.Exactly.Models; using Refit; +using System; +using System.Threading; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Services; @@ -29,10 +31,26 @@ public async Task CreateTransactionAsync(OrderPart orderPart) return EvaluateResult(result); } - public async Task GetTransactionDetailsAsync(string transactionId) + public async Task GetTransactionDetailsAsync( + string transactionId, + ChargeResponse.ChargeResponseStatus? waitForStatus = null, + CancellationToken cancellationToken = default) { - using var result = await _api.GetTransactionDetailsAsync(transactionId); - return EvaluateResult(result); + ArgumentNullException.ThrowIfNull(transactionId); + + for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) + { + using var result = await _api.GetTransactionDetailsAsync(transactionId); + var content = EvaluateResult(result); + + if (waitForStatus == null || content.Attributes.Status == waitForStatus) return content; + + await Task.Delay(100, cancellationToken); + } + + throw new TimeoutException( + $"Couldn't get the transaction \"{transactionId}\" with the status \"{waitForStatus}\" within " + + $"the expected timeframe."); } private T EvaluateResult(IApiResponse> result) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs index 6c7904974..9b2a189e4 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs @@ -8,7 +8,6 @@ namespace OrchardCore.Commerce.Payment.Exactly.Services; /// Wrapper for the Exactly API. See here. All method documentation is /// copied from this source. The authorization API key is provided by an /// -[Headers("Content-Type: application/vnd.api+json")] public interface IExactlyApi { /// diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs index 552a5b185..793f95ed1 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs @@ -1,5 +1,6 @@ using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Payment.Exactly.Models; +using System.Threading; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Services; @@ -17,5 +18,8 @@ public interface IExactlyService /// /// Returns details of a transaction. /// - Task GetTransactionDetailsAsync(string transactionId); + Task GetTransactionDetailsAsync( + string transactionId, + ChargeResponse.ChargeResponseStatus? waitForStatus = null, + CancellationToken cancellationToken = default); } From 45786ddf2806e43d9818ad4cff2a9593645d4d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Apr 2024 01:23:44 +0200 Subject: [PATCH 26/90] listen to all URL --- src/OrchardCore.Commerce.Web/Properties/launchSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore.Commerce.Web/Properties/launchSettings.json b/src/OrchardCore.Commerce.Web/Properties/launchSettings.json index 2f04421f3..b669cb76a 100644 --- a/src/OrchardCore.Commerce.Web/Properties/launchSettings.json +++ b/src/OrchardCore.Commerce.Web/Properties/launchSettings.json @@ -22,7 +22,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + "applicationUrl": "https://*:5001;http://*:5000" } } } From f62f7755c33b3c922db3598c10cf85f23312d5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Apr 2024 01:24:00 +0200 Subject: [PATCH 27/90] disable test button on field edit --- .../Views/ExactlySettings.Edit.cshtml | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index dfa9244ef..70f95f548 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -6,13 +6,14 @@
- @T["Warning: pressing this button won't save any edits you made on this page. Save before pressing it."] + @T["Warning: pressing this button won't save the current page. If you have edited any fields it gets disabled."]
@@ -37,4 +38,18 @@ @T["You can find your project information at https://dashboard.exactly.com/projects/."] - \ No newline at end of file + + + \ No newline at end of file From 832535a4b0d01c2fab6ab71bcf942b0824c5cb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Apr 2024 01:39:12 +0200 Subject: [PATCH 28/90] Show payment button if the site settings are filled. --- .../Models/ExactlySetings.cs | 6 +++--- .../Services/ExactlyPaymentProvider.cs | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs index 9eec41550..1b704f2db 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs @@ -15,8 +15,8 @@ public class ExactlySettings public void CopyTo(ExactlySettings target) { - if (Uri.TryCreate(BaseAddress, UriKind.Absolute, out var _)) target.BaseAddress = BaseAddress; - if (!string.IsNullOrWhiteSpace(ProjectId)) target.ProjectId = ProjectId; - if (!string.IsNullOrWhiteSpace(ApiKey)) target.ApiKey = ApiKey; + if (Uri.TryCreate(BaseAddress, UriKind.Absolute, out var baseUri)) target.BaseAddress = baseUri.AbsoluteUri; + if (!string.IsNullOrWhiteSpace(ProjectId)) target.ProjectId = ProjectId.Trim(); + if (!string.IsNullOrWhiteSpace(ApiKey)) target.ApiKey = ApiKey.Trim(); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs index 999a1560c..8c1b43ff8 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs @@ -1,23 +1,35 @@ using Microsoft.AspNetCore.Mvc; using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.ContentManagement; +using OrchardCore.Settings; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Services; public class ExactlyPaymentProvider : IPaymentProvider { + private readonly IExactlyService _exactlyService; + private readonly ISiteService _siteService; public const string ProviderName = "Exactly"; public string Name => ProviderName; - public ExactlyPaymentProvider() + public ExactlyPaymentProvider( + IExactlyService exactlyService, + ISiteService siteService) { + _exactlyService = exactlyService; + _siteService = siteService; + } + + public async Task CreatePaymentProviderDataAsync(IPaymentViewModel model) + { + var settings = (await _siteService.GetSiteSettingsAsync())?.As(); + return string.IsNullOrEmpty(settings?.ApiKey) || string.IsNullOrEmpty(settings.ProjectId) ? null : new object(); } - public Task CreatePaymentProviderDataAsync(IPaymentViewModel model) => - throw new System.NotImplementedException(); public Task UpdateAndRedirectToFinishedOrderAsync( Controller controller, ContentItem order, From 9d8cc3e3705d378ff466ea8de8e290afc749ae16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Apr 2024 02:25:54 +0200 Subject: [PATCH 29/90] Fix multiple errors formatting. --- .../Views/CheckoutExactly.cshtml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml index ff5c68e37..cfc5cabf3 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml @@ -33,7 +33,17 @@ const json = await fetchJson(@actionUrl.JsonHtmlContent(), { method: 'post', body: new FormData(form) }); if (!json || json.error) { - alert(json?.error ?? @defaultErrorMessage.Json()); + let error = json?.error ?? @defaultErrorMessage.Json(); + + if (json?.html) { + const div = document.createElement('div'); + div.innerHTML = json.html; + document.body.append(div); + error = div.innerText.trim(); + document.body.removeChild(div); + } + + alert(error); } else { const statusUrl = (@statusUrl.JsonHtmlContent()).replace('TRANSACTION_ID', json.id); From 8bf541b5faae591a8beb3d0acebc926fee41a28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Apr 2024 02:26:36 +0200 Subject: [PATCH 30/90] Remove duplicate errors. --- .../Models/ExactlyResponse.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs index 8d7b9ccfa..56aa6dd80 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs @@ -16,6 +16,7 @@ public void ThrowIfHasErrors() var errors = Errors .Select(error => $"{error.Code}: {error.Title} ({error.Details?.Trim()})".Replace(" ()", string.Empty)) + .Distinct() .ToList(); FrontendException.ThrowIfAny(errors); From e59172d41d9bfbd1a8f9569b5ea4ba42d1222742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Apr 2024 21:36:23 +0200 Subject: [PATCH 31/90] Revert FrontendException to main branch. --- .../Exceptions/FrontendException.cs | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs index 9821407b8..985ea41c5 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs @@ -1,9 +1,5 @@ -using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Localization; using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; namespace OrchardCore.Commerce.Abstractions.Exceptions; @@ -27,30 +23,4 @@ public FrontendException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// If the provided collection of is not empty, it throws an exception with the included - /// texts. If there are multiple, they are merged with a HTML line break. - /// - /// The possible collection of error texts. - public static void ThrowIfAny([AllowNull] ICollection errors) - { - if (errors == null || errors.Count == 0) return; - - if (errors.Count == 1) throw new FrontendException(errors.Single()); - - throw new FrontendException(new HtmlString("
").Join( - errors.Select(error => new LocalizedHtmlString(error, error)).ToArray())); - } - - /// - public static void ThrowIfAny([AllowNull] ICollection errors) - { - if (errors == null || errors.Count == 0) return; - - if (errors.Count == 1) throw new FrontendException(errors.Single()); - - var errorsArray = errors.ToArray(); - throw new FrontendException(new HtmlString("
").Join(errorsArray)); - } } From 0afe5fb400264c62e25a7286d128e25146dfb902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 6 Apr 2024 21:36:37 +0200 Subject: [PATCH 32/90] Update HL and fix FrontendException usage. --- Directory.Packages.props | 4 ++-- .../Exceptions/FrontendException.cs | 1 + .../Controllers/PaymentController.cs | 4 ++-- .../OrchardCore.Commerce.Payment/Services/PaymentService.cs | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 56d51664c..4cec6d366 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,8 +5,8 @@ - - + + diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs index 985ea41c5..8f0e1d955 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Exceptions/FrontendException.cs @@ -3,6 +3,7 @@ namespace OrchardCore.Commerce.Abstractions.Exceptions; +[Obsolete("Use the version in Lombiq.HelpfulLibraries.AspNetCore.Exceptions instead.")] public class FrontendException : Exception { public LocalizedHtmlString HtmlMessage { get; } diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index 908796378..17f157449 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging; using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Abstractions.Constants; -using OrchardCore.Commerce.Abstractions.Exceptions; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.MoneyDataType.Abstractions; @@ -24,6 +23,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Payment.Controllers; @@ -146,7 +146,7 @@ await _paymentProviders } catch (FrontendException exception) { - return Json(new { Errors = new[] { exception.HtmlMessage.Html() } }); + return Json(new { Errors = exception.HtmlMessages }); } catch (Exception exception) { diff --git a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs index 3db5696a7..0c4d6c76a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs @@ -1,3 +1,4 @@ +using Lombiq.HelpfulLibraries.AspNetCore.Exceptions; using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; @@ -5,7 +6,6 @@ using Microsoft.AspNetCore.Mvc.Localization; 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; From bb9f9d93b5838994d88366a180f5603b3eb1a2f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sun, 7 Apr 2024 01:38:23 +0200 Subject: [PATCH 33/90] Use the FrontendException from HL everywhere. --- Directory.Packages.props | 4 +- .../Controllers/ExactlyController.cs | 55 ++++--------------- .../Models/ExactlyResponse.cs | 4 +- .../Services/ExactlyService.cs | 1 + .../Controllers/ShoppingCartController.cs | 5 +- .../Drivers/DiscountPartDisplayDriver.cs | 5 +- .../Drivers/TaxRateTaxPartDisplayDriver.cs | 5 +- .../Services/ShoppingCartHelpers.cs | 2 +- 8 files changed, 25 insertions(+), 56 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4cec6d366..8f796aca0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,8 +5,8 @@ - - + + diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 0002739aa..c454fb09d 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,6 +1,4 @@ -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; @@ -12,17 +10,17 @@ using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.Commerce.Payment.Exactly.Services; using OrchardCore.ContentManagement; -using OrchardCore.DisplayManagement.Html; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Mvc.Core.Utilities; using Refit; using System; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; -using AdminController = OrchardCore.Settings.Controllers.AdminController; + using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; +using AdminController = OrchardCore.Settings.Controllers.AdminController; + namespace OrchardCore.Commerce.Payment.Exactly.Controllers; public class ExactlyController : Controller @@ -57,32 +55,15 @@ public ExactlyController( public async Task CreateTransaction(string shoppingCartId) => await this.SafeJsonAsync(async () => { - try - { - var order = await _paymentService.CreatePendingOrderFromShoppingCartAsync( - shoppingCartId, - notifyOnError: false, - throwOnError: true); - return await _exactlyService.CreateTransactionAsync(order.As()); - } - catch (Exception exception) - { - return Error(exception); - } + var order = await _paymentService.CreatePendingOrderFromShoppingCartAsync( + shoppingCartId, + notifyOnError: false, + throwOnError: true); + return await _exactlyService.CreateTransactionAsync(order.As()); }); public async Task GetRedirectUrl(string transactionId) => - await this.SafeJsonAsync(async () => - { - try - { - return await GetActionRedirectRequested(transactionId); - } - catch (FrontendException exception) - { - return new { error = exception.Message, html = exception.HtmlMessage.Html() }; - } - }); + await this.SafeJsonAsync(async () => await GetActionRedirectRequested(transactionId)); [HttpGet("checkout/middleware/Exactly")] public Task Middleware() => @@ -138,7 +119,6 @@ await _notifier.SuccessAsync( private async Task GetActionRedirectRequested(string transactionId) { - var result = await _exactlyService.GetTransactionDetailsAsync( transactionId, ChargeResponse.ChargeResponseStatus.ActionRequired, @@ -149,19 +129,4 @@ await _notifier.SuccessAsync( .Select(action => action.Attributes) .First(action => action.Action == "redirect-required" && action.HttpMethod == "GET"); } - - private object Error(Exception exception) - { - if (exception is FrontendException frontendException) - { - return Error(exception.Message, frontendException.HtmlMessage.Html()); - } - - return Error(HttpContext.IsDevelopmentAndLocalhost() - ? exception.Message - : S["There was an unknown error while contacting with the payment service, please try again."]); - } - - private object Error(string text, string html = null) => - new { error = text, html = html ?? new HtmlContentString(text).Html() }; } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs index 56aa6dd80..deb464f75 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs @@ -1,6 +1,6 @@ -using OrchardCore.Commerce.Abstractions.Exceptions; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Payment.Exactly.Models; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index 5c6006232..21653c69a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -7,6 +7,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Payment.Exactly.Services; diff --git a/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs b/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs index cdeec109e..9bb55bf66 100644 --- a/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs +++ b/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs @@ -1,10 +1,10 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Validation; using Lombiq.HelpfulLibraries.OrchardCore.Workflow; using Microsoft.AspNetCore.Http.Features; 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.Inventory.Models; @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Controllers; @@ -197,7 +198,7 @@ await _workflowManagers.TriggerEventAsync( } catch (FrontendException exception) { - await _notifier.ErrorAsync(exception.HtmlMessage); + await _notifier.FrontEndErrorAsync(exception); } return RedirectToAction(nameof(Index), new { shoppingCartId }); diff --git a/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs index fc4e3a7ef..e937cd604 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 Lombiq.HelpfulLibraries.OrchardCore.Validation; using OrchardCore.Commerce.Abstractions.Abstractions; -using OrchardCore.Commerce.Abstractions.Exceptions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Promotion.Extensions; @@ -15,6 +15,7 @@ using OrchardCore.DisplayManagement.Views; using System.Linq; using System.Threading.Tasks; +using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Drivers; @@ -104,7 +105,7 @@ public override async Task DisplayAsync(ProductPart part, BuildP } catch (FrontendException exception) { - await _notifier.ErrorAsync(exception.HtmlMessage); + await _notifier.FrontEndErrorAsync(exception); return null; } } diff --git a/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs index 9dd4fbdf4..47d5bf7b3 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs @@ -1,7 +1,7 @@ using Lombiq.HelpfulLibraries.OrchardCore.Contents; +using Lombiq.HelpfulLibraries.OrchardCore.Validation; using Microsoft.AspNetCore.Http; using OrchardCore.Commerce.Abstractions.Abstractions; -using OrchardCore.Commerce.Abstractions.Exceptions; using OrchardCore.Commerce.Models; using OrchardCore.Commerce.Tax.Extensions; using OrchardCore.Commerce.Tax.Models; @@ -12,6 +12,7 @@ using OrchardCore.DisplayManagement.Notify; using OrchardCore.DisplayManagement.Views; using System.Threading.Tasks; +using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Drivers; @@ -58,7 +59,7 @@ public override async Task DisplayAsync(TaxPart part, BuildPartD } catch (FrontendException exception) { - await _notifier.ErrorAsync(exception.HtmlMessage); + await _notifier.FrontEndErrorAsync(exception); return null; } } diff --git a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs index e490e547f..ca0902945 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs @@ -1,8 +1,8 @@ +using Lombiq.HelpfulLibraries.AspNetCore.Exceptions; 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; From 1cdb9521ed9d90cd054b86ce4e1589d6fd76c267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sun, 7 Apr 2024 02:12:05 +0200 Subject: [PATCH 34/90] Code cleanup. --- .../Controllers/ExactlyController.cs | 18 +++++++++--------- .../Drivers/ExactlySettingsDisplayDriver.cs | 3 +-- .../Models/ChargeAction.cs | 2 +- .../Models/ChargeResponse.cs | 10 +++++----- .../Models/ExactlyResponse.cs | 6 ++---- .../Models/IExactlyAmount.cs | 8 ++++---- .../Services/ExactlyPaymentProvider.cs | 3 ++- .../Services/ExactlyService.cs | 8 +++++--- .../Services/ExactlySettingsConfiguration.cs | 1 - .../Services/IExactlyApi.cs | 2 +- .../Controllers/PaymentController.cs | 3 ++- .../Controllers/ShoppingCartController.cs | 3 ++- .../Drivers/DiscountPartDisplayDriver.cs | 3 ++- .../Drivers/TaxRateTaxPartDisplayDriver.cs | 3 ++- 14 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index c454fb09d..80a822e9d 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,8 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Lombiq.HelpfulLibraries.OrchardCore.Validation; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; -using OrchardCore.Commerce.Abstractions.Exceptions; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Payment.Abstractions; @@ -16,10 +16,10 @@ using System; using System.Linq; using System.Threading.Tasks; - using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; using AdminController = OrchardCore.Settings.Controllers.AdminController; +using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Payment.Exactly.Controllers; @@ -52,6 +52,7 @@ public ExactlyController( } [HttpPost] + [ValidateAntiForgeryToken] public async Task CreateTransaction(string shoppingCartId) => await this.SafeJsonAsync(async () => { @@ -63,7 +64,7 @@ await this.SafeJsonAsync(async () => }); public async Task GetRedirectUrl(string transactionId) => - await this.SafeJsonAsync(async () => await GetActionRedirectRequested(transactionId)); + await this.SafeJsonAsync(async () => await GetActionRedirectRequestedAsync(transactionId)); [HttpGet("checkout/middleware/Exactly")] public Task Middleware() => @@ -86,11 +87,10 @@ public async Task VerifyApi() }); var result = await _exactlyService.CreateTransactionAsync(order.As()); - var action = await GetActionRedirectRequested(result.Id); + var action = await GetActionRedirectRequestedAsync(result.Id); await _notifier.SuccessAsync( - H["The Exactly API access works correctly. You can test the redirection by clicking here", - action.Url]); + H["The Exactly API access works correctly. You can test the redirection by clicking here", action.Url]); } catch (ApiException exception) { @@ -100,7 +100,7 @@ await _notifier.SuccessAsync( catch (FrontendException exception) { _logger.LogError(exception, "A front-end readable error was encountered."); - await _notifier.ErrorAsync(exception.HtmlMessage); + await _notifier.FrontEndErrorAsync(exception); } catch (Exception exception) { @@ -117,7 +117,7 @@ await _notifier.SuccessAsync( new { area = "OrchardCore.Settings", groupId = ExactlySettingsDisplayDriver.EditorGroupId }); } - private async Task GetActionRedirectRequested(string transactionId) + private async Task GetActionRedirectRequestedAsync(string transactionId) { var result = await _exactlyService.GetTransactionDetailsAsync( transactionId, diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs index bc1f902b0..32fc9ccc9 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs @@ -1,5 +1,4 @@ -using Lombiq.HelpfulLibraries.OrchardCore.Contents; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using OrchardCore.Commerce.Payment.Exactly.Models; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs index 74f892892..7257392c9 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs @@ -13,7 +13,7 @@ public class ChargeActionAttributes { public string Action { get; set; } public Uri Url { get; set; } - public IList Parameters { get; set; } + public IEnumerable Parameters { get; set; } public string HttpMethod { get; set; } } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs index 888a649cd..1a5cbd7d6 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs @@ -7,7 +7,7 @@ namespace OrchardCore.Commerce.Payment.Exactly.Models; public class ChargeResponse : IExactlyResponseData { - private static Dictionary StatusMap = new() + private static readonly Dictionary _statusMap = new() { ["action-required"] = ChargeResponseStatus.ActionRequired, ["processing"] = ChargeResponseStatus.Processing, @@ -64,8 +64,8 @@ public class ChargeAttributes [JsonIgnore] public ChargeResponseStatus Status { - get => StatusMap.GetMaybe(StringStatus); - set => StringStatus = StatusMap.Single(pair => pair.Value == value).Key; + get => _statusMap.GetMaybe(StringStatus); + set => StringStatus = _statusMap.Single(pair => pair.Value == value).Key; } [JsonPropertyName("environmentMode")] @@ -81,10 +81,10 @@ public bool IsLive public DateTime CreatedAt { get; set; } public ChargeProcessing Processing { get; set; } - public IList StatusHistory { get; set; } + public IEnumerable StatusHistory { get; set; } public object Meta { get; set; } public string ReferenceId { get; set; } public DateTime ExpireAt { get; set; } - public IList Actions { get; set; } + public IEnumerable Actions { get; set; } } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs index deb464f75..25802d6f0 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlyResponse.cs @@ -8,13 +8,11 @@ public class ExactlyResponse where T : IExactlyResponseData { public T Data { get; set; } - public IList Errors { get; set; } + public IEnumerable Errors { get; set; } public void ThrowIfHasErrors() { - if (Errors?.Any() != true) return; - - var errors = Errors + var errors = Errors? .Select(error => $"{error.Code}: {error.Title} ({error.Details?.Trim()})".Replace(" ()", string.Empty)) .Distinct() .ToList(); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs index 17644f383..1f13d313e 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs @@ -9,14 +9,14 @@ namespace OrchardCore.Commerce.Payment.Exactly.Models; public interface IExactlyAmount { /// - /// Gets the decimal amount represented as string. Point (".") must be used as decimal separator. Decimal separator - /// must always be present for currencies with minor units. Max len is 37 chars: 18 digits before decimal separator, - /// decimal separator, 18 digits after + /// Gets or sets the decimal amount represented as string. Point (".") must be used as decimal separator. Decimal + /// separator must always be present for currencies with minor units. The maximum length is 37 characters: 18 digits + /// before the decimal separator, the decimal separator and 18 digits after. /// public string Amount { get; set; } /// - /// Gets the currency code, can be ISO 4217 alpha code or unofficial, e.g. XBT for Bitcoin. + /// Gets or sets the currency code, can be ISO 4217 alpha code or unofficial, e.g. XBT for Bitcoin. /// public string Currency { get; set; } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs index 8c1b43ff8..46094c637 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs @@ -10,9 +10,10 @@ namespace OrchardCore.Commerce.Payment.Exactly.Services; public class ExactlyPaymentProvider : IPaymentProvider { + public const string ProviderName = "Exactly"; + private readonly IExactlyService _exactlyService; private readonly ISiteService _siteService; - public const string ProviderName = "Exactly"; public string Name => ProviderName; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index 21653c69a..1ff77e7f5 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -1,13 +1,13 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; -using OrchardCore.Commerce.Abstractions.Exceptions; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Payment.Exactly.Models; using Refit; using System; using System.Threading; using System.Threading.Tasks; -using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; + +using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Payment.Exactly.Services; @@ -39,6 +39,7 @@ public async Task GetTransactionDetailsAsync( { ArgumentNullException.ThrowIfNull(transactionId); +#pragma warning disable // Boolean expressions should not be gratuitous (false positive) for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) { using var result = await _api.GetTransactionDetailsAsync(transactionId); @@ -48,13 +49,14 @@ public async Task GetTransactionDetailsAsync( await Task.Delay(100, cancellationToken); } +#pragma warning restore S2589 // Boolean expressions should not be gratuitous (false positive) throw new TimeoutException( $"Couldn't get the transaction \"{transactionId}\" with the status \"{waitForStatus}\" within " + $"the expected timeframe."); } - private T EvaluateResult(IApiResponse> result) + private static T EvaluateResult(IApiResponse> result) where T : IExactlyResponseData { // If the request is not successful, try to parse the response error and throw a more specific FrontendException diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlySettingsConfiguration.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlySettingsConfiguration.cs index 2e350fcea..c04a8e6bc 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlySettingsConfiguration.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlySettingsConfiguration.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Options; using OrchardCore.Commerce.Payment.Exactly.Models; -using OrchardCore.Entities; using OrchardCore.Settings; namespace OrchardCore.Commerce.Payment.Exactly.Services; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs index 9b2a189e4..37f8405df 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyApi.cs @@ -6,7 +6,7 @@ namespace OrchardCore.Commerce.Payment.Exactly.Services; /// /// Wrapper for the Exactly API. See here. All method documentation is -/// copied from this source. The authorization API key is provided by an +/// copied from this source. /// public interface IExactlyApi { diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index 17f157449..f24c3bee5 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -23,7 +23,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; + +using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Payment.Controllers; diff --git a/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs b/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs index 9bb55bf66..8d69d9c5f 100644 --- a/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs +++ b/src/Modules/OrchardCore.Commerce/Controllers/ShoppingCartController.cs @@ -18,7 +18,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; + +using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Controllers; diff --git a/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs index e937cd604..c003ba3ac 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs @@ -15,7 +15,8 @@ using OrchardCore.DisplayManagement.Views; using System.Linq; using System.Threading.Tasks; -using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; + +using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Drivers; diff --git a/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs index 47d5bf7b3..6204fa731 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/TaxRateTaxPartDisplayDriver.cs @@ -12,7 +12,8 @@ using OrchardCore.DisplayManagement.Notify; using OrchardCore.DisplayManagement.Views; using System.Threading.Tasks; -using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; + +using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Drivers; From 59670e6ba16cbaabc6020fad342aef7edd9160bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 8 Apr 2024 02:53:58 +0200 Subject: [PATCH 35/90] Update and fixes. --- Directory.Packages.props | 4 ++-- .../Controllers/ExactlyController.cs | 8 ++------ .../Views/CheckoutExactly.cshtml | 17 +++++++++++------ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8f796aca0..b9c870e4e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,8 +5,8 @@ - - + + diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 80a822e9d..9899eb590 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,7 +1,6 @@ using Lombiq.HelpfulLibraries.OrchardCore.Validation; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; -using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; @@ -19,7 +18,7 @@ using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; using AdminController = OrchardCore.Settings.Controllers.AdminController; -using FrontendException=Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; +using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Payment.Exactly.Controllers; @@ -31,7 +30,6 @@ public class ExactlyController : Controller private readonly INotifier _notifier; private readonly IPaymentService _paymentService; private readonly IHtmlLocalizer H; - private readonly IStringLocalizer S; public ExactlyController( IContentManager contentManager, @@ -39,8 +37,7 @@ public ExactlyController( ILogger logger, INotifier notifier, IPaymentService paymentService, - IHtmlLocalizer htmlLocalizer, - IStringLocalizer stringLocalizer) + IHtmlLocalizer htmlLocalizer) { _contentManager = contentManager; _exactlyService = exactlyService; @@ -48,7 +45,6 @@ public ExactlyController( _notifier = notifier; _paymentService = paymentService; H = htmlLocalizer; - S = stringLocalizer; } [HttpPost] diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml index cfc5cabf3..e4e991292 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml @@ -35,12 +35,17 @@ if (!json || json.error) { let error = json?.error ?? @defaultErrorMessage.Json(); - if (json?.html) { - const div = document.createElement('div'); - div.innerHTML = json.html; - document.body.append(div); - error = div.innerText.trim(); - document.body.removeChild(div); + if (Array.isArray(json?.html)) { + error = json.html.map((html) => { + const div = document.createElement('div'); + div.innerHTML = html; + + document.body.append(div); + const text = div.innerText.trim(); + document.body.removeChild(div); + + return text; + }).join('\n'); } alert(error); From cc7f41ab99a6ebd0a62841a4685d558d1b8a3116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 8 Apr 2024 12:15:38 +0200 Subject: [PATCH 36/90] Use LineItems instead of Charge. --- .../Controllers/ExactlyController.cs | 11 ++++++----- .../Models/ChargeRequest.cs | 16 ++++++++++------ .../Services/ExactlyService.cs | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 9899eb590..4a9f8584c 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -74,12 +74,13 @@ public async Task VerifyApi() order.Alter(part => { part.OrderId.Text = Guid.NewGuid().ToString("D"); - part.Charges.Add(new Payment.Models.Payment( - "card", - Guid.NewGuid().ToString("D"), - "Test Transaction", + part.LineItems.Add(new OrderLineItem( + quantity: 1, + "TEST", + "TEST", new Amount(1, Currency.Euro), - DateTime.UtcNow)); + new Amount(1, Currency.Euro), + contentItemVersion: null)); }); var result = await _exactlyService.CreateTransactionAsync(order.As()); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index 4057907ab..a4acea5bd 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Http; +using Lombiq.HelpfulLibraries.Common.Utilities; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -38,30 +39,33 @@ public ChargeRequest() public ChargeRequest(OrderPart orderPart, User user, string projectId, Uri returnUrl) { if (!returnUrl.IsAbsoluteUri) throw new ArgumentException("The return URL must be absolute.", nameof(returnUrl)); + var descriptionParts = orderPart + .LineItems + .Select(item => StringHelper.CreateInvariant($"{item.Quantity} × {item.UnitPrice} {item.FullSku}")); ProjectId = projectId; ReferenceId = orderPart.OrderId.Text; - CustomerDescription = string.Join(", ", orderPart.Charges.Select(payment => $"{payment.Amount} × {payment.ChargeText}")); + CustomerDescription = string.Join(", ", descriptionParts); ReturnUrl = returnUrl.AbsoluteUri; CustomerId = user.UserId; Email = user.Email; Meta = orderPart.ContentItem.ContentItemId; - this.SetAmount(orderPart.Charges.Select(payment => payment.Amount).Sum()); + this.SetAmount(orderPart.LineItems.Select(item => item.LinePrice).Sum()); } public static implicit operator ExactlyRequest(ChargeRequest attributes) => new() { Attributes = attributes }; - public static async Task CreateUserAsync(OrderPart orderPart, HttpContext context) + public static async Task CreateForCurrentUserAsync(OrderPart orderPart, HttpContext context) { var provider = context.RequestServices; - var returnurl = context.ActionTask(controller => controller.Middleware()); + var returnUrl = context.ActionTask(controller => controller.Middleware()); return new ChargeRequest( orderPart, await provider.GetRequiredService().GetFullUserAsync(context.User), provider.GetRequiredService>().Value.ProjectId, - new Uri(new Uri(context.Request.GetDisplayUrl()), returnurl)); + new Uri(new Uri(context.Request.GetDisplayUrl()), returnUrl)); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index 1ff77e7f5..ee4b174c1 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -24,7 +24,7 @@ public ExactlyService(IExactlyApi api, IHttpContextAccessor hca) public async Task CreateTransactionAsync(OrderPart orderPart) { - var charge = await ChargeRequest.CreateUserAsync(orderPart, _hca.HttpContext); + var charge = await ChargeRequest.CreateForCurrentUserAsync(orderPart, _hca.HttpContext); var request = new ExactlyDataWrapper>( new ExactlyRequest { Attributes = charge }); From e881264d09fde16bf86fd663a4c9faa1d905e63b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 8 Apr 2024 12:16:05 +0200 Subject: [PATCH 37/90] Fix redirect and reuse error handling. --- .../Views/CheckoutExactly.cshtml | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml index e4e991292..513da8271 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml @@ -25,6 +25,29 @@ return new Promise(r => setTimeout(r, ms)); } + function handleError(json) { + if (json && !json.error) return true; + + let error = json?.error ?? @defaultErrorMessage.Json(); + + if (Array.isArray(json?.html)) { + error = json.html.map((html) => { + const div = document.createElement('div'); + div.innerHTML = html; + + document.body.append(div); + const text = div.innerText.trim(); + document.body.removeChild(div); + + return text; + }).join('\n'); + } + + alert(error); + setButtons(true); + return false; + } + button.addEventListener('click', async (event) => { event.preventDefault(); setButtons(false); @@ -32,31 +55,13 @@ const form = document.querySelector('.payment-form'); const json = await fetchJson(@actionUrl.JsonHtmlContent(), { method: 'post', body: new FormData(form) }); - if (!json || json.error) { - let error = json?.error ?? @defaultErrorMessage.Json(); - - if (Array.isArray(json?.html)) { - error = json.html.map((html) => { - const div = document.createElement('div'); - div.innerHTML = html; - - document.body.append(div); - const text = div.innerText.trim(); - document.body.removeChild(div); - - return text; - }).join('\n'); - } - - alert(error); - } - else { + if (handleError(json)) { const statusUrl = (@statusUrl.JsonHtmlContent()).replace('TRANSACTION_ID', json.id); while (true) { - const redirectUrl = await fetchJson(statusUrl, { method: 'get' }); + const response = await fetchJson(statusUrl, { method: 'get' }); - if (redirectUrl) { - location.href = redirectUrl; + if (handleError(response)) { + location.href = response.url; break; } From 4de5cfc06591a0e5c8f8aea5a2f0459a561d42ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 8 Apr 2024 12:18:11 +0200 Subject: [PATCH 38/90] Handle if there is no response at first. --- .../Controllers/ExactlyController.cs | 2 +- .../Views/CheckoutExactly.cshtml | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 4a9f8584c..e8305baff 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -124,6 +124,6 @@ await _notifier.SuccessAsync( .Attributes .Actions .Select(action => action.Attributes) - .First(action => action.Action == "redirect-required" && action.HttpMethod == "GET"); + .FirstOrDefault(action => action.Action == "redirect-required" && action.HttpMethod == "GET"); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml index 513da8271..fd009e181 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml @@ -44,7 +44,6 @@ } alert(error); - setButtons(true); return false; } @@ -53,15 +52,15 @@ setButtons(false); const form = document.querySelector('.payment-form'); - const json = await fetchJson(@actionUrl.JsonHtmlContent(), { method: 'post', body: new FormData(form) }); + const actionResponse = await fetchJson(@actionUrl.JsonHtmlContent(), { method: 'post', body: new FormData(form) }); - if (handleError(json)) { - const statusUrl = (@statusUrl.JsonHtmlContent()).replace('TRANSACTION_ID', json.id); + if (handleError(actionResponse)) { + const statusUrl = (@statusUrl.JsonHtmlContent()).replace('TRANSACTION_ID', actionResponse.id); while (true) { - const response = await fetchJson(statusUrl, { method: 'get' }); + const statusResponse = await fetchJson(statusUrl, { method: 'get' }); - if (handleError(response)) { - location.href = response.url; + if (statusResponse && handleError(statusResponse)) { + location.href = statusResponse.url; break; } From f456a299ba80a1d307f93a84fe8e141cc7bed1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 8 Apr 2024 13:51:57 +0200 Subject: [PATCH 39/90] Add middleware. --- .../Controllers/ExactlyController.cs | 71 ++++++++++++++++--- .../Models/ChargeRequest.cs | 2 +- .../Models/ChargeResponse.cs | 12 ++++ .../Controllers/PaymentController.cs | 3 + .../Views/Payment/Wait.cshtml | 20 ++++++ 5 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index e8305baff..a77586e39 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,24 +1,34 @@ -using Lombiq.HelpfulLibraries.OrchardCore.Validation; +using Lombiq.HelpfulLibraries.OrchardCore.Contents; +using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; +using Lombiq.HelpfulLibraries.OrchardCore.Validation; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Logging; +using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Controllers; using OrchardCore.Commerce.Payment.Exactly.Drivers; using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.Commerce.Payment.Exactly.Services; +using OrchardCore.ContentFields.Indexing.SQL; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Mvc.Core.Utilities; +using OrchardCore.Mvc.Utilities; using Refit; using System; using System.Linq; using System.Threading.Tasks; +using YesSql; using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; using AdminController = OrchardCore.Settings.Controllers.AdminController; using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; +using PaymentFeatureIds = OrchardCore.Commerce.Payment.Constants.FeatureIds; + namespace OrchardCore.Commerce.Payment.Exactly.Controllers; @@ -29,22 +39,22 @@ public class ExactlyController : Controller private readonly ILogger _logger; private readonly INotifier _notifier; private readonly IPaymentService _paymentService; + private readonly ISession _session; private readonly IHtmlLocalizer H; public ExactlyController( - IContentManager contentManager, IExactlyService exactlyService, - ILogger logger, INotifier notifier, IPaymentService paymentService, - IHtmlLocalizer htmlLocalizer) + IOrchardServices services) { - _contentManager = contentManager; + _contentManager = services.ContentManager.Value; _exactlyService = exactlyService; - _logger = logger; + _logger = services.Logger.Value; _notifier = notifier; _paymentService = paymentService; - H = htmlLocalizer; + _session = services.Session.Value; + H = services.HtmlLocalizer.Value; } [HttpPost] @@ -63,8 +73,50 @@ public async Task GetRedirectUrl(string transactionId) => await this.SafeJsonAsync(async () => await GetActionRedirectRequestedAsync(transactionId)); [HttpGet("checkout/middleware/Exactly")] - public Task Middleware() => - Task.FromResult(Json(new { Request = Request.ToString() })); + public async Task Middleware(string transactionId, string referenceId) + { + var response = await _exactlyService.GetTransactionDetailsAsync(transactionId, cancellationToken: HttpContext.RequestAborted); + var order = response.Attributes.Status is ChargeResponse.ChargeResponseStatus.Failed or ChargeResponse.ChargeResponseStatus.Processed + ? await _session.QueryContentItem(PublicationStatus.Published, Order) + .With(index => index.ContentField == nameof(OrderPart.OrderId) && index.Text == referenceId) + .FirstOrDefaultAsync() + : null; + + switch (response.Attributes.Status) + { + case ChargeResponse.ChargeResponseStatus.ActionRequired: + case ChargeResponse.ChargeResponseStatus.Processing: + return RedirectToAction( + nameof(PaymentController.Wait), + typeof(PaymentController).ControllerName(), + new { area = PaymentFeatureIds.Payment, returnUrl = HttpContext.Request.GetDisplayUrl() }); + case ChargeResponse.ChargeResponseStatus.Processed: + if (order == null) + { + await _notifier.ErrorAsync( + H["Couldn't find the order associated with the reference ID \"{0}\".", referenceId ?? string.Empty]); + return NotFound(); + } + + return await _paymentService.UpdateAndRedirectToFinishedOrderAsync( + controller: this, + order, + shoppingCartId: null, + ExactlyPaymentProvider.ProviderName, + _ => [response.ToPayment()]); + case ChargeResponse.ChargeResponseStatus.Failed: + if (order != null) + { + order.Alter(part => part.Status.Text = OrderStatuses.PaymentFailed.HtmlClassify()); + await _contentManager.PublishAsync(order); + } + + await _notifier.ErrorAsync(H["Your transaction has failed."]); + return Redirect("~/cart"); + default: + throw new ArgumentOutOfRangeException(response.Attributes.Status.ToString()); + } + } public async Task VerifyApi() { @@ -82,6 +134,7 @@ public async Task VerifyApi() new Amount(1, Currency.Euro), contentItemVersion: null)); }); + await _contentManager.CreateAsync(order); var result = await _exactlyService.CreateTransactionAsync(order.As()); var action = await GetActionRedirectRequestedAsync(result.Id); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index a4acea5bd..db642c5c7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -60,7 +60,7 @@ public static implicit operator ExactlyRequest(ChargeRequest attr public static async Task CreateForCurrentUserAsync(OrderPart orderPart, HttpContext context) { var provider = context.RequestServices; - var returnUrl = context.ActionTask(controller => controller.Middleware()); + var returnUrl = context.ActionTask(controller => controller.Middleware(null, null)); return new ChargeRequest( orderPart, diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs index 1a5cbd7d6..dfc72ca50 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs @@ -19,6 +19,18 @@ public class ChargeResponse : IExactlyResponseData public string Id { get; set; } public ChargeAttributes Attributes { get; set; } + public Payment.Models.Payment ToPayment() + { + var type = Attributes.Processing.PaymentMethod.Type; + var amount = Attributes.Processing.GetAmount(); + return new( + type, + Id, + $"Exactly transaction via {type} for {amount}", + amount, + Attributes.CreatedAt); + } + public enum ChargeResponseStatus { /// diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index f24c3bee5..a1aed9c16 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -271,4 +271,7 @@ public async Task Callback(string paymentProviderName, string? or await _notifier.ErrorAsync(H["The payment has failed, please try again."]); return RedirectToAction(nameof(Index)); } + + [Route("checkout/wait")] + public IActionResult Wait(string returnUrl) => View(returnUrl); } diff --git a/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml new file mode 100644 index 000000000..6381deaa3 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml @@ -0,0 +1,20 @@ +@model string + +
+
+
+
+

@T["Your transaction is being processed..."]

+
+
+

+ @T["Please wait a moment, this page will automatically reload."] +

+
+
+
+
+ + \ No newline at end of file From 50342d4edf12cfc04b8236865d013b855b454a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 11:15:36 +0200 Subject: [PATCH 40/90] Move callback logic into provider instead of separate method. --- .../Controllers/ExactlyController.cs | 58 +---------------- .../Manifest.cs | 14 +++++ .../Models/ChargeRequest.cs | 14 ++++- .../Services/ExactlyPaymentProvider.cs | 62 +++++++++++++++++-- 4 files changed, 83 insertions(+), 65 deletions(-) create mode 100644 src/Modules/OrchardCore.Commerce.Payment.Exactly/Manifest.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index a77586e39..9eb5f4b9d 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -1,33 +1,25 @@ -using Lombiq.HelpfulLibraries.OrchardCore.Contents; -using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; +using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; using Lombiq.HelpfulLibraries.OrchardCore.Validation; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Logging; -using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Payment.Abstractions; -using OrchardCore.Commerce.Payment.Controllers; using OrchardCore.Commerce.Payment.Exactly.Drivers; using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.Commerce.Payment.Exactly.Services; -using OrchardCore.ContentFields.Indexing.SQL; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Mvc.Core.Utilities; -using OrchardCore.Mvc.Utilities; using Refit; using System; using System.Linq; using System.Threading.Tasks; -using YesSql; using static OrchardCore.Commerce.Abstractions.Constants.ContentTypes; using AdminController = OrchardCore.Settings.Controllers.AdminController; using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; -using PaymentFeatureIds = OrchardCore.Commerce.Payment.Constants.FeatureIds; namespace OrchardCore.Commerce.Payment.Exactly.Controllers; @@ -39,7 +31,6 @@ public class ExactlyController : Controller private readonly ILogger _logger; private readonly INotifier _notifier; private readonly IPaymentService _paymentService; - private readonly ISession _session; private readonly IHtmlLocalizer H; public ExactlyController( @@ -53,7 +44,6 @@ public ExactlyController( _logger = services.Logger.Value; _notifier = notifier; _paymentService = paymentService; - _session = services.Session.Value; H = services.HtmlLocalizer.Value; } @@ -72,52 +62,6 @@ await this.SafeJsonAsync(async () => public async Task GetRedirectUrl(string transactionId) => await this.SafeJsonAsync(async () => await GetActionRedirectRequestedAsync(transactionId)); - [HttpGet("checkout/middleware/Exactly")] - public async Task Middleware(string transactionId, string referenceId) - { - var response = await _exactlyService.GetTransactionDetailsAsync(transactionId, cancellationToken: HttpContext.RequestAborted); - var order = response.Attributes.Status is ChargeResponse.ChargeResponseStatus.Failed or ChargeResponse.ChargeResponseStatus.Processed - ? await _session.QueryContentItem(PublicationStatus.Published, Order) - .With(index => index.ContentField == nameof(OrderPart.OrderId) && index.Text == referenceId) - .FirstOrDefaultAsync() - : null; - - switch (response.Attributes.Status) - { - case ChargeResponse.ChargeResponseStatus.ActionRequired: - case ChargeResponse.ChargeResponseStatus.Processing: - return RedirectToAction( - nameof(PaymentController.Wait), - typeof(PaymentController).ControllerName(), - new { area = PaymentFeatureIds.Payment, returnUrl = HttpContext.Request.GetDisplayUrl() }); - case ChargeResponse.ChargeResponseStatus.Processed: - if (order == null) - { - await _notifier.ErrorAsync( - H["Couldn't find the order associated with the reference ID \"{0}\".", referenceId ?? string.Empty]); - return NotFound(); - } - - return await _paymentService.UpdateAndRedirectToFinishedOrderAsync( - controller: this, - order, - shoppingCartId: null, - ExactlyPaymentProvider.ProviderName, - _ => [response.ToPayment()]); - case ChargeResponse.ChargeResponseStatus.Failed: - if (order != null) - { - order.Alter(part => part.Status.Text = OrderStatuses.PaymentFailed.HtmlClassify()); - await _contentManager.PublishAsync(order); - } - - await _notifier.ErrorAsync(H["Your transaction has failed."]); - return Redirect("~/cart"); - default: - throw new ArgumentOutOfRangeException(response.Attributes.Status.ToString()); - } - } - public async Task VerifyApi() { try diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Manifest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Manifest.cs new file mode 100644 index 000000000..777f916c7 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Manifest.cs @@ -0,0 +1,14 @@ +using OrchardCore.Modules.Manifest; +using static OrchardCore.Commerce.Payment.Constants.FeatureIds; + +[assembly: Module( + Name = "Orchard Core Commerce - Payment - Exactly", + Author = "The Orchard Team", + Website = "https://github.com/OrchardCMS/OrchardCore.Commerce", + Version = "0.0.1", + Description = + "Exactly payment provider for Orchard Core Commerce. Note: you must configure it in Admin > Configuration > " + + "Commerce > Exactly API or it won't appear in the front end.", + Category = "Commerce", + Dependencies = [Payment, "OrchardCore.ContentFields.Indexing.SQL"] +)] diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index db642c5c7..d916afcb7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -6,7 +6,9 @@ using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType.Extensions; +using OrchardCore.Commerce.Payment.Controllers; using OrchardCore.Commerce.Payment.Exactly.Controllers; +using OrchardCore.Commerce.Payment.Exactly.Services; using OrchardCore.Users.Models; using System; using System.Linq; @@ -32,7 +34,8 @@ public class ChargeRequest : IExactlyRequestAttributes, IExactlyAmount public int Lifetime { get; set; } = 3600; public object Meta { get; set; } - public ChargeRequest() + [JsonConstructor] + private ChargeRequest() { } @@ -60,12 +63,17 @@ public static implicit operator ExactlyRequest(ChargeRequest attr public static async Task CreateForCurrentUserAsync(OrderPart orderPart, HttpContext context) { var provider = context.RequestServices; - var returnUrl = context.ActionTask(controller => controller.Middleware(null, null)); + + var returnUrl = context.ActionTask(controller => controller.Callback( + ExactlyPaymentProvider.ProviderName, + orderPart.ContentItem.ContentItemId, + null)); + var absoluteReturnUrl = new Uri(new Uri(context.Request.GetDisplayUrl()), returnUrl); return new ChargeRequest( orderPart, await provider.GetRequiredService().GetFullUserAsync(context.User), provider.GetRequiredService>().Value.ProjectId, - new Uri(new Uri(context.Request.GetDisplayUrl()), returnUrl)); + absoluteReturnUrl); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs index 46094c637..31a091aaf 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs @@ -1,28 +1,55 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Localization; using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Constants; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Controllers; using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Mvc.Core.Utilities; +using OrchardCore.Mvc.Utilities; using OrchardCore.Settings; +using System; using System.Threading.Tasks; +using PaymentFeatureIds = OrchardCore.Commerce.Payment.Constants.FeatureIds; + namespace OrchardCore.Commerce.Payment.Exactly.Services; public class ExactlyPaymentProvider : IPaymentProvider { public const string ProviderName = "Exactly"; + private readonly IContentManager _contentManager; private readonly IExactlyService _exactlyService; + private readonly IHttpContextAccessor _hca; + private readonly INotifier _notifier; + private readonly IPaymentService _paymentService; private readonly ISiteService _siteService; + private readonly IHtmlLocalizer H; public string Name => ProviderName; public ExactlyPaymentProvider( + IContentManager contentManager, IExactlyService exactlyService, - ISiteService siteService) + IHttpContextAccessor hca, + INotifier notifier, + IPaymentService paymentService, + ISiteService siteService, + IHtmlLocalizer htmlLocalizer) { + _contentManager = contentManager; _exactlyService = exactlyService; + _hca = hca; + _notifier = notifier; + _paymentService = paymentService; _siteService = siteService; + H = htmlLocalizer; } public async Task CreatePaymentProviderDataAsync(IPaymentViewModel model) @@ -31,9 +58,34 @@ public async Task CreatePaymentProviderDataAsync(IPaymentViewModel model return string.IsNullOrEmpty(settings?.ApiKey) || string.IsNullOrEmpty(settings.ProjectId) ? null : new object(); } - public Task UpdateAndRedirectToFinishedOrderAsync( + public async Task UpdateAndRedirectToFinishedOrderAsync( Controller controller, ContentItem order, - string shoppingCartId) => - throw new System.NotImplementedException(); + string shoppingCartId) + { + var context = _hca.HttpContext!; + var transactionId = context.Request.Query["transactionId"]; + var response = await _exactlyService.GetTransactionDetailsAsync(transactionId, cancellationToken: context.RequestAborted); + + switch (response.Attributes.Status) + { + case ChargeResponse.ChargeResponseStatus.ActionRequired: + case ChargeResponse.ChargeResponseStatus.Processing: + return controller.RedirectToAction( + nameof(PaymentController.Wait), + typeof(PaymentController).ControllerName(), + new { area = PaymentFeatureIds.Payment, returnUrl = context.Request.GetDisplayUrl() }); + case ChargeResponse.ChargeResponseStatus.Processed: + return await _paymentService.UpdateAndRedirectToFinishedOrderAsync( + controller, order, shoppingCartId, ProviderName, _ => [response.ToPayment()]); + case ChargeResponse.ChargeResponseStatus.Failed: + order.Alter(part => part.Status.Text = OrderStatuses.PaymentFailed.HtmlClassify()); + await _contentManager.PublishAsync(order); + + await _notifier.ErrorAsync(H["Your transaction has failed."]); + return controller.Redirect("~/cart"); + default: + throw new ArgumentOutOfRangeException(response.Attributes.Status.ToString()); + } + } } From ff226f1abb8d82af5fc5cad98d95edd4d15958e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 12:12:29 +0200 Subject: [PATCH 41/90] DRY tenant reload warnings. --- Directory.Packages.props | 4 +-- .../Drivers/ExactlySettingsDisplayDriver.cs | 26 ++++++++++++------- .../Views/ExactlySettings.Edit.cshtml | 2 -- .../Drivers/StripeApiSettingsDisplayDriver.cs | 7 ++++- .../Views/StripeApiSettings.Edit.cshtml | 2 -- .../PriceDisplaySettingsDisplayDriver.cs | 7 ++++- .../Drivers/RegionSettingsDisplayDriver.cs | 10 ++++++- .../Settings/CurrencySettingsDisplayDriver.cs | 7 ++++- .../Views/CurrencySettings.Edit.cshtml | 2 -- .../Views/PriceDisplaySettings.Edit.cshtml | 1 - .../Views/RegionSettings.Edit.cshtml | 2 -- 11 files changed, 45 insertions(+), 25 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b9c870e4e..35ff20cc5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,8 +5,8 @@ - - + + diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs index 32fc9ccc9..5f9fb8066 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs @@ -4,9 +4,11 @@ using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Implementation; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; using OrchardCore.Settings; +using System; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Exactly.Drivers; @@ -35,16 +37,20 @@ public ExactlySettingsDisplayDriver( _ssoSettings = ssoSettings.Value; } - public override async Task EditAsync(ExactlySettings section, BuildEditorContext context) => - await AuthorizeAsync() - ? Initialize($"{nameof(ExactlySettings)}_Edit", settings => - { - _ssoSettings.CopyTo(settings); - settings.ApiKey = string.Empty; - }) - .PlaceInContent() - .OnGroup(EditorGroupId) - : null; + public override async Task EditAsync(ExactlySettings section, BuildEditorContext context) + { + if (!context.GroupId.EqualsOrdinalIgnoreCase(EditorGroupId) || !await AuthorizeAsync()) return null; + + context.Shape.AddTenantReloadWarning(); + + return Initialize($"{nameof(ExactlySettings)}_Edit", settings => + { + _ssoSettings.CopyTo(settings); + settings.ApiKey = string.Empty; + }) + .PlaceInContent() + .OnGroup(EditorGroupId); + } public override async Task UpdateAsync(ExactlySettings section, BuildEditorContext context) { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 70f95f548..09b1e000a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -17,8 +17,6 @@ -

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

-
diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Drivers/StripeApiSettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Drivers/StripeApiSettingsDisplayDriver.cs index 25d8802c0..f8da3af66 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Drivers/StripeApiSettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Drivers/StripeApiSettingsDisplayDriver.cs @@ -8,9 +8,11 @@ using OrchardCore.Commerce.Payment.Stripe.ViewModels; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Implementation; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; using OrchardCore.Settings; +using System; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Stripe.Drivers; @@ -45,11 +47,14 @@ public override async Task EditAsync(StripeApiSettings section, { var user = _hca.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageStripeApiSettings)) + if (!context.GroupId.EqualsOrdinalIgnoreCase(GroupId) || + !await _authorizationService.AuthorizeAsync(user, Permissions.ManageStripeApiSettings)) { return null; } + context.Shape.AddTenantReloadWarning(); + return Initialize("StripeApiSettings_Edit", model => { model.PublishableKey = section.PublishableKey; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml index 9d1451eeb..827068929 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/StripeApiSettings.Edit.cshtml @@ -8,8 +8,6 @@ var emptyAfterSaving = T["The field will be empty after saving it, for security reasons."]; } -

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

-
diff --git a/src/Modules/OrchardCore.Commerce/Drivers/PriceDisplaySettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/PriceDisplaySettingsDisplayDriver.cs index 7d8313957..e81284617 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/PriceDisplaySettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/PriceDisplaySettingsDisplayDriver.cs @@ -4,9 +4,11 @@ using OrchardCore.Commerce.ViewModels; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Implementation; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; using OrchardCore.Settings; +using System; using System.Threading.Tasks; namespace OrchardCore.Commerce.Drivers; @@ -36,11 +38,14 @@ public override async Task EditAsync(PriceDisplaySettings sectio { var user = _hca.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManagePriceDisplaySettings)) + if (!context.GroupId.EqualsOrdinalIgnoreCase(GroupId) || + !await _authorizationService.AuthorizeAsync(user, Permissions.ManagePriceDisplaySettings)) { return null; } + context.Shape.AddTenantReloadWarning(); + return Initialize("PriceDisplaySettings_Edit", model => { model.UseNetPriceDisplay = section.UseNetPriceDisplay; diff --git a/src/Modules/OrchardCore.Commerce/Drivers/RegionSettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Drivers/RegionSettingsDisplayDriver.cs index f8091da01..54143db07 100644 --- a/src/Modules/OrchardCore.Commerce/Drivers/RegionSettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Drivers/RegionSettingsDisplayDriver.cs @@ -7,9 +7,11 @@ using OrchardCore.Commerce.ViewModels; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Implementation; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; using OrchardCore.Settings; +using System; using System.Linq; using System.Threading.Tasks; @@ -43,7 +45,13 @@ public override async Task EditAsync(RegionSettings section, Bui { var user = _hca.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageRegionSettings)) return null; + if (!context.GroupId.EqualsOrdinalIgnoreCase(GroupId) || + !await _authorizationService.AuthorizeAsync(user, Permissions.ManageRegionSettings)) + { + return null; + } + + context.Shape.AddTenantReloadWarning(); return Initialize("RegionSettings_Edit", model => { diff --git a/src/Modules/OrchardCore.Commerce/Settings/CurrencySettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce/Settings/CurrencySettingsDisplayDriver.cs index 582c96241..7a288a9bf 100644 --- a/src/Modules/OrchardCore.Commerce/Settings/CurrencySettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce/Settings/CurrencySettingsDisplayDriver.cs @@ -6,9 +6,11 @@ using OrchardCore.Commerce.ViewModels; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Implementation; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; using OrchardCore.Settings; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -45,11 +47,14 @@ public override async Task EditAsync(CurrencySettings section, B { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageCurrencySettings)) + if (!context.GroupId.EqualsOrdinalIgnoreCase(GroupId) || + !await _authorizationService.AuthorizeAsync(user, Permissions.ManageCurrencySettings)) { return null; } + context.Shape.AddTenantReloadWarning(); + var shapes = new List { Initialize("CurrencySettings_Edit", model => diff --git a/src/Modules/OrchardCore.Commerce/Views/CurrencySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce/Views/CurrencySettings.Edit.cshtml index f36828154..befac89f1 100644 --- a/src/Modules/OrchardCore.Commerce/Views/CurrencySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce/Views/CurrencySettings.Edit.cshtml @@ -1,7 +1,5 @@ @model CurrencySettingsViewModel -

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

-
diff --git a/src/Modules/OrchardCore.Commerce/Views/PriceDisplaySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce/Views/PriceDisplaySettings.Edit.cshtml index 54214be46..9ae5eb658 100644 --- a/src/Modules/OrchardCore.Commerce/Views/PriceDisplaySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce/Views/PriceDisplaySettings.Edit.cshtml @@ -1,6 +1,5 @@ @model PriceDisplaySettingsViewModel -

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

diff --git a/src/Modules/OrchardCore.Commerce/Views/RegionSettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce/Views/RegionSettings.Edit.cshtml index 5b37f9bb1..2dbe193d0 100644 --- a/src/Modules/OrchardCore.Commerce/Views/RegionSettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce/Views/RegionSettings.Edit.cshtml @@ -1,7 +1,5 @@ @model RegionSettingsViewModel -

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

-
From 3b8cf60a9b1ccaf273591f9d2837fc8947f9eba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 12:14:31 +0200 Subject: [PATCH 42/90] Code cleanup. --- .../Controllers/ExactlyController.cs | 1 - .../OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 9eb5f4b9d..88acc0494 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -21,7 +21,6 @@ using AdminController = OrchardCore.Settings.Controllers.AdminController; using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; - namespace OrchardCore.Commerce.Payment.Exactly.Controllers; public class ExactlyController : Controller diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index d916afcb7..0bde594ca 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -7,7 +7,6 @@ using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType.Extensions; using OrchardCore.Commerce.Payment.Controllers; -using OrchardCore.Commerce.Payment.Exactly.Controllers; using OrchardCore.Commerce.Payment.Exactly.Services; using OrchardCore.Users.Models; using System; From 93d84b30289f594806e342a75e78703f6af9c37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 14:18:20 +0200 Subject: [PATCH 43/90] Add RedirectToWait helper method. --- .../Controllers/PaymentController.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index a1aed9c16..73f6b8d2f 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; @@ -13,11 +14,13 @@ using OrchardCore.Commerce.MoneyDataType.Abstractions; using OrchardCore.Commerce.MoneyDataType.Extensions; using OrchardCore.Commerce.Payment.Abstractions; +using OrchardCore.Commerce.Payment.Constants; using OrchardCore.Commerce.Payment.ViewModels; using OrchardCore.Commerce.ViewModels; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Mvc.Utilities; using System; using System.Collections.Generic; @@ -273,5 +276,17 @@ public async Task Callback(string paymentProviderName, string? or } [Route("checkout/wait")] - public IActionResult Wait(string returnUrl) => View(returnUrl); + public IActionResult Wait(string returnUrl) => View(new CheckoutWaitViewModel(returnUrl)); + + public static IActionResult RedirectToWait(Controller controller, string returnUrl = null) => + controller.RedirectToAction( + nameof(Wait), + typeof(PaymentController).ControllerName(), + new + { + area = FeatureIds.Payment, + returnUrl = string.IsNullOrEmpty(returnUrl) + ? controller.HttpContext.Request.GetDisplayUrl() + : returnUrl, + }); } From e711196cdcb489806109282cb67d0c1a929d2499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 14:19:17 +0200 Subject: [PATCH 44/90] Callback should only evaluate if status is pending, otherwise redirect to display. --- .../Controllers/PaymentController.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index 73f6b8d2f..e60c1a583 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -238,6 +238,11 @@ await _paymentService.CreatePendingOrderFromShoppingCartAsync(shoppingCartId, mu ? await _paymentService.UpdateAndRedirectToFinishedOrderAsync(this, order, shoppingCartId) : NotFound(); + [HttpGet] + [Route("checkout/callback/{paymentProviderName}/{orderId?}")] + public Task CallbackGet(string paymentProviderName, string? orderId, string? shoppingCartId) => + Callback(paymentProviderName, orderId, shoppingCartId); + [AllowAnonymous] [HttpPost] [ValidateAntiForgeryToken] @@ -253,24 +258,21 @@ public async Task Callback(string paymentProviderName, string? or var status = order.As()?.Status?.Text ?? OrderStatuses.Pending.HtmlClassify(); - if (status == OrderStatuses.Ordered.HtmlClassify()) + if (status != OrderStatuses.Pending.HtmlClassify()) { return this.RedirectToContentDisplay(order); } - if (status == OrderStatuses.Pending.HtmlClassify()) + foreach (var provider in _paymentProviders.WhereName(paymentProviderName)) { - foreach (var provider in _paymentProviders.WhereName(paymentProviderName)) + if (await provider.UpdateAndRedirectToFinishedOrderAsync(this, order, shoppingCartId) is { } result) { - if (await provider.UpdateAndRedirectToFinishedOrderAsync(this, order, shoppingCartId) is { } result) - { - return result; - } + return result; } - - return this.RedirectToContentDisplay(order); } + return this.RedirectToContentDisplay(order); + await _notifier.ErrorAsync(H["The payment has failed, please try again."]); return RedirectToAction(nameof(Index)); } From 179b2188e419ee558da6395d04326bfc41761f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 14:19:38 +0200 Subject: [PATCH 45/90] Fix wait view. --- .../ViewModels/CheckoutWaitViewModel.cs | 3 +++ .../OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 src/Modules/OrchardCore.Commerce.Payment/ViewModels/CheckoutWaitViewModel.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment/ViewModels/CheckoutWaitViewModel.cs b/src/Modules/OrchardCore.Commerce.Payment/ViewModels/CheckoutWaitViewModel.cs new file mode 100644 index 000000000..83f159fec --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment/ViewModels/CheckoutWaitViewModel.cs @@ -0,0 +1,3 @@ +namespace OrchardCore.Commerce.Payment.ViewModels; + +public record CheckoutWaitViewModel(string ReturnUrl); diff --git a/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml index 6381deaa3..a5efd7af0 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml @@ -1,4 +1,4 @@ -@model string +@model OrchardCore.Commerce.Payment.ViewModels.CheckoutWaitViewModel
@@ -16,5 +16,5 @@
\ No newline at end of file From ef83e7aba66440aeaaec0e5d2c4ac693960fa929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 14:21:29 +0200 Subject: [PATCH 46/90] Calculate response currency using money service. --- .../Controllers/ExactlyController.cs | 2 +- .../Models/ChargeAction.cs | 4 +++ .../Models/IExactlyAmount.cs | 5 ++-- .../Services/ExactlyPaymentProvider.cs | 25 +++++++++---------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 88acc0494..21e496a1c 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -120,6 +120,6 @@ await _notifier.SuccessAsync( .Attributes .Actions .Select(action => action.Attributes) - .FirstOrDefault(action => action.Action == "redirect-required" && action.HttpMethod == "GET"); + .FirstOrDefault(action => action.Action == "redirect-required" && action.IsGet); } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs index 7257392c9..2b298909f 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace OrchardCore.Commerce.Payment.Exactly.Models; @@ -15,5 +16,8 @@ public class ChargeActionAttributes public Uri Url { get; set; } public IEnumerable Parameters { get; set; } public string HttpMethod { get; set; } + + [JsonIgnore] + public bool IsGet => HttpMethod == "GET"; } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs index 1f13d313e..7cbdd00f6 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/IExactlyAmount.cs @@ -1,4 +1,5 @@ using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.MoneyDataType.Abstractions; using System.Globalization; namespace OrchardCore.Commerce.Payment.Exactly.Models; @@ -29,8 +30,8 @@ public static void SetAmount(this IExactlyAmount target, Amount source) target.Currency = source.Currency.CurrencyIsoCode; } - public static Amount GetAmount(this IExactlyAmount source) => + public static Amount GetAmount(this IExactlyAmount source, IMoneyService moneyService) => new( decimal.Parse(source.Amount, CultureInfo.InvariantCulture), - Currency.FromIsoCurrencyCode(source.Currency)); + moneyService.GetCurrency(source.Currency)); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs index 31a091aaf..50336f5b2 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs @@ -1,23 +1,22 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; +using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.MoneyDataType.Abstractions; using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.Controllers; using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.Notify; -using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Mvc.Utilities; using OrchardCore.Settings; using System; +using System.Linq; using System.Threading.Tasks; -using PaymentFeatureIds = OrchardCore.Commerce.Payment.Constants.FeatureIds; - namespace OrchardCore.Commerce.Payment.Exactly.Services; public class ExactlyPaymentProvider : IPaymentProvider @@ -27,6 +26,7 @@ public class ExactlyPaymentProvider : IPaymentProvider private readonly IContentManager _contentManager; private readonly IExactlyService _exactlyService; private readonly IHttpContextAccessor _hca; + private readonly IMoneyService _moneyService; private readonly INotifier _notifier; private readonly IPaymentService _paymentService; private readonly ISiteService _siteService; @@ -35,21 +35,20 @@ public class ExactlyPaymentProvider : IPaymentProvider public string Name => ProviderName; public ExactlyPaymentProvider( - IContentManager contentManager, IExactlyService exactlyService, - IHttpContextAccessor hca, + IMoneyService moneyService, INotifier notifier, IPaymentService paymentService, - ISiteService siteService, - IHtmlLocalizer htmlLocalizer) + IOrchardServices services) { - _contentManager = contentManager; + _contentManager = services.ContentManager.Value; _exactlyService = exactlyService; - _hca = hca; + _hca = services.HttpContextAccessor.Value; + _moneyService = moneyService; _notifier = notifier; _paymentService = paymentService; - _siteService = siteService; - H = htmlLocalizer; + _siteService = services.SiteService.Value; + H = services.HtmlLocalizer.Value; } public async Task CreatePaymentProviderDataAsync(IPaymentViewModel model) From 2ed160a271cdce0029eff09df8ed14b6aa1a6b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 14:21:44 +0200 Subject: [PATCH 47/90] Fix success. --- .../Models/ChargeResponse.cs | 7 +++--- .../Services/ExactlyPaymentProvider.cs | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs index dfc72ca50..7fab50f43 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs @@ -1,4 +1,5 @@ -using System; +using OrchardCore.Commerce.MoneyDataType.Abstractions; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; @@ -19,10 +20,10 @@ public class ChargeResponse : IExactlyResponseData public string Id { get; set; } public ChargeAttributes Attributes { get; set; } - public Payment.Models.Payment ToPayment() + public Payment.Models.Payment ToPayment(IMoneyService moneyService) { var type = Attributes.Processing.PaymentMethod.Type; - var amount = Attributes.Processing.GetAmount(); + var amount = Attributes.Processing.GetAmount(moneyService); return new( type, Id, diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs index 50336f5b2..0cbe54779 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs @@ -65,24 +65,26 @@ public async Task UpdateAndRedirectToFinishedOrderAsync( var context = _hca.HttpContext!; var transactionId = context.Request.Query["transactionId"]; var response = await _exactlyService.GetTransactionDetailsAsync(transactionId, cancellationToken: context.RequestAborted); + var data = response.Attributes; - switch (response.Attributes.Status) + switch (data.Processing.ResultCode, data.Status) { - case ChargeResponse.ChargeResponseStatus.ActionRequired: - case ChargeResponse.ChargeResponseStatus.Processing: - return controller.RedirectToAction( - nameof(PaymentController.Wait), - typeof(PaymentController).ControllerName(), - new { area = PaymentFeatureIds.Payment, returnUrl = context.Request.GetDisplayUrl() }); - case ChargeResponse.ChargeResponseStatus.Processed: + case (_, ChargeResponse.ChargeResponseStatus.Processing): + return PaymentController.RedirectToWait(controller); + case (_, ChargeResponse.ChargeResponseStatus.Processed): + case ("success", _): return await _paymentService.UpdateAndRedirectToFinishedOrderAsync( - controller, order, shoppingCartId, ProviderName, _ => [response.ToPayment()]); - case ChargeResponse.ChargeResponseStatus.Failed: + controller, order, shoppingCartId, ProviderName, _ => [response.ToPayment(_moneyService)]); + case (_, ChargeResponse.ChargeResponseStatus.ActionRequired) + when data.Actions?.FirstOrDefault(action => action.Attributes.IsGet) is { } action: + return PaymentController.RedirectToWait(controller, action.Attributes.Url.AbsoluteUri); + case (_, ChargeResponse.ChargeResponseStatus.ActionRequired): + case (_, ChargeResponse.ChargeResponseStatus.Failed): order.Alter(part => part.Status.Text = OrderStatuses.PaymentFailed.HtmlClassify()); await _contentManager.PublishAsync(order); await _notifier.ErrorAsync(H["Your transaction has failed."]); - return controller.Redirect("~/cart"); + return controller.Redirect("~/checkout"); default: throw new ArgumentOutOfRangeException(response.Attributes.Status.ToString()); } From ddf1f5d37a8c5c72e3a97be081a6e67eb1d5ee59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 15:43:54 +0200 Subject: [PATCH 48/90] Various bug fixes. --- .../Models/ChargeResponse.cs | 45 ++++++++++++++++++- .../Services/ExactlyPaymentProvider.cs | 28 +++++++----- .../Controllers/PaymentController.cs | 10 ++--- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs index 7fab50f43..56bc2adff 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs @@ -1,4 +1,5 @@ -using OrchardCore.Commerce.MoneyDataType.Abstractions; +using Microsoft.Extensions.Localization; +using OrchardCore.Commerce.MoneyDataType.Abstractions; using System; using System.Collections.Generic; using System.Linq; @@ -20,6 +21,48 @@ public class ChargeResponse : IExactlyResponseData public string Id { get; set; } public ChargeAttributes Attributes { get; set; } + public static IDictionary GetResultCodes(IStringLocalizer T) => + new Dictionary + { + ["success"] = T["processing of the transaction was successfully completed"], + ["transaction_failed"] = T["transaction failed, no specific details could be provided"], + ["refund_not_allowed_for_transaction_in_progress"] = T["refund is not allowed for the payment in progress"], + ["refundable_amount_exceeded"] = T["refund amount exceeds the amount of the original payment"], + ["refunds_restricted_for_transaction"] = T["refunds not allowed for the original payment"], + ["reversals_restricted_for_transaction"] = T["reversals not allowed for the original payment"], + ["reversible_amount_exceeded"] = T["amount to reverse exceeds the amount of the original payment"], + ["not_allowed_for_failed_transaction"] = T["the operation is not allowed for the failed transaction"], + ["sub_recurrings_restricted_for_transaction"] = T["subsequent recurring payments are not allowed for the initial payment"], + ["already_captured"] = T["payment was already captured"], + ["capture_amount_exceeded"] = T["capture amount exceeds the amount of the original authorize"], + ["authentication_failed"] = T["customer failed 3DS or any other authentication"], + ["authentication_expired"] = T["customer didn't complete 3DS or any other authentication in expected time"], + ["invalid_card_data"] = T["invalid card details were provided"], + ["cancelled_by_customer"] = T["customer cancelled the transaction"], + ["blocked_by_issuer"] = T["the transaction was blocked by the issuer"], + ["declined_by_issuer"] = T["the transaction was declined by the issuer"], + ["insufficient_funds"] = T["customer's account doesn't have enough funds for the payment"], + ["insufficient_balance"] = T["merchant's account doesn't have enough funds for the transaction"], + ["try_again_later"] = T["transaction declined, wait for some time before re-trying the transaction"], + ["try_again"] = T["transaction declined, re-try the transaction"], + ["transaction_declined"] = T["transaction declined, no additional details provided"], + ["contact_support"] = T["transaction declined, please contact Support Team providing the transaction ID"], + ["country_not_allowed"] = T["customer location or issuer country is not allowed"], + ["count_limit_exceeded"] = T["allowed limit for total number of transactions was reached"], + ["volume_limit_exceeded"] = T["allowed limit for total volume was reached"], + ["amount_too_large"] = T["amount of the transaction is too large"], + ["amount_too_small"] = T["amount of the transaction is too small"], + ["invalid_transaction_attributes"] = + T["invalid transaction attributes provided by customer or merchant, e.g. card details, customer details"], + ["fraud_suspected"] = T["the transaction was declined due to suspected fraud"], + ["risk_check_failed"] = T["the transaction was declined due to failed risk check"], + ["volume_limit_per_source_exceeded"] = T["allowed limit was reached for total volume per source, e.g. card, wallet account, etc."], + ["card_blocked"] = T["the card is blocked"], + ["expire_timeout"] = T["exceeded default allowed time for transaction processing"], + ["custom_expire_timeout"] = T["exceeded allowed time for transaction processing"], + ["contact_issuer"] = T["the transaction was declined, the customer should contact issuer"], + }; + public Payment.Models.Payment ToPayment(IMoneyService moneyService) { var type = Attributes.Processing.PaymentMethod.Type; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs index 0cbe54779..dd4812b47 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs @@ -2,18 +2,17 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Localization; using OrchardCore.Commerce.Abstractions.Abstractions; -using OrchardCore.Commerce.Abstractions.Constants; -using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType.Abstractions; using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.Controllers; using OrchardCore.Commerce.Payment.Exactly.Models; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.Notify; -using OrchardCore.Mvc.Utilities; using OrchardCore.Settings; using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -23,7 +22,7 @@ public class ExactlyPaymentProvider : IPaymentProvider { public const string ProviderName = "Exactly"; - private readonly IContentManager _contentManager; + private readonly IStringLocalizer _chargeResponseStringLocalizer; private readonly IExactlyService _exactlyService; private readonly IHttpContextAccessor _hca; private readonly IMoneyService _moneyService; @@ -35,13 +34,14 @@ public class ExactlyPaymentProvider : IPaymentProvider public string Name => ProviderName; public ExactlyPaymentProvider( + IStringLocalizer chargeResponseStringLocalizer, IExactlyService exactlyService, IMoneyService moneyService, INotifier notifier, IPaymentService paymentService, IOrchardServices services) { - _contentManager = services.ContentManager.Value; + _chargeResponseStringLocalizer = chargeResponseStringLocalizer; _exactlyService = exactlyService; _hca = services.HttpContextAccessor.Value; _moneyService = moneyService; @@ -78,15 +78,21 @@ public async Task UpdateAndRedirectToFinishedOrderAsync( case (_, ChargeResponse.ChargeResponseStatus.ActionRequired) when data.Actions?.FirstOrDefault(action => action.Attributes.IsGet) is { } action: return PaymentController.RedirectToWait(controller, action.Attributes.Url.AbsoluteUri); - case (_, ChargeResponse.ChargeResponseStatus.ActionRequired): + case ({ } resultCode, _) when !string.IsNullOrEmpty(resultCode): + var resultCodes = ChargeResponse.GetResultCodes(_chargeResponseStringLocalizer); + var resultMessage = resultCodes.GetMaybe(resultCode) ?? resultCodes["transaction_failed"]; + return await FailAsync(controller, order, H["Your transaction has failed: {0}.", resultMessage]); case (_, ChargeResponse.ChargeResponseStatus.Failed): - order.Alter(part => part.Status.Text = OrderStatuses.PaymentFailed.HtmlClassify()); - await _contentManager.PublishAsync(order); - - await _notifier.ErrorAsync(H["Your transaction has failed."]); - return controller.Redirect("~/checkout"); + case (_, ChargeResponse.ChargeResponseStatus.ActionRequired): + return await FailAsync(controller, order, H["Your transaction has failed."]); default: throw new ArgumentOutOfRangeException(response.Attributes.Status.ToString()); } } + + private async Task FailAsync(Controller controller, ContentItem order, LocalizedHtmlString message) + { + await _notifier.ErrorAsync(message); + return PaymentController.RedirectToWait(controller, controller.Url.Content("~/checkout")); + } } diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index e60c1a583..9791d04b0 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -256,9 +256,11 @@ public async Task Callback(string paymentProviderName, string? or : await _contentManager.GetAsync(orderId); if (order is null) return NotFound(); - var status = order.As()?.Status?.Text ?? OrderStatuses.Pending.HtmlClassify(); + var pending = OrderStatuses.Pending.HtmlClassify(); + var failed = OrderStatuses.PaymentFailed.HtmlClassify(); + var status = order.As()?.Status?.Text ?? pending; - if (status != OrderStatuses.Pending.HtmlClassify()) + if (status != pending && status != failed) { return this.RedirectToContentDisplay(order); } @@ -271,8 +273,6 @@ public async Task Callback(string paymentProviderName, string? or } } - return this.RedirectToContentDisplay(order); - await _notifier.ErrorAsync(H["The payment has failed, please try again."]); return RedirectToAction(nameof(Index)); } @@ -280,7 +280,7 @@ public async Task Callback(string paymentProviderName, string? or [Route("checkout/wait")] public IActionResult Wait(string returnUrl) => View(new CheckoutWaitViewModel(returnUrl)); - public static IActionResult RedirectToWait(Controller controller, string returnUrl = null) => + public static IActionResult RedirectToWait(Controller controller, string? returnUrl = null) => controller.RedirectToAction( nameof(Wait), typeof(PaymentController).ControllerName(), From 6e9379254bcb7001ca5a91caa1252b528511bb6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 16:03:23 +0200 Subject: [PATCH 49/90] Clean up order statuses, don't use HtmlClassify when not needed. --- .../Constants/OrderStatuses.cs | 9 +++ .../Models/OrderPart.cs | 24 ++++++++ .../Services/ExactlyPaymentProvider.cs | 6 ++ .../Controllers/StripeController.cs | 8 +-- .../Services/StripePaymentService.cs | 4 +- .../Controllers/PaymentController.cs | 6 +- .../Services/PaymentService.cs | 4 +- .../Migrations/OrderMigrations.cs | 58 ++++++++++++++----- 8 files changed, 92 insertions(+), 27 deletions(-) diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs index e5bc057a5..cdeda8cbb 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs @@ -7,4 +7,13 @@ public static class OrderStatuses public const string Ordered = nameof(Ordered); public const string Arrived = nameof(Arrived); public const string Shipped = nameof(Shipped); + + public static class OrderStatusCodes + { + public const string Pending = "pending"; + public const string PaymentFailed = "payment-failed"; + public const string Ordered = "ordered"; + public const string Arrived = "arrived"; + public const string Shipped = "shipped"; + } } diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs index 2693c18a7..ed781ed30 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs @@ -1,11 +1,16 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.ContentFields.Fields; using OrchardCore.ContentManagement; +using OrchardCore.Mvc.Utilities; +using System; using System.Collections.Generic; +using static OrchardCore.Commerce.Abstractions.Constants.OrderStatuses; + namespace OrchardCore.Commerce.Abstractions.Models; public class OrderPart : ContentPart @@ -45,4 +50,23 @@ public class OrderPart : ContentPart public BooleanField IsCorporation { get; set; } = new(); public IDictionary AdditionalData { get; } = new Dictionary(); + + [JsonIgnore] + public bool IsPending => string.IsNullOrWhiteSpace(Status?.Text) || Status.Text.EqualsOrdinalIgnoreCase(OrderStatusCodes.Pending); + + [JsonIgnore] + public bool IsOrdered => Status?.Text?.EqualsOrdinalIgnoreCase(OrderStatusCodes.Ordered) == true; + + [JsonIgnore] + public bool IsFailed => Status?.Text?.EqualsOrdinalIgnoreCase(OrderStatusCodes.PaymentFailed) == true; + + /// + /// Sets the to . + /// + public void FailPayment() => Status.Text = OrderStatusCodes.PaymentFailed; + + /// + /// Sets the to . + /// + public void SucceedPayment() => Status.Text = OrderStatusCodes.Ordered; } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs index dd4812b47..fe3942e15 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyPaymentProvider.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType.Abstractions; using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.Controllers; @@ -23,6 +24,7 @@ public class ExactlyPaymentProvider : IPaymentProvider public const string ProviderName = "Exactly"; private readonly IStringLocalizer _chargeResponseStringLocalizer; + private readonly IContentManager _contentManager; private readonly IExactlyService _exactlyService; private readonly IHttpContextAccessor _hca; private readonly IMoneyService _moneyService; @@ -42,6 +44,7 @@ public ExactlyPaymentProvider( IOrchardServices services) { _chargeResponseStringLocalizer = chargeResponseStringLocalizer; + _contentManager = services.ContentManager.Value; _exactlyService = exactlyService; _hca = services.HttpContextAccessor.Value; _moneyService = moneyService; @@ -92,6 +95,9 @@ public async Task UpdateAndRedirectToFinishedOrderAsync( private async Task FailAsync(Controller controller, ContentItem order, LocalizedHtmlString message) { + order.Alter(part => part.FailPayment()); + await _contentManager.UpdateAsync(order); + await _notifier.ErrorAsync(message); return PaymentController.RedirectToWait(controller, controller.Url.Content("~/checkout")); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs index 7403915e1..8193c5e06 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs @@ -70,16 +70,16 @@ await _notifier.ErrorAsync( return NotFound(); } - var status = order.As()?.Status?.Text; + var part = order.As() ?? new OrderPart(); var succeeded = fetchedPaymentIntent.Status == PaymentIntentStatuses.Succeeded; // Looks like there is nothing to do here. - if (succeeded && status == OrderStatuses.Ordered.HtmlClassify()) + if (succeeded && part.IsOrdered) { return this.RedirectToContentDisplay(order); } - if (succeeded && status == OrderStatuses.Pending.HtmlClassify()) + if (succeeded && part.IsPending) { return await _stripePaymentService.UpdateAndRedirectToFinishedOrderAsync( this, @@ -87,7 +87,7 @@ await _notifier.ErrorAsync( fetchedPaymentIntent); } - if (status == OrderStatuses.PaymentFailed.HtmlClassify()) + if (part.IsFailed) { return await PaymentFailedAsync(); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs index b37d6778d..38fd3c0b3 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs @@ -117,7 +117,7 @@ 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() }); + orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.OrderStatusCodes.PaymentFailed }); await _contentManager.UpdateAsync(order); } @@ -147,7 +147,7 @@ public async Task CreateOrUpdateOrderFromShoppingCartAsync(IUpdateM // Shopping cart orderPart.LineItems.SetItems(lineItems); - orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.Pending.HtmlClassify() }; + orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.OrderStatusCodes.Pending }; // Store the current applicable discount info so they will be available in the future. orderPart.AdditionalData.SetDiscountsByProduct(cartViewModel diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index 9791d04b0..6f2256e19 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -256,11 +256,9 @@ public async Task Callback(string paymentProviderName, string? or : await _contentManager.GetAsync(orderId); if (order is null) return NotFound(); - var pending = OrderStatuses.Pending.HtmlClassify(); - var failed = OrderStatuses.PaymentFailed.HtmlClassify(); - var status = order.As()?.Status?.Text ?? pending; + var status = order.As()?.Status?.Text ?? OrderStatuses.OrderStatusCodes.Pending; - if (status != pending && status != failed) + if (status != OrderStatuses.OrderStatusCodes.Pending && status != OrderStatuses.OrderStatusCodes.PaymentFailed) { return this.RedirectToContentDisplay(order); } diff --git a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs index 0c4d6c76a..3b19f4866 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs @@ -194,7 +194,7 @@ await _paymentProvidersLazy await order.AlterAsync(async orderPart => { orderPart.LineItems.SetItems(lineItems); - orderPart.Status.Text = OrderStatuses.Pending.HtmlClassify(); + orderPart.Status.Text = OrderStatuses.OrderStatusCodes.Pending; if (orderPart.BillingAndShippingAddressesMatch.Value) { @@ -258,7 +258,7 @@ public async Task UpdateOrderToOrderedAsync( orderPart.Charges.SetItems(newCharges); } - orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.Ordered.HtmlClassify() }; + orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.OrderStatusCodes.Ordered }; }); await _orderEvents.AwaitEachAsync(orderEvent => orderEvent.OrderedAsync(order, shoppingCartId)); diff --git a/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs index 22c299eab..7ecdf1264 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs @@ -80,12 +80,13 @@ await _contentDefinitionManager { Options = [ - new ListValueOption { Name = Pending, Value = Pending.HtmlClassify() }, - new ListValueOption { Name = Ordered, Value = Ordered.HtmlClassify() }, - new ListValueOption { Name = Shipped, Value = Shipped.HtmlClassify() }, - new ListValueOption { Name = Arrived, Value = Arrived.HtmlClassify() }, + new ListValueOption { Name = Pending, Value = OrderStatusCodes.Pending }, + new ListValueOption { Name = Ordered, Value = OrderStatusCodes.Ordered }, + new ListValueOption { Name = Shipped, Value = OrderStatusCodes.Shipped }, + new ListValueOption { Name = Arrived, Value = OrderStatusCodes.Arrived }, + new ListValueOption { Name = PaymentFailed, Value = OrderStatusCodes.PaymentFailed }, ], - DefaultValue = Pending.HtmlClassify(), + DefaultValue = OrderStatusCodes.Pending, Editor = EditorOption.Radio, })) .WithField(nameof(OrderPart.Email), field => field @@ -112,7 +113,7 @@ await _contentDefinitionManager .WithDisplayName("Buyer is a corporation")) ); - return 7; + return 8; } public async Task UpdateFrom1Async() @@ -149,11 +150,11 @@ await _contentDefinitionManager { Options = [ - new() { Name = Ordered, Value = Ordered.HtmlClassify() }, - new() { Name = Shipped, Value = Shipped.HtmlClassify() }, - new() { Name = Arrived, Value = Arrived.HtmlClassify() }, + new() { Name = Ordered, Value = OrderStatusCodes.Ordered }, + new() { Name = Shipped, Value = OrderStatusCodes.Shipped }, + new() { Name = Arrived, Value = OrderStatusCodes.Arrived }, ], - DefaultValue = Pending.HtmlClassify(), + DefaultValue = OrderStatusCodes.Pending, Editor = EditorOption.Radio, })) .WithField(nameof(OrderPart.BillingAddress), field => field @@ -186,12 +187,12 @@ await _contentDefinitionManager { Options = [ - new ListValueOption { Name = Pending, Value = Pending.HtmlClassify() }, - new ListValueOption { Name = Ordered, Value = Ordered.HtmlClassify() }, - new ListValueOption { Name = Shipped, Value = Shipped.HtmlClassify() }, - new ListValueOption { Name = Arrived, Value = Arrived.HtmlClassify() }, + new ListValueOption { Name = Pending, Value = OrderStatusCodes.Pending }, + new ListValueOption { Name = Ordered, Value = OrderStatusCodes.Ordered }, + new ListValueOption { Name = Shipped, Value = OrderStatusCodes.Shipped }, + new ListValueOption { Name = Arrived, Value = OrderStatusCodes.Arrived }, ], - DefaultValue = Pending.HtmlClassify(), + DefaultValue = OrderStatusCodes.Pending, Editor = EditorOption.Radio, })) ); @@ -280,4 +281,31 @@ public int UpdateFrom6() return 7; } + + public async Task UpdateFrom7Async() + { + await _contentDefinitionManager + .AlterPartDefinitionAsync(nameof(OrderPart), part => part + .WithField(nameof(OrderPart.Status), field => field + .OfType(nameof(TextField)) + .WithDisplayName(nameof(OrderPart.Status)) + .WithDescription("The status of the order.") + .WithEditor("PredefinedList") + .WithSettings(new TextFieldPredefinedListEditorSettings + { + Options = + [ + new ListValueOption { Name = Pending, Value = OrderStatusCodes.Pending }, + new ListValueOption { Name = Ordered, Value = OrderStatusCodes.Ordered }, + new ListValueOption { Name = Shipped, Value = OrderStatusCodes.Shipped }, + new ListValueOption { Name = Arrived, Value = OrderStatusCodes.Arrived }, + new ListValueOption { Name = PaymentFailed, Value = OrderStatusCodes.PaymentFailed }, + ], + DefaultValue = OrderStatusCodes.Pending, + Editor = EditorOption.Radio, + })) + ); + + return 8; + } } From 00a670d5503b88c73e26bb94df99f904fcf2c043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 16:31:10 +0200 Subject: [PATCH 50/90] Add documentation. --- OrchardCore.Commerce.sln | 1 + docs/features/exactly-payment.md | 32 ++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 34 insertions(+) create mode 100644 docs/features/exactly-payment.md diff --git a/OrchardCore.Commerce.sln b/OrchardCore.Commerce.sln index f4865a21e..aeedd9aec 100644 --- a/OrchardCore.Commerce.sln +++ b/OrchardCore.Commerce.sln @@ -79,6 +79,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "features", "features", "{9B docs\features\user-features.md = docs\features\user-features.md docs\features\workflows.md = docs\features\workflows.md docs\features\payment-providers.md = docs\features\payment-providers.md + docs\features\exactly-payment.md = docs\features\exactly-payment.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "actions", "actions", "{83C01924-6F58-4777-A9EC-07943F7A2E31}" diff --git a/docs/features/exactly-payment.md b/docs/features/exactly-payment.md new file mode 100644 index 000000000..7e3bf1da1 --- /dev/null +++ b/docs/features/exactly-payment.md @@ -0,0 +1,32 @@ +# Exactly Payment + +Orchard Core Commerce supports multiple [payment providers](payment-providers.md). [Exactly](https://exactly.com/) is one of the officially included ones. Unlike [our Stripe implementation](stripe-payment.md), it uses redirects to send you to a payment interface on their domain. + +To start using, follow these steps: +1. Sign up via . +2. Once they respond, ask your contact person for the following: + - Whitelist for your web domain (at the time of writing _localhost_ is not supported). + - If needed, a sandbox project you can use for testing. + - A live project for your final site. +3. Go to . +4. Take note of your _Project ID_ and your _Project API key_ (both are GUID style hexadecimal strings). +5. On your Orchard Core tenant go to Admin dashboard > Configuration > Features. +6. Make sure the _Orchard Core Commerce - Payment - Exactly_ feature is enabled. +7. Go to Admin dashboard > Configuration > Commerce > Exactly API. +8. Fill out the _Project ID_ and _API key_ fields. (Don't alter the _API base address_ field!) +9. Save and then click the _Verify currently saved API configuration_ button to test it. This will create a new transaction you can check on . + +Once you have set up the site configuration, an additional _Pay with Exactly_ button will appear during checkout. + +### Cards + +There are available test cards that can be found in [Stripe's documentation](https://stripe.com/docs/testing). + +There are multiple test cards that can simulate any scenario, including error codes. Here are two examples: + +| Brand | Number | CVC | Date | Result | +|------------|---------------------| ------------ | --------------- |--------------------------------------------------| +| Visa | 4000 0000 0000 7775 | Any 3 digits | Any future date | the card has insufficient funds | +| Visa | 4000 0000 0000 3220 | Any 3 digits | Any future date | success during 3DS Auth (3DS is always expected) | +| Visa | 4000 0084 0000 1280 | Any 3 digits | Any future date | the card fails 3DS Auth (3DS is always expected) | +| Mastercard | 5555 5555 5555 4444 | Any 3 digits | Any future date | Mastercard test card | diff --git a/mkdocs.yml b/mkdocs.yml index 92870da2d..8e0abdcef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,6 +80,7 @@ nav: - Inventory: features/inventory.md - Products and Prices: features/products-and-prices.md - Promotions: features/promotions.md + - Exactly Payment: features/exactly-payment.md - Stripe Payment: features/stripe-payment.md - Taxation: features/taxation.md - User Features: features/user-features.md From e78ce118568b37e0c22fe99bc9fe3351bb2dc0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 16:37:08 +0200 Subject: [PATCH 51/90] Code cleanup --- .../Constants/OrderStatuses.cs | 16 ++++++++-------- .../Models/OrderPart.cs | 7 ++----- .../Models/ChargeResponse.cs | 5 +++++ .../Controllers/StripeController.cs | 2 -- .../Services/StripePaymentService.cs | 4 ++-- .../Controllers/PaymentController.cs | 4 ++-- .../Services/PaymentService.cs | 4 ++-- .../Migrations/OrderMigrations.cs | 2 +- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs index cdeda8cbb..1f641cfce 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Constants/OrderStatuses.cs @@ -7,13 +7,13 @@ public static class OrderStatuses public const string Ordered = nameof(Ordered); public const string Arrived = nameof(Arrived); public const string Shipped = nameof(Shipped); +} - public static class OrderStatusCodes - { - public const string Pending = "pending"; - public const string PaymentFailed = "payment-failed"; - public const string Ordered = "ordered"; - public const string Arrived = "arrived"; - public const string Shipped = "shipped"; - } +public static class OrderStatusCodes +{ + public const string Pending = "pending"; + public const string PaymentFailed = "payment-failed"; + public const string Ordered = "ordered"; + public const string Arrived = "arrived"; + public const string Shipped = "shipped"; } diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs index ed781ed30..9f4c15ab5 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs @@ -5,12 +5,9 @@ using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.ContentFields.Fields; using OrchardCore.ContentManagement; -using OrchardCore.Mvc.Utilities; using System; using System.Collections.Generic; -using static OrchardCore.Commerce.Abstractions.Constants.OrderStatuses; - namespace OrchardCore.Commerce.Abstractions.Models; public class OrderPart : ContentPart @@ -61,12 +58,12 @@ public class OrderPart : ContentPart public bool IsFailed => Status?.Text?.EqualsOrdinalIgnoreCase(OrderStatusCodes.PaymentFailed) == true; /// - /// Sets the to . + /// Sets the OrderStatusCodesderStatusCodes.PaymentFailed"/>. /// public void FailPayment() => Status.Text = OrderStatusCodes.PaymentFailed; /// - /// Sets the to . + /// Sets the to . /// public void SucceedPayment() => Status.Text = OrderStatusCodes.Ordered; } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs index 56bc2adff..95d6963e4 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs @@ -2,6 +2,7 @@ using OrchardCore.Commerce.MoneyDataType.Abstractions; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -21,6 +22,10 @@ public class ChargeResponse : IExactlyResponseData public string Id { get; set; } public ChargeAttributes Attributes { get; set; } + [SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1313:Parameter names should begin with lower-case letter", + Justification = "Necessary for localization extractor.")] public static IDictionary GetResultCodes(IStringLocalizer T) => new Dictionary { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs index 8193c5e06..0dfabd379 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Controllers/StripeController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Newtonsoft.Json; -using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.Stripe.Abstractions; @@ -11,7 +10,6 @@ using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Mvc.Core.Utilities; -using OrchardCore.Mvc.Utilities; using Stripe; using System.Threading.Tasks; using Address = OrchardCore.Commerce.AddressDataType.Address; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs index 38fd3c0b3..4e524d1ca 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs @@ -117,7 +117,7 @@ public async Task UpdateOrderToPaymentFailedAsync(PaymentIntent paymentIntent) { var order = await GetOrderByPaymentIntentIdAsync(paymentIntent.Id); order.Alter(orderPart => - orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.OrderStatusCodes.PaymentFailed }); + orderPart.Status = new TextField { ContentItem = order, Text = OrderStatusCodes.PaymentFailed }); await _contentManager.UpdateAsync(order); } @@ -147,7 +147,7 @@ public async Task CreateOrUpdateOrderFromShoppingCartAsync(IUpdateM // Shopping cart orderPart.LineItems.SetItems(lineItems); - orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.OrderStatusCodes.Pending }; + orderPart.Status = new TextField { ContentItem = order, Text = OrderStatusCodes.Pending }; // Store the current applicable discount info so they will be available in the future. orderPart.AdditionalData.SetDiscountsByProduct(cartViewModel diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index 6f2256e19..2227e2512 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -256,9 +256,9 @@ public async Task Callback(string paymentProviderName, string? or : await _contentManager.GetAsync(orderId); if (order is null) return NotFound(); - var status = order.As()?.Status?.Text ?? OrderStatuses.OrderStatusCodes.Pending; + var status = order.As()?.Status?.Text ?? OrderStatusCodes.Pending; - if (status != OrderStatuses.OrderStatusCodes.Pending && status != OrderStatuses.OrderStatusCodes.PaymentFailed) + if (status is not OrderStatusCodes.Pending and not OrderStatusCodes.PaymentFailed) { return this.RedirectToContentDisplay(order); } diff --git a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs index 3b19f4866..2083c5a32 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs @@ -194,7 +194,7 @@ await _paymentProvidersLazy await order.AlterAsync(async orderPart => { orderPart.LineItems.SetItems(lineItems); - orderPart.Status.Text = OrderStatuses.OrderStatusCodes.Pending; + orderPart.Status.Text = OrderStatusCodes.Pending; if (orderPart.BillingAndShippingAddressesMatch.Value) { @@ -258,7 +258,7 @@ public async Task UpdateOrderToOrderedAsync( orderPart.Charges.SetItems(newCharges); } - orderPart.Status = new TextField { ContentItem = order, Text = OrderStatuses.OrderStatusCodes.Ordered }; + orderPart.Status = new TextField { ContentItem = order, Text = OrderStatusCodes.Ordered }; }); await _orderEvents.AwaitEachAsync(orderEvent => orderEvent.OrderedAsync(order, shoppingCartId)); diff --git a/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs index 7ecdf1264..b3d08e1fe 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/OrderMigrations.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Fields; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; @@ -13,7 +14,6 @@ using OrchardCore.Data.Migration; using OrchardCore.Environment.Shell.Scope; using OrchardCore.Html.Models; -using OrchardCore.Mvc.Utilities; using OrchardCore.Title.Models; using System; using System.Diagnostics.CodeAnalysis; From 363f2c5d4844e6123dc87527a8d3f137933ba616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 9 Apr 2024 23:58:00 +0200 Subject: [PATCH 52/90] Just get rid of the base address editor, it's dangerous! --- docs/features/exactly-payment.md | 2 +- .../Views/ExactlySettings.Edit.cshtml | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/features/exactly-payment.md b/docs/features/exactly-payment.md index 7e3bf1da1..1a190b6e4 100644 --- a/docs/features/exactly-payment.md +++ b/docs/features/exactly-payment.md @@ -13,7 +13,7 @@ To start using, follow these steps: 5. On your Orchard Core tenant go to Admin dashboard > Configuration > Features. 6. Make sure the _Orchard Core Commerce - Payment - Exactly_ feature is enabled. 7. Go to Admin dashboard > Configuration > Commerce > Exactly API. -8. Fill out the _Project ID_ and _API key_ fields. (Don't alter the _API base address_ field!) +8. Fill out the _Project ID_ and _API key_ fields. 9. Save and then click the _Verify currently saved API configuration_ button to test it. This will create a new transaction you can check on . Once you have set up the site configuration, an additional _Pay with Exactly_ button will appear during checkout. diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 09b1e000a..f00f85979 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -17,10 +17,6 @@ -
-
From bcd42129a9d00f017eb4ea7bfdc8bf3d9e1c55e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Apr 2024 00:04:10 +0200 Subject: [PATCH 53/90] Adjust documentation. --- docs/features/exactly-payment.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/features/exactly-payment.md b/docs/features/exactly-payment.md index 1a190b6e4..f1aeba19d 100644 --- a/docs/features/exactly-payment.md +++ b/docs/features/exactly-payment.md @@ -3,9 +3,9 @@ Orchard Core Commerce supports multiple [payment providers](payment-providers.md). [Exactly](https://exactly.com/) is one of the officially included ones. Unlike [our Stripe implementation](stripe-payment.md), it uses redirects to send you to a payment interface on their domain. To start using, follow these steps: -1. Sign up via . +1. Sign up [here](https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ). 2. Once they respond, ask your contact person for the following: - - Whitelist for your web domain (at the time of writing _localhost_ is not supported). + - Whitelist for your web domain. - If needed, a sandbox project you can use for testing. - A live project for your final site. 3. Go to . @@ -18,14 +18,16 @@ To start using, follow these steps: Once you have set up the site configuration, an additional _Pay with Exactly_ button will appear during checkout. +> ℹ At the time of writing callback URLs targeting _localhost_ are not supported. If you want to test your site locally, we suggest adding a whitelisted domain to your [hosts file](https://en.wikipedia.org/wiki/Hosts_(file)). The address doesn't have to be accessible from their server so this approach is safer than exposing your machine via port forwarding or tunneling.. + ### Cards There are available test cards that can be found in [Stripe's documentation](https://stripe.com/docs/testing). There are multiple test cards that can simulate any scenario, including error codes. Here are two examples: -| Brand | Number | CVC | Date | Result | -|------------|---------------------| ------------ | --------------- |--------------------------------------------------| +| Brand | Number | CVC | Date | Result | +|------------|---------------------|--------------|-----------------|--------------------------------------------------| | Visa | 4000 0000 0000 7775 | Any 3 digits | Any future date | the card has insufficient funds | | Visa | 4000 0000 0000 3220 | Any 3 digits | Any future date | success during 3DS Auth (3DS is always expected) | | Visa | 4000 0084 0000 1280 | Any 3 digits | Any future date | the card fails 3DS Auth (3DS is always expected) | From ba6a64e4b98f3a25b6ecf1949df59e3fa2aa230f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Apr 2024 00:26:28 +0200 Subject: [PATCH 54/90] Add info notification with referral. --- .../Views/ExactlySettings.Edit.cshtml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index f00f85979..6691d8471 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -4,6 +4,12 @@ @using OrchardCore.Commerce.Payment.Exactly.Controllers @model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings +

+ @T["Please be sure to use this link to create your exactly account.", "https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ"] + @T["That way Lombiq (the steward of the Orchard Core Commerce project) will get a commission on the payment fees, which helps cover some of the development costs of OCC."] + @T["This is at no cost to you; the fees you pay are the same either way."] +

+
Date: Wed, 10 Apr 2024 00:45:44 +0200 Subject: [PATCH 55/90] Spelling. --- .github/actions/spelling/allow/occ.txt | 3 +++ .../OrchardCore.Commerce.Abstractions/Models/OrderPart.cs | 2 +- .../Models/ChargeResponse.cs | 3 ++- .../Models/{ExactlySetings.cs => ExactlySettings.cs} | 0 .../Services/ExactlyService.cs | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) rename src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/{ExactlySetings.cs => ExactlySettings.cs} (100%) diff --git a/.github/actions/spelling/allow/occ.txt b/.github/actions/spelling/allow/occ.txt index 75bf7e879..7fbcf6a82 100644 --- a/.github/actions/spelling/allow/occ.txt +++ b/.github/actions/spelling/allow/occ.txt @@ -19,6 +19,7 @@ GST htmlfield LCID markdownlint +Mastercard mkdocs numericfield Nunavut @@ -30,10 +31,12 @@ pricefield roadmap shoppingcart skus +sso testfreeproduct testdiscountedproduct testproduct testproductvariant unpublish +vnd webhooks webshop \ No newline at end of file diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs index 9f4c15ab5..98d39f185 100644 --- a/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Models/OrderPart.cs @@ -58,7 +58,7 @@ public class OrderPart : ContentPart public bool IsFailed => Status?.Text?.EqualsOrdinalIgnoreCase(OrderStatusCodes.PaymentFailed) == true; /// - /// Sets the OrderStatusCodesderStatusCodes.PaymentFailed"/>. + /// Sets the to . /// public void FailPayment() => Status.Text = OrderStatusCodes.PaymentFailed; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs index 95d6963e4..2f0d3c0d9 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeResponse.cs @@ -37,7 +37,8 @@ public static IDictionary GetResultCodes(IStringLocaliz ["reversals_restricted_for_transaction"] = T["reversals not allowed for the original payment"], ["reversible_amount_exceeded"] = T["amount to reverse exceeds the amount of the original payment"], ["not_allowed_for_failed_transaction"] = T["the operation is not allowed for the failed transaction"], - ["sub_recurrings_restricted_for_transaction"] = T["subsequent recurring payments are not allowed for the initial payment"], + ["sub_recurrings_restricted_for_transaction"] = // #spell-check-ignore-line + T["subsequent recurring payments are not allowed for the initial payment"], ["already_captured"] = T["payment was already captured"], ["capture_amount_exceeded"] = T["capture amount exceeds the amount of the original authorize"], ["authentication_failed"] = T["customer failed 3DS or any other authentication"], diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySettings.cs similarity index 100% rename from src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySetings.cs rename to src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ExactlySettings.cs diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index ee4b174c1..19990330a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -53,7 +53,7 @@ public async Task GetTransactionDetailsAsync( throw new TimeoutException( $"Couldn't get the transaction \"{transactionId}\" with the status \"{waitForStatus}\" within " + - $"the expected timeframe."); + $"the expected time frame."); } private static T EvaluateResult(IApiResponse> result) From a09ae7f0d8c176285add315cdc76148cec00e51e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Apr 2024 01:51:13 +0200 Subject: [PATCH 56/90] Mention referral in docs too. --- docs/features/exactly-payment.md | 2 +- src/Modules/OrchardCore.Commerce.Payment.Exactly/Readme.md | 2 +- src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/exactly-payment.md b/docs/features/exactly-payment.md index f1aeba19d..0894d5b6a 100644 --- a/docs/features/exactly-payment.md +++ b/docs/features/exactly-payment.md @@ -3,7 +3,7 @@ Orchard Core Commerce supports multiple [payment providers](payment-providers.md). [Exactly](https://exactly.com/) is one of the officially included ones. Unlike [our Stripe implementation](stripe-payment.md), it uses redirects to send you to a payment interface on their domain. To start using, follow these steps: -1. Sign up [here](https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ). +1. Sign up to Exactly. Please be sure to use [this link](https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ) to create your account. That way [Lombiq](https://lombiq.com/\) (the steward of the Orchard Core Commerce project) will get a commission on the payment fees, which helps cover some of the development costs of OCC. This is at no cost to you; the fees you pay are the same either way. 2. Once they respond, ask your contact person for the following: - Whitelist for your web domain. - If needed, a sandbox project you can use for testing. diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Readme.md b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Readme.md index 8f163896a..3ea7f8119 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Readme.md +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Readme.md @@ -2,6 +2,6 @@ ## About -Stripe payment provider for Orchard Core Commerce. +Exactly payment provider for Orchard Core Commerce. See [the documentation](https://commerce.orchardcore.net/en/latest/features/exactly-payment/) for more details. 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/Readme.md b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md index 626fa7db3..79a2e3fa7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md @@ -2,6 +2,6 @@ ## About -Stripe payment provider for Orchard Core Commerce. +Stripe payment provider for Orchard Core Commerce. See [the documentation](https://commerce.orchardcore.net/en/latest/features/stripe-payment/) for more details. For general details about and on using Orchard Core Commerce see the [root Readme](../../../Readme.md). From d5dce4251b3917a92a2ea2850f212997669896a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 10 Apr 2024 01:54:10 +0200 Subject: [PATCH 57/90] wat? --- src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md index 79a2e3fa7..77cb976ae 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Readme.md @@ -1,4 +1,4 @@ -# Orchard Core Commerce - Inventory +# Orchard Core Commerce - Payment - Stripe ## About From 2d68f1a9b5331b8eb8253567d6822f4185b2cd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 15 Apr 2024 09:12:38 +0200 Subject: [PATCH 58/90] Create CreateOrderPartAddressViewModelsAsync extension method. --- .../Extensions/UpdateModelExtensions.cs | 24 +++++++++++++++++++ .../Controllers/PaymentController.cs | 13 ++-------- 2 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 src/Modules/OrchardCore.Commerce.ContentFields/Extensions/UpdateModelExtensions.cs diff --git a/src/Modules/OrchardCore.Commerce.ContentFields/Extensions/UpdateModelExtensions.cs b/src/Modules/OrchardCore.Commerce.ContentFields/Extensions/UpdateModelExtensions.cs new file mode 100644 index 000000000..524cb1ed4 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.ContentFields/Extensions/UpdateModelExtensions.cs @@ -0,0 +1,24 @@ +using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.DisplayManagement.ModelBinding; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace OrchardCore.Commerce.ViewModels; + +public static class UpdateModelExtensions +{ + public static async Task<(AddressFieldViewModel Shipping, AddressFieldViewModel Billing)> CreateOrderPartAddressViewModelsAsync( + this IUpdateModel updater) + { + var shippingViewModel = new AddressFieldViewModel(); + var billingViewModel = new AddressFieldViewModel(); + if (!await updater.TryUpdateModelAsync(shippingViewModel, $"{nameof(OrderPart)}.{nameof(OrderPart.ShippingAddress)}") || + !await updater.TryUpdateModelAsync(billingViewModel, $"{nameof(OrderPart)}.{nameof(OrderPart.BillingAddress)}")) + { + throw new InvalidOperationException(updater.GetModelErrorMessages().JoinNotNullOrEmpty()); + } + + return (Shipping: shippingViewModel, Billing: billingViewModel); + } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index 2227e2512..2a2f1f9d3 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -21,12 +21,10 @@ using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Mvc.Core.Utilities; -using OrchardCore.Mvc.Utilities; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - using FrontendException = Lombiq.HelpfulLibraries.AspNetCore.Exceptions.FrontendException; namespace OrchardCore.Commerce.Payment.Controllers; @@ -104,15 +102,8 @@ await this.SafeJsonAsync(async () => throw new InvalidOperationException("Unauthorized."); } - var updater = _updateModelAccessor.ModelUpdater; - var shippingViewModel = new AddressFieldViewModel(); - var billingViewModel = new AddressFieldViewModel(); - if (!await updater.TryUpdateModelAsync(shippingViewModel, $"{nameof(OrderPart)}.{nameof(OrderPart.ShippingAddress)}") || - !await updater.TryUpdateModelAsync(billingViewModel, $"{nameof(OrderPart)}.{nameof(OrderPart.BillingAddress)}")) - { - throw new InvalidOperationException( - _updateModelAccessor.ModelUpdater.GetModelErrorMessages().JoinNotNullOrEmpty()); - } + var (shippingViewModel, billingViewModel) = + await _updateModelAccessor.ModelUpdater.CreateOrderPartAddressViewModelsAsync(); var checkoutViewModel = await _paymentService.CreateCheckoutViewModelAsync( shoppingCartId, From 8a0845422411272ec140e4390a70e2e8de9fdb38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 15 Apr 2024 09:40:38 +0200 Subject: [PATCH 59/90] Extract logic from ~/checkout/price into IPaymentService.GetTotalAsync(). --- .../Abstractions/IPaymentService.cs | 5 ++++ .../Controllers/PaymentController.cs | 13 +--------- .../Services/PaymentService.cs | 24 +++++++++++++++++-- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs index 47b0d7a87..26198681c 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Abstractions/IPaymentService.cs @@ -28,6 +28,11 @@ public interface IPaymentService string? shoppingCartId, Action? updateOrderPart = null); + /// + /// Calculates the shopping cart's checkout total. + /// + public Task GetTotalAsync(string? shoppingCartId); + /// /// When the order is payed this logic should be run to set properties that represents its state. /// diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index 2a2f1f9d3..a75c3f831 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -102,18 +102,7 @@ await this.SafeJsonAsync(async () => throw new InvalidOperationException("Unauthorized."); } - var (shippingViewModel, billingViewModel) = - await _updateModelAccessor.ModelUpdater.CreateOrderPartAddressViewModelsAsync(); - - var checkoutViewModel = await _paymentService.CreateCheckoutViewModelAsync( - shoppingCartId, - part => - { - part.ShippingAddress.Address = shippingViewModel.Address ?? part.ShippingAddress.Address; - part.BillingAddress.Address = billingViewModel.Address ?? part.BillingAddress.Address; - }); - - var total = checkoutViewModel?.SingleCurrencyTotal ?? new Amount(0, _moneyService.DefaultCurrency); + var total = await _paymentService.GetTotalAsync(shoppingCartId); return new { diff --git a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs index 2083c5a32..2488aa193 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Services/PaymentService.cs @@ -9,17 +9,18 @@ using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Extensions; using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.MoneyDataType.Abstractions; using OrchardCore.Commerce.MoneyDataType.Extensions; using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.ViewModels; using OrchardCore.Commerce.Services; 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; @@ -43,6 +44,7 @@ public class PaymentService : IPaymentService private readonly Lazy> _paymentProvidersLazy; private readonly IEnumerable _checkoutEvents; private readonly INotifier _notifier; + private readonly IMoneyService _moneyService; private readonly IHtmlLocalizer H; // We need all of them. @@ -57,7 +59,8 @@ public PaymentService( IEnumerable orderEvents, Lazy> paymentProvidersLazy, IEnumerable checkoutEvents, - INotifier notifier) + INotifier notifier, + IMoneyService moneyService) #pragma warning restore S107 // Methods should not have too many parameters { _fieldsOnlyDisplayManager = fieldsOnlyDisplayManager; @@ -71,6 +74,7 @@ public PaymentService( _paymentProvidersLazy = paymentProvidersLazy; _checkoutEvents = checkoutEvents; _notifier = notifier; + _moneyService = moneyService; _hca = services.HttpContextAccessor.Value; H = services.HtmlLocalizer.Value; } @@ -148,6 +152,22 @@ await _notifier.WarningAsync(new HtmlString(" ").Join( return viewModel; } + public async Task GetTotalAsync(string? shoppingCartId) + { + var (shippingViewModel, billingViewModel) = + await _updateModelAccessor.ModelUpdater.CreateOrderPartAddressViewModelsAsync(); + + var checkoutViewModel = await CreateCheckoutViewModelAsync( + shoppingCartId, + part => + { + part.ShippingAddress.Address = shippingViewModel.Address ?? part.ShippingAddress.Address; + part.BillingAddress.Address = billingViewModel.Address ?? part.BillingAddress.Address; + }); + + return checkoutViewModel?.SingleCurrencyTotal ?? new Amount(0, _moneyService.DefaultCurrency); + } + public async Task FinalModificationOfOrderAsync(ContentItem order, string? shoppingCartId, string? paymentProviderName) { await _orderEvents.AwaitEachAsync(orderEvent => From c225ffbfb97d438d90833d9fa636db10e37c469b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 15 Apr 2024 09:41:19 +0200 Subject: [PATCH 60/90] Bug fix incorrect price when discounted. Use GetTotalAsync for Exactly charge total calculation. --- .../Controllers/ExactlyController.cs | 7 ++-- .../Models/ChargeRequest.cs | 32 ++++++++++++------- .../Services/ExactlyService.cs | 5 +-- .../Services/IExactlyService.cs | 12 ++++++- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 21e496a1c..98a834bd4 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -65,6 +65,7 @@ public async Task VerifyApi() { try { + var testAmount = new Amount(1, Currency.Euro); var order = await _contentManager.NewAsync(Order); order.Alter(part => { @@ -73,13 +74,13 @@ public async Task VerifyApi() quantity: 1, "TEST", "TEST", - new Amount(1, Currency.Euro), - new Amount(1, Currency.Euro), + testAmount, + testAmount, contentItemVersion: null)); }); await _contentManager.CreateAsync(order); - var result = await _exactlyService.CreateTransactionAsync(order.As()); + var result = await _exactlyService.CreateTransactionAsync(order.As(), testAmount); var action = await GetActionRedirectRequestedAsync(result.Id); await _notifier.SuccessAsync( diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs index 0bde594ca..b0f432ec5 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeRequest.cs @@ -5,11 +5,13 @@ using Microsoft.Extensions.Options; using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Abstractions.Models; -using OrchardCore.Commerce.MoneyDataType.Extensions; +using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.Controllers; using OrchardCore.Commerce.Payment.Exactly.Services; using OrchardCore.Users.Models; using System; +using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -31,35 +33,41 @@ public class ChargeRequest : IExactlyRequestAttributes, IExactlyAmount public string CustomerId { get; set; } public string Email { get; set; } public int Lifetime { get; set; } = 3600; - public object Meta { get; set; } + public object Meta { get; set; } = string.Empty; [JsonConstructor] private ChargeRequest() { } - public ChargeRequest(OrderPart orderPart, User user, string projectId, Uri returnUrl) + public ChargeRequest( + string orderId, + IEnumerable lineItems, + Amount total, + User user, + string projectId, + Uri returnUrl) { if (!returnUrl.IsAbsoluteUri) throw new ArgumentException("The return URL must be absolute.", nameof(returnUrl)); - var descriptionParts = orderPart - .LineItems - .Select(item => StringHelper.CreateInvariant($"{item.Quantity} × {item.UnitPrice} {item.FullSku}")); + var descriptionParts = lineItems.Select(item => StringHelper.CreateInvariant($"{item.Quantity} × {item.FullSku}")); ProjectId = projectId; - ReferenceId = orderPart.OrderId.Text; + ReferenceId = orderId; CustomerDescription = string.Join(", ", descriptionParts); ReturnUrl = returnUrl.AbsoluteUri; CustomerId = user.UserId; Email = user.Email; - Meta = orderPart.ContentItem.ContentItemId; - this.SetAmount(orderPart.LineItems.Select(item => item.LinePrice).Sum()); + this.SetAmount(total); } public static implicit operator ExactlyRequest(ChargeRequest attributes) => new() { Attributes = attributes }; - public static async Task CreateForCurrentUserAsync(OrderPart orderPart, HttpContext context) + public static async Task CreateForCurrentUserAsync( + OrderPart orderPart, + HttpContext context, + Amount? total = null) { var provider = context.RequestServices; @@ -70,7 +78,9 @@ public static async Task CreateForCurrentUserAsync(OrderPart orde var absoluteReturnUrl = new Uri(new Uri(context.Request.GetDisplayUrl()), returnUrl); return new ChargeRequest( - orderPart, + orderPart.ContentItem.ContentItemId, + orderPart.LineItems, + total ?? await provider.GetRequiredService().GetTotalAsync(shoppingCartId: null), await provider.GetRequiredService().GetFullUserAsync(context.User), provider.GetRequiredService>().Value.ProjectId, absoluteReturnUrl); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs index 19990330a..0028a19cf 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/ExactlyService.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Payment.Exactly.Models; using Refit; using System; @@ -22,9 +23,9 @@ public ExactlyService(IExactlyApi api, IHttpContextAccessor hca) _hca = hca; } - public async Task CreateTransactionAsync(OrderPart orderPart) + public async Task CreateTransactionAsync(OrderPart orderPart, Amount? total = null) { - var charge = await ChargeRequest.CreateForCurrentUserAsync(orderPart, _hca.HttpContext); + var charge = await ChargeRequest.CreateForCurrentUserAsync(orderPart, _hca.HttpContext, total); var request = new ExactlyDataWrapper>( new ExactlyRequest { Attributes = charge }); diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs index 793f95ed1..ed17845c0 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Services/IExactlyService.cs @@ -1,5 +1,7 @@ using OrchardCore.Commerce.Abstractions.Models; +using OrchardCore.Commerce.MoneyDataType; using OrchardCore.Commerce.Payment.Exactly.Models; +using OrchardCore.ContentManagement; using System.Threading; using System.Threading.Tasks; @@ -13,7 +15,15 @@ public interface IExactlyService /// /// Creates a new transaction for the current user based on the provided order. /// - Task CreateTransactionAsync(OrderPart orderPart); + /// + /// The part whose and + /// are used in the request. + /// + /// + /// The charge total to be sent in the request. If , it's calculated using + /// the total from the current shopping cart with all checkout events applied. + /// + Task CreateTransactionAsync(OrderPart orderPart, Amount? total = null); /// /// Returns details of a transaction. From f28eb24890112610e18a86c9ea6e15549fc78edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 15 Apr 2024 09:52:43 +0200 Subject: [PATCH 61/90] Remove unused service injection. --- .../Controllers/PaymentController.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs index a75c3f831..eadca58a0 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment/Controllers/PaymentController.cs @@ -10,13 +10,10 @@ using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Abstractions.Constants; using OrchardCore.Commerce.Abstractions.Models; -using OrchardCore.Commerce.MoneyDataType; -using OrchardCore.Commerce.MoneyDataType.Abstractions; using OrchardCore.Commerce.MoneyDataType.Extensions; using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.Constants; using OrchardCore.Commerce.Payment.ViewModels; -using OrchardCore.Commerce.ViewModels; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; @@ -38,7 +35,6 @@ public class PaymentController : Controller private readonly IStringLocalizer T; private readonly IHtmlLocalizer H; private readonly INotifier _notifier; - private readonly IMoneyService _moneyService; private readonly IEnumerable _paymentProviders; private readonly IPaymentService _paymentService; private readonly IRegionService _regionService; @@ -47,7 +43,6 @@ public PaymentController( IOrchardServices services, IUpdateModelAccessor updateModelAccessor, INotifier notifier, - IMoneyService moneyService, IEnumerable paymentProviders, IPaymentService paymentService, IRegionService regionService) @@ -59,7 +54,6 @@ public PaymentController( T = services.StringLocalizer.Value; H = services.HtmlLocalizer.Value; _notifier = notifier; - _moneyService = moneyService; _paymentProviders = paymentProviders; _paymentService = paymentService; _regionService = regionService; From b47ffd68b24291eb93b7b728a3e249c860e6a228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 15 Apr 2024 22:44:53 +0200 Subject: [PATCH 62/90] Update docs/features/exactly-payment.md Co-authored-by: Szabolcs Deme <80963259+DemeSzabolcs@users.noreply.github.com> --- docs/features/exactly-payment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/exactly-payment.md b/docs/features/exactly-payment.md index 0894d5b6a..0a44cfea2 100644 --- a/docs/features/exactly-payment.md +++ b/docs/features/exactly-payment.md @@ -3,7 +3,7 @@ Orchard Core Commerce supports multiple [payment providers](payment-providers.md). [Exactly](https://exactly.com/) is one of the officially included ones. Unlike [our Stripe implementation](stripe-payment.md), it uses redirects to send you to a payment interface on their domain. To start using, follow these steps: -1. Sign up to Exactly. Please be sure to use [this link](https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ) to create your account. That way [Lombiq](https://lombiq.com/\) (the steward of the Orchard Core Commerce project) will get a commission on the payment fees, which helps cover some of the development costs of OCC. This is at no cost to you; the fees you pay are the same either way. +1. Sign up to Exactly. Please be sure to use [this link](https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ) to create your account. That way [Lombiq](https://lombiq.com) (the steward of the Orchard Core Commerce project) will get a commission on the payment fees, which helps cover some of the development costs of OCC. This is at no cost to you; the fees you pay are the same either way. 2. Once they respond, ask your contact person for the following: - Whitelist for your web domain. - If needed, a sandbox project you can use for testing. From 46f53f2cf13e0e009ce370be7952e3dbc1042e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 15 Apr 2024 23:04:39 +0200 Subject: [PATCH 63/90] Spacing. --- Directory.Packages.props | 2 +- .../Views/CheckoutExactly.cshtml | 2 +- .../Views/ExactlySettings.Edit.cshtml | 2 +- .../OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 35ff20cc5..e9f4705bb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,4 +36,4 @@ - \ No newline at end of file + diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml index fd009e181..3c6c967ef 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml @@ -71,4 +71,4 @@ setButtons(true); }); }); - \ No newline at end of file + diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 6691d8471..60823077b 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -52,4 +52,4 @@ $(ids.map((id) => document.getElementById(id))).change(() => button.classList.add('disabled')); }); - \ No newline at end of file + diff --git a/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml index a5efd7af0..de82e67d3 100644 --- a/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment/Views/Payment/Wait.cshtml @@ -17,4 +17,4 @@ \ No newline at end of file + From 8c3fe9aad82d700d16a458be5d717cd7d4dacfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 15 Apr 2024 23:04:57 +0200 Subject: [PATCH 64/90] Fix copy-paste error in documentation. --- docs/features/exactly-payment.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/features/exactly-payment.md b/docs/features/exactly-payment.md index 0894d5b6a..a2e3f1818 100644 --- a/docs/features/exactly-payment.md +++ b/docs/features/exactly-payment.md @@ -22,9 +22,7 @@ Once you have set up the site configuration, an additional _Pay with Exactly_ bu ### Cards -There are available test cards that can be found in [Stripe's documentation](https://stripe.com/docs/testing). - -There are multiple test cards that can simulate any scenario, including error codes. Here are two examples: +There are available test cards that can be found in [Exactly's documentation](https://exactly.com/docs/api#tag/Transactions/operation/createTransaction). Some of these test card numbers are commonly used by other payment providers as well. | Brand | Number | CVC | Date | Result | |------------|---------------------|--------------|-----------------|--------------------------------------------------| From cf7bc3b5bd7556032c01d89773d8a0821aae0f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 15 Apr 2024 23:10:52 +0200 Subject: [PATCH 65/90] Case insensitive GET compare. --- .../OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs index 2b298909f..59473bb76 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Models/ChargeAction.cs @@ -18,6 +18,6 @@ public class ChargeActionAttributes public string HttpMethod { get; set; } [JsonIgnore] - public bool IsGet => HttpMethod == "GET"; + public bool IsGet => "GET".EqualsOrdinalIgnoreCase(HttpMethod); } } From 511cf36515c5264cf9149aee726fa1a6801ed9fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 15 Apr 2024 23:41:47 +0200 Subject: [PATCH 66/90] Set API test order's title. --- .../Controllers/ExactlyController.cs | 5 +++++ .../OrchardCore.Commerce/Services/OrderLineItemService.cs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 98a834bd4..8ad3b62ad 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -2,6 +2,7 @@ using Lombiq.HelpfulLibraries.OrchardCore.Validation; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.MoneyDataType; @@ -12,6 +13,7 @@ using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Mvc.Core.Utilities; +using OrchardCore.Title.Models; using Refit; using System; using System.Linq; @@ -31,6 +33,7 @@ public class ExactlyController : Controller private readonly INotifier _notifier; private readonly IPaymentService _paymentService; private readonly IHtmlLocalizer H; + private readonly IStringLocalizer S; public ExactlyController( IExactlyService exactlyService, @@ -44,6 +47,7 @@ public ExactlyController( _notifier = notifier; _paymentService = paymentService; H = services.HtmlLocalizer.Value; + S = services.StringLocalizer.Value; } [HttpPost] @@ -78,6 +82,7 @@ public async Task VerifyApi() testAmount, contentItemVersion: null)); }); + order.Alter(part => part.Title = S["Exactly API test order"]); await _contentManager.CreateAsync(order); var result = await _exactlyService.CreateTransactionAsync(order.As(), testAmount); diff --git a/src/Modules/OrchardCore.Commerce/Services/OrderLineItemService.cs b/src/Modules/OrchardCore.Commerce/Services/OrderLineItemService.cs index ae1256568..436058346 100644 --- a/src/Modules/OrchardCore.Commerce/Services/OrderLineItemService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/OrderLineItemService.cs @@ -61,7 +61,10 @@ public OrderLineItemService( IList lineItems, OrderPart orderPart) { - if (!lineItems.Any()) return (Array.Empty(), Amount.Unspecified); + if (lineItems.Count == 0 || lineItems.All(item => item.ContentItemVersion == null)) + { + return (Array.Empty(), Amount.Unspecified); + } var products = await _productService.GetProductDictionaryByContentItemVersionsAsync( lineItems.Select(line => line.ContentItemVersion)); From 882ceb7a94f28f3370725e589516ecb2f27a77d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 00:06:11 +0200 Subject: [PATCH 67/90] Fix order title. --- .../Controllers/ExactlyController.cs | 2 +- .../OrchardCore.Commerce/Handlers/OrderPartHandler.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 8ad3b62ad..1035249e3 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -82,7 +82,7 @@ public async Task VerifyApi() testAmount, contentItemVersion: null)); }); - order.Alter(part => part.Title = S["Exactly API test order"]); + order.DisplayText = S["Exactly API test order"]; await _contentManager.CreateAsync(order); var result = await _exactlyService.CreateTransactionAsync(order.As(), testAmount); diff --git a/src/Modules/OrchardCore.Commerce/Handlers/OrderPartHandler.cs b/src/Modules/OrchardCore.Commerce/Handlers/OrderPartHandler.cs index f3bcddc5e..3a78c02a0 100644 --- a/src/Modules/OrchardCore.Commerce/Handlers/OrderPartHandler.cs +++ b/src/Modules/OrchardCore.Commerce/Handlers/OrderPartHandler.cs @@ -26,7 +26,10 @@ public override async Task UpdatedAsync(UpdateContentContext context, OrderPart var guid = orderPart.OrderId.Text ?? Guid.NewGuid().ToString(); orderPart.OrderId.Text = guid; - orderPart.ContentItem.DisplayText = T["Order {0}", guid]; + if (string.IsNullOrWhiteSpace(orderPart.ContentItem.DisplayText)) + { + orderPart.ContentItem.DisplayText = T["Order {0}", guid]; + } orderPart.Apply(); await _session.SaveAsync(orderPart.ContentItem); From 06584453e5a57d6953fb6e412de98313e31c8440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 00:06:25 +0200 Subject: [PATCH 68/90] Merge localized string lines. --- .../Views/ExactlySettings.Edit.cshtml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 60823077b..2658f86d7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -5,9 +5,7 @@ @model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings

- @T["Please be sure to use this link to create your exactly account.", "https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ"] - @T["That way Lombiq (the steward of the Orchard Core Commerce project) will get a commission on the payment fees, which helps cover some of the development costs of OCC."] - @T["This is at no cost to you; the fees you pay are the same either way."] + @T["Please be sure to use this link to create your exactly account. That way Lombiq (the steward of the Orchard Core Commerce project) will get a commission on the payment fees, which helps cover some of the development costs of OCC. This is at no cost to you; the fees you pay are the same either way.", "https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ"]

From c19fe05de5f59057f2756875d5454b937809ad39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 00:50:19 +0200 Subject: [PATCH 69/90] Add Technical overview to the docs. --- docs/features/exactly-payment.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/features/exactly-payment.md b/docs/features/exactly-payment.md index 0d8cbb635..e084fdebe 100644 --- a/docs/features/exactly-payment.md +++ b/docs/features/exactly-payment.md @@ -16,7 +16,7 @@ To start using, follow these steps: 8. Fill out the _Project ID_ and _API key_ fields. 9. Save and then click the _Verify currently saved API configuration_ button to test it. This will create a new transaction you can check on . -Once you have set up the site configuration, an additional _Pay with Exactly_ button will appear during checkout. +Once you have set up the site configuration, an additional _Pay with exactly_ button will appear during checkout. > ℹ At the time of writing callback URLs targeting _localhost_ are not supported. If you want to test your site locally, we suggest adding a whitelisted domain to your [hosts file](https://en.wikipedia.org/wiki/Hosts_(file)). The address doesn't have to be accessible from their server so this approach is safer than exposing your machine via port forwarding or tunneling.. @@ -30,3 +30,13 @@ There are available test cards that can be found in [Exactly's documentation](ht | Visa | 4000 0000 0000 3220 | Any 3 digits | Any future date | success during 3DS Auth (3DS is always expected) | | Visa | 4000 0084 0000 1280 | Any 3 digits | Any future date | the card fails 3DS Auth (3DS is always expected) | | Mastercard | 5555 5555 5555 4444 | Any 3 digits | Any future date | Mastercard test card | + +### Technical overview + +As mentioned above, this module uses redirects to communicate with the payment processor. This means the OrchardCore.Commerce site never sees the buyer's payment information, which avoids potential liability and improves buyer confidence. Here is a broad overview of what happens when you click on the _Pay with exactly_ button: +- JS script sends a POST request that only contains the contents of the checkout page (i.e. addresses). +- C# backend creates a new Order content item from the checkout data and the stored shopping cart. +- C# backend sends a POST request to the Exactly API including the order total and the return URL. +- JS script gets a redirect URL (on Exactly's domain) as response, then navigates there. +- Payment continues on Exactly, then redirects back to the return URL. +- C# backend validates the transaction state, updates Order and redirects to the Success page. From 35f6c689d61f0a83d3256f67bb5c150360a307fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 00:51:02 +0200 Subject: [PATCH 70/90] Turn the signup link into a reusable constant. --- .../Drivers/ExactlySettingsDisplayDriver.cs | 1 + .../Views/ExactlySettings.Edit.cshtml | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs index 5f9fb8066..e3eddb572 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs @@ -16,6 +16,7 @@ namespace OrchardCore.Commerce.Payment.Exactly.Drivers; public class ExactlySettingsDisplayDriver : SectionDisplayDriver { public const string EditorGroupId = "Exactly"; + public const string SignupLink = "https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ"; private readonly IAuthorizationService _authorizationService; private readonly IHttpContextAccessor _hca; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 2658f86d7..7024ba3c4 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -2,10 +2,11 @@ @using Microsoft.AspNetCore.Mvc.Localization @using OrchardCore @using OrchardCore.Commerce.Payment.Exactly.Controllers +@using OrchardCore.Commerce.Payment.Exactly.Drivers @model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings -

- @T["Please be sure to use this link to create your exactly account. That way Lombiq (the steward of the Orchard Core Commerce project) will get a commission on the payment fees, which helps cover some of the development costs of OCC. This is at no cost to you; the fees you pay are the same either way.", "https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ"] +

From 5cf63e5406c4bfd7ed4d57dc9205f293f89a0bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 00:52:18 +0200 Subject: [PATCH 71/90] Add test to verify signup notification. --- .../ExactlyTests/BehaviorExactlyTests.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs diff --git a/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs b/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs new file mode 100644 index 000000000..13e4541f1 --- /dev/null +++ b/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs @@ -0,0 +1,35 @@ +using Lombiq.Tests.UI.Attributes; +using Lombiq.Tests.UI.Extensions; +using Lombiq.Tests.UI.Services; +using OpenQA.Selenium; +using OrchardCore.Commerce.Payment.Exactly.Constants; +using OrchardCore.Commerce.Payment.Exactly.Drivers; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +namespace OrchardCore.Commerce.Tests.UI.Tests.ExactlyTests; + +public class BehaviorExactlyTests : UITestBase +{ + public BehaviorExactlyTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + { + } + + [Theory, Chrome] + public Task ExactlySettingsSignupLinkShouldDisplayCorrectly(Browser browser) => + ExecuteTestAfterSetupAsync( + async context => + { + await context.SignInDirectlyAsync(); + await context.EnableFeatureDirectlyAsync(FeatureIds.Area); + + await context.GoToAdminRelativeUrlAsync("/Settings/Exactly"); + context + .Get(By.CssSelector(".exactly-signup-info a")) + .GetAttribute("href") + .ShouldBe(ExactlySettingsDisplayDriver.SignupLink); + }, + browser); +} From d45f043c11c583b246f6ee982e647f1eae705072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 01:00:08 +0200 Subject: [PATCH 72/90] bug fix remove reference to deleted field --- .../Views/ExactlySettings.Edit.cshtml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 7024ba3c4..1788e318f 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -44,7 +44,6 @@ const button = document.getElementById('ExactlySettings__testButton'); const ids = @Json.Serialize(new[] { - Html.IdFor(model => model.BaseAddress), Html.IdFor(model => model.ProjectId), Html.IdFor(model => model.ApiKey), }); From d68bb4f5d389f724e4bcd1a072c14cb2313825cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 01:01:20 +0200 Subject: [PATCH 73/90] SignUp instead of Signup --- .../Drivers/ExactlySettingsDisplayDriver.cs | 2 +- .../Views/ExactlySettings.Edit.cshtml | 2 +- .../Tests/ExactlyTests/BehaviorExactlyTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs index e3eddb572..fea656edd 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Drivers/ExactlySettingsDisplayDriver.cs @@ -16,7 +16,7 @@ namespace OrchardCore.Commerce.Payment.Exactly.Drivers; public class ExactlySettingsDisplayDriver : SectionDisplayDriver { public const string EditorGroupId = "Exactly"; - public const string SignupLink = "https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ"; + public const string SignUpLink = "https://application.exactly.com/?utm_source=partner&utm_medium=kirill&utm_campaign=LOMBIQ"; private readonly IAuthorizationService _authorizationService; private readonly IHttpContextAccessor _hca; diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index 1788e318f..ea8a0953f 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -6,7 +6,7 @@ @model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings
diff --git a/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs b/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs index 13e4541f1..d3b80076e 100644 --- a/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs +++ b/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs @@ -29,7 +29,7 @@ public Task ExactlySettingsSignupLinkShouldDisplayCorrectly(Browser browser) => context .Get(By.CssSelector(".exactly-signup-info a")) .GetAttribute("href") - .ShouldBe(ExactlySettingsDisplayDriver.SignupLink); + .ShouldBe(ExactlySettingsDisplayDriver.SignUpLink); }, browser); } From 9199e179b2d93eaadce338f781456f46aba06f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 01:04:23 +0200 Subject: [PATCH 74/90] update class too --- .../Views/ExactlySettings.Edit.cshtml | 2 +- .../Tests/ExactlyTests/BehaviorExactlyTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml index ea8a0953f..b84786a2b 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/ExactlySettings.Edit.cshtml @@ -5,7 +5,7 @@ @using OrchardCore.Commerce.Payment.Exactly.Drivers @model OrchardCore.Commerce.Payment.Exactly.Models.ExactlySettings - diff --git a/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs b/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs index d3b80076e..82a96966f 100644 --- a/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs +++ b/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs @@ -27,7 +27,7 @@ public Task ExactlySettingsSignupLinkShouldDisplayCorrectly(Browser browser) => await context.GoToAdminRelativeUrlAsync("/Settings/Exactly"); context - .Get(By.CssSelector(".exactly-signup-info a")) + .Get(By.CssSelector(".exactly-sign-up-info a")) .GetAttribute("href") .ShouldBe(ExactlySettingsDisplayDriver.SignUpLink); }, From fde507a54df854288739774239920a0456d3540c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 01:06:15 +0200 Subject: [PATCH 75/90] sigh --- .../Tests/ExactlyTests/BehaviorExactlyTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs b/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs index 82a96966f..722270b28 100644 --- a/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs +++ b/test/OrchardCore.Commerce.Tests.UI/Tests/ExactlyTests/BehaviorExactlyTests.cs @@ -18,7 +18,7 @@ public BehaviorExactlyTests(ITestOutputHelper testOutputHelper) } [Theory, Chrome] - public Task ExactlySettingsSignupLinkShouldDisplayCorrectly(Browser browser) => + public Task ExactlySettingsSignUpLinkShouldDisplayCorrectly(Browser browser) => ExecuteTestAfterSetupAsync( async context => { From d45ffaeca5b3aeea256c0472ea668279144d192d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 01:12:28 +0200 Subject: [PATCH 76/90] unusing --- .../Controllers/ExactlyController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs index 1035249e3..f11bdc5d7 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Controllers/ExactlyController.cs @@ -13,7 +13,6 @@ using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Mvc.Core.Utilities; -using OrchardCore.Title.Models; using Refit; using System; using System.Linq; From 983f551bb38e345f614a633aac275a11abc5e6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 11:41:58 +0200 Subject: [PATCH 77/90] Add note about only EUR and USD in sandbox. --- docs/features/exactly-payment.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/features/exactly-payment.md b/docs/features/exactly-payment.md index e084fdebe..21acbf715 100644 --- a/docs/features/exactly-payment.md +++ b/docs/features/exactly-payment.md @@ -22,7 +22,7 @@ Once you have set up the site configuration, an additional _Pay with exactly_ bu ### Cards -There are available test cards that can be found in [Exactly's documentation](https://exactly.com/docs/api#tag/Transactions/operation/createTransaction). Some of these test card numbers are commonly used by other payment providers as well. +There are available test cards for the sandbox environment that can be found in [Exactly's documentation](https://exactly.com/docs/api#tag/Transactions/operation/createTransaction). Some of these test card numbers are commonly used by other payment providers as well. | Brand | Number | CVC | Date | Result | |------------|---------------------|--------------|-----------------|--------------------------------------------------| @@ -31,6 +31,8 @@ There are available test cards that can be found in [Exactly's documentation](ht | Visa | 4000 0084 0000 1280 | Any 3 digits | Any future date | the card fails 3DS Auth (3DS is always expected) | | Mastercard | 5555 5555 5555 4444 | Any 3 digits | Any future date | Mastercard test card | +> ⚠ The sandbox environment only supports EUR and USD currencies. If payment is attempted with anything else, it will display a _403.21: Unsupported currency_ error. + ### Technical overview As mentioned above, this module uses redirects to communicate with the payment processor. This means the OrchardCore.Commerce site never sees the buyer's payment information, which avoids potential liability and improves buyer confidence. Here is a broad overview of what happens when you click on the _Pay with exactly_ button: From 403c835499fe481c728f9e406aa2b8c3558b2150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 15:59:04 +0200 Subject: [PATCH 78/90] Add create-webshop.md as solution item. --- OrchardCore.Commerce.sln | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OrchardCore.Commerce.sln b/OrchardCore.Commerce.sln index aeedd9aec..4b0c7ccbb 100644 --- a/OrchardCore.Commerce.sln +++ b/OrchardCore.Commerce.sln @@ -113,6 +113,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Commerce.Abstra EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Commerce.Payment.Exactly", "src\Modules\OrchardCore.Commerce.Payment.Exactly\OrchardCore.Commerce.Payment.Exactly.csproj", "{73925C09-BF96-4727-91D8-57A88AD1601F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "guides", "guides", "{EF8008F1-64F5-4053-A639-6285084AFA52}" + ProjectSection(SolutionItems) = preProject + docs\guides\create-webshop.md = docs\guides\create-webshop.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -208,6 +213,7 @@ Global {A4D69733-CDC0-46AE-B46A-163CCC6F77F9} = {E6C02BDF-EEB0-4ABD-ADEC-9932F60923AE} {28DB6CBB-1527-42A1-8EFE-3D95BF185884} = {90913510-3D7F-4BCC-B55E-56343128F049} {73925C09-BF96-4727-91D8-57A88AD1601F} = {E6C02BDF-EEB0-4ABD-ADEC-9932F60923AE} + {EF8008F1-64F5-4053-A639-6285084AFA52} = {BEBA1764-178A-4722-A193-4DEF26DCE8D1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {456CBC78-579D-483F-A4C3-AF5C12AB3324} From f1068765ad57ac7478bd4f1362689e55ccca6711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 16:40:04 +0200 Subject: [PATCH 79/90] Update "Creating a webshop" guide. --- .../create-webshop/step-6/exactly-form.png | Bin 0 -> 91157 bytes .../create-webshop/step-6/exactly-settings.png | Bin 0 -> 53951 bytes .../create-webshop/step-6/stripe-form.png | Bin 39948 -> 0 bytes .../create-webshop/step-6/stripe-settings.png | Bin 47752 -> 0 bytes docs/guides/create-webshop.md | 16 ++++++++-------- ...dCore.Commerce.Development.Setup.recipe.json | 1 - 6 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 docs/assets/images/create-webshop/step-6/exactly-form.png create mode 100644 docs/assets/images/create-webshop/step-6/exactly-settings.png delete mode 100644 docs/assets/images/create-webshop/step-6/stripe-form.png delete mode 100644 docs/assets/images/create-webshop/step-6/stripe-settings.png diff --git a/docs/assets/images/create-webshop/step-6/exactly-form.png b/docs/assets/images/create-webshop/step-6/exactly-form.png new file mode 100644 index 0000000000000000000000000000000000000000..47897e2522ea350c174b13b81f5f555b10e7a3b8 GIT binary patch literal 91157 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 Rh(7=T002ovPDHLkV1jBWViEuV literal 0 HcmV?d00001 diff --git a/docs/assets/images/create-webshop/step-6/exactly-settings.png b/docs/assets/images/create-webshop/step-6/exactly-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..7c23251529e02fdf270ee69c701fb8d4936dd5b8 GIT binary patch literal 53951 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 literal 0 HcmV?d00001 diff --git a/docs/assets/images/create-webshop/step-6/stripe-form.png b/docs/assets/images/create-webshop/step-6/stripe-form.png deleted file mode 100644 index c767ca03280a693d992c4713dc49a0472d798397..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39948 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
  • Z{-N z@ZW2&ZhWE}@MQJp282MOa^d##k~h?6w7D8QJ^QEl-|sGkly$2&w);opR6>qAFymmK ztcCgepP7;!18Pm~I2gyczbi_OkFKpa;(C~BIPCQR>77t@XV^ei$>1l}cmHm2DC`1aCYGz>8t_IoK+fZ#cD*-R@cD6Z z$I+3>eXs?t?wM^wN3~mUL^|PkM6v)w{26PP{aR#GXpS~BJ?*oD22dHu5LlP8?8?$) zghWT3q1U<7qrLvFbG-4k+>3B5Exd- zdHJxEMzF{XFs8(m9{-;h1^9*Fg804N>S{qfMH)C0gPB$NU|mHJd*~Ai3cj z7;2~d>YB<*9G~BAU~4MKXLlh`$?OzZtXeskLM}_G;t@@sUn%f2Y(^r#Fs5xn_~RIG z2ZIpivKuTKZXQ$*I#_3)w*9hjbxkrl9~SQJIPSLub(NB$CqrzYYt=p4GibmUm4_3# zWM4KEx;S%Pa7RDKWRx}_RZwGr^aTKkx{4WPsi%DeIgPG0 z-PhuVfk2y4n?Hc5*3T4I@cFe0oTRIJgnRZpj zaNP6tZ21*PE_P9O@oV>j=lkjuv^#i7?zQ*tTw2T94ujr9;IyA86+6e_obFkZ;d?DN zme#mkX{*+~PXJ|XsEC8L0l$GCxSZ7ZWImn`or(BHnLZKMmh(bvc?~({FcmI>l*9pN z*8vg}RoIDBNN0@9D84AXP4wPUFC~Olt891UEUf=agU5v<@PcV~rlGG#4J7WDRqQzR z!$`F}dJkViSM%~XkJD@+Iu+gCDsR;r1wL;ueO0DhjTJaL&$3Z5$CIie;(V_w8=gL4 zKN?tOA-Ac11l#F}5W~OpUi#H{e04EStuy*5^8)a9!gI6Hs(5k&ncA}oUJ0A#)(5Wf z7pS*ub>rjt7?BRJGL+4x3fLPootiCL(++3zy9emJn>DfWl;p6;Fn{>#L%)j>$aIcH z{`C3Dc=cm!t`S#bT@7Yo#`?mM8#Bf|;lSYz{p^p3ggUA$k$u@I|9mEY`~h} zks(AhScVV(@+#4mg#yv1l!ZyStIHal#eDd7zEJ-&N5jGtrA8cT^LB7}3`9^|q_MmT zm@8^80|y1Y<33b&EU_~z?DtPq6XY2pV zLX>e{LM3#>tgDTETh}c6%*R*zJNErVT_+S9v4FJw-5yw)K2%oqk48@}V)*j3SUEi{ z-P{6Jvd*`~MJNoGhTxszR{b-dxS4k(sXTLVmr4K%TTw%%N_Z`IBX!q^K^sZphdC4p0u-g6sGOrSvPp1PIz{%Fx~{s>m2q*Jz%&u3--m)Y#*I%R0ZkuFW$Y!fCc1w zR|Yn&e3jb|_yZH|4Y9D{!==C~i=TIQMu8)|Kka|`rq-pNlG90Iqrf@?l=<#TdiKsB zW}eFfe=HD7Hu}zfPC7JseShphADf7@#_8nRVe9<%^AD@RabV*xrQmTsuzCFzvfu6B zZg`KNplN&53AO7NnhAOnh_jl?pXc?AOAZ;!5&*v3i_;VVGrbFE^8Z{n8|oUR47C}5mQ6=qRDyC zNBE9|#Kwb+v@6=lGgTi_%ev%^yF@{4EtV=gEbMpA_9{WYZX*}29W@-I;YJQmITfZ> zBK&(wupSO-s&xsO)~%9L*f(Fr8(U!dxs%b_s@knV+#`+N3mu>I(FcYx zdPDc;a4z^L-31#hQ@Cooa=dg$dcDd=yz1#HZ*iY@>-S_)==;r_LQCLYjFS54&ywy@*f0CEYx3@sNwKEgW;)z;ZU~J6P%7UV1&x

    r;_bJqf`;sUIY4nyz*$RjjJr}4I_&!7X#rJDa5e<$ZBm-^|{sXstQj# zkBy;bZ1@6PtjTmg<}p)|FFhsGX8x+U&<5*%K|b4~TS+TUZtatQ`R{ZD0<{}&1x9Q} z3H(&4zD?J4ldzot7bRF{I|=UD=Rk>5+p8^(`URKu2@~C^a_UYy2sZ(iau~?)bjBT2 zQdroj2~)MCJ-yz=Pum&Wb?umn_92gc`A34}xUAq#U=E5?qoR=67qYgRbch+3=Zb~u zsGfWe=6))(Kk8eV5CHQ_xk7aGi{+I322qopsF2c08qj^BVzGYb!LPYePAt+6?wLYqq>G-V2*zP!` zDHf{1i!M)5%Bc&zX#lY}B*+au39*`-*R{|^-WLfmGa6cUIrEG@$zbof@I9GvYwdP0cc+m&a=-Nmox>hb`}{ zJS8MzN)5dL*M4J)zvzq0NR8m~Kq*hjyu;e!2wx;5m){M)ey`~NK&N?!{03_8Umb|v zOWQ`&YvZ-wKySdq!dffazI%k{hAA;^ix0P}6ith}ZK;-2Xf0fK1gBa*r{(vU4zw5R zPag+>%~x#29^}&N$1X)PF!{q0tQwAt9>`6Q%!?VdDN@{<=cCcznUj6DJQ|gxi;h*4 z^e*onY}qXDdLCh0IsKT))gukg*;Lg|Uj$pGWMHa6&b)E4nfCTDz8f(18iHcBpDyu2 z6ot={Y$mBp3`MPrvHhCaUrl1W?$tuK+^YhDwqg&19k&P);X?j9!}pftSA^XrD%>^$ zUo&9_M8IpTl&8Y6@ed03_orlqK`8mUPYItx3QgKbzQpC`9+gFuorO>&72nCsY1|u) z;3pz7eo4@UypxgbA8TsgY6)M#8}OHBP*jz^TdmB`6}7TEJBL zwb=-#j$=L=EzO_r z^ectO!aYJvle=bf4_fR|RD?qz0Mz&F%ryFddpwYQ9uy5 zKJgPt3<ZW663{(RU=|_9V zp;T0Nn%vBP-X#>%`DQU$$y#}u{d38(9bGf-290heK0#gcPq^usvOYwR&@&AZ%cH}Q zV+|pcmXGTf-42?Iq<8dmTTuIb3R3K9^~V}vE6GB(o#P711nIF*Bx*>*?Pqf5gOfoq zZSX~05nwj&A(Qj*4r_PAHKys^OziZGaBpJuWGg3s*~_br7neKHSMPM=K%&`7wlp@B zBfu1gBfHsP&)y!yuCI2o;49M=>v<}=_8cQt5)nmla5@UkkrO5;Q|k{?IMhmqKh+3f z0tJrh4UbX0_|Z843aZaL#e?|9!^nOy4%hy6GuGMphCjvH!^oe+8ad-1v;X>%wHSCv zQ9>&GqoPXDk26u=qiNqVBErGgLOZv1<_qVr?7sb=_ldffs9MWsS1{bu&UWRx=930` zXrf4wTT0Zw-Tn8lHIO?g^5$LC0I_+cy}U3R$$B%(94 zQX^6O)uu+jMKYgFl&9y$nkx=mlpprs;yzTADck5)71ah4hh{X1cx&5lJ zi3y>U(cs*HriTbv>^Wa2nkAF{FUlMqh{Qhqf_8zmSYCa3XucH9u(@hpk2g?QmD5pp z5qF^kcHUcAPQBinC|KzoJWeYoPL^vLj5%$~Ao+c8 z;$Y6j>MDT;0RVo?i;mueB=lAD%q*zCufjoavV^Vy+*nWZkzY0&*f9Vf*PZ z7e-?UI}ew#YK1V|DQ%^f>X>_fi=BNu~Q|?I}nVq#$`V zuRkK3337lR7j=flk5nv@b`of4!F#zlj=b8(wX7TTOvGKWl|v5!rq9ZZ@h=kNczONw z{+dd+{cbA&Yre@WfSfpD98e@6kkl1+kj$ds7koV z%pKgBrBncopO!WltO!z5Cj#tataryK7E#|ONV(=qi^dZ>ktLbmcSXH7o1%`j?Kg($ z4(^qDdj34t>;#q4#L7vMt<)1L8Vw&KE>sirS1$1O^8s@8;g4tPY#Q1ciVD3pPj>wv z@q$aqJsD;Rx9g-k%7=^AK9he}K&dNN+~*^I8k9PD^RYk{)t$E6 zhC+J@;LZB9G%=I)Z^kw0Csx_MxwmA-E=`}{hu*g|CFL@TyNe%k9ow-HH18jP9*^u9 zn7*QGiaAJ|tWq|+$V2?E{>7h|&N7MO1q#K!7`}(Zpmlz?OS;#cSw3+Rw0QN3hQEf8 zqd*!>%c{uLz|F{E@8GiiG@Hom!-HP79UbKQWL6gFSga%C^^E=>>M?goJ(gG8-KH^M zfqu%#$A_t2_lK&%yw(o}ww#dGZ<#oezvDyH`UW8s47|ppEJ>agK0_=9vH8Mfh*fHU z``li1C#Zy7aC!Lqbj`zm&A?tBw4wRtH3Zx)PZ)(U0-LBf=vIyyj0M z);pS~D1#cljrqlKT<7D|S_UJ3uQ@GAloS90eoAJ55Rc$~hnL?Oyd zM}bz8H%1;Y0QBF1r_`7-l?O0<8Am0U|U{J%PSv}m9Qj`;W^%?=qdA(q>A}-&x0O$ zuRRambk{vUHQu5ltgLX~(e^7*!!JA9Kgzf2*4Cjn<9^@i4ote8-#V)3vvNPN&gok& z9;>Nt#j+u{%_#nN2~C~8sfgn5nputfTWy4$p{7ipRjf8(@-VZwotM zfqpZ!j*^q`N?Q4%8mE7bYqWCz5ANPGs;Tc?7ex{ECn6#O(p8!eA%Ya?iuB$)L=Z%J z?+{d^_ufIegeIL(rI!$qE`$!D_s~MiUGe|ld!KX1I3M;n=YBYMePRG>uG!~1-{*av zck6gNNAPk3SK;tyT0FK4TKe9&K3Xq3mNTtp7u9N%tywt7R)@^0aI*iJ>G-57Hmugx z%20xjsHAr{b1B+>b^1hoZG)DglH-+W+sd_wum+=3>(hEP)bY%9D|P*bSm;Y@)t<$j z!A8S;B<=6|dXGC^UOflv2zTiZ%k*9p)pSD~YFkIN`{<9LuiQKRas{NHQ{7e2PD{9sc%{b__zuZkUQV@a z+;=&D6o45@iQDzf&GAvi*(+{-QBmJ&PHWT|(5>$f*S>P1zGbwSoczox%vITzeTO(trBh zus}=uviiF+yH;JTiqqG;>D9%n_}xNEK=8=(aM_vkdcM1$2yes8p5;hT%}Bkh5071B z+PR7@(~dFXB2db z7JZuW8>+0m3!N=f-GGtqLB*~iG}a-Xt=JZ4*LZLXD|&pJ%fd)sgxE)+E>SU9TO>uq zOkK06UmI3-zrDD}JrjH5%;VO^GW*UCTzLWGix=|DJKT`L)$%U^2jeUwnq{gz`9lHT#fpH`u<;| zoz;b?_3Ubjym#R4?ziQ0Jl_N_4s*{Gv3GcRWd}sl?QwT@cJ{IWG@YNTTw=Y0m}8Q< zXIhg#tY%WXDoyX0_$ZH7J)GOWqkfdcu3qbuUnw;w#KH-5%q*?*f9iq!=TFrCjxqng zT`c6^IvtRA+4w(exrh$2);Ku zb7xuBemZ>o(VIXg0Wtr0R$e9#*A%+FV#YhlxWRXj?Xl)e0dS_o|hK@ zy&ytwYdTS{BXK(k#38NVM!#cJ{KIerra@Z}rSGd-#>}*xBHerwI#Xb5E(#|D%Kk+8 zF_33~o$&GMKHRU?AgllqmD)V9Z)N(FN&TjWw~^blxc6dks+KkM+&Oi>Gl;{e*6`p5 zTULUUF+AB<&iljQ>1PPam1dRlK9>MKu<|ztzOv!1jqjrvmUcaw<&s8n&b5$MPHL1d z8npa*(6*fh&0HgK-Hgq}>TbT25$jelGLYwKsY%qWJ{3m)kXx!n&R@<>m;?^+96Dz70{bNYWl}|anZW0j3H{; zu#7I{PtBXibDD^|^|f{wLHHN<7*`p5i@!+#K_3?TBF_CrTrj~cT)yuU%ZledC%^pnov;cf>eb^sN10eVSz0EzlH1YYWVkTR6HdsY8W8#L zDJnDsdTp7xjlp87mU@+KLb4liB{W*Kl6FH{^xB?ahkOKxieK60AKYpZ`Tj#P0*!34b_Rm^r-0o zjU`{roye%;B(f{S6X3h2RgQLC*wUjypRe6rx6TOxCp zJ>$(mT+N3^A)2m4u@Aiej)>r8*mh=sB;fA2SvCoQfOzRzb z7Osv$`}y8@&_nAqW@e*iUCf868_&w+=!-w@%#P2MUD!&T*C+q973ab&?FCS6=zUg1 zhN0ZIM7@V!A31l;33)qQ2HH;SdIdL1h)2n3p;+G+`lS7Wsv70_jFh9m)85NV^pDDC z4a4H7JnZwwI3Fm?{Ze3GYo2 z3B^yII@25ofndTLv88DzwFg%tu?aggFDEwEsK>3w`PpOs*>|CyEgh+L`ISH7jHz_~ zinpkKK<7$bj`qj~F-z|;lw7G>hgY@|kr^#3KoFR9tDN$Nq;%fxy+1!bss~+RE;W?b zANYCo#Im|=Q{fs`7eHXwr%d=hp8|*+WC)EJbjI2;_#Fq8#y;HlhcQxU=9co+W$z?g zxv^YG_%kv7gY^y+t@dE8^;u1v_oq?a9J?hLmL$JYzj_2s>!h{~$BePj_yfc+M6Hd! zaT#-i*Zi8%39(UXt#Rd1aC~p@!)sttJ0r^pm*_N!Cp%c<+z-NVkwiBs8_l&+kWA<0 z3t#{Dq0RTHmZ}^icN1r%x1E|cij>VH7Zbf(ys9@RC^)zExd6bjnI3TUELUQ}MhsT! zcKA2u6HvoIzi#e0nVfyUBDAoJOdR)~%e@Wss|N_b*yW7F)W@YTeH47OS_}SGK!?F6*}pU$UPq_**!sA@e>i*qBJJH|~p+;gP|2VG&Qgg#bBrwx^k{kKSuntnGU9@Tvs zJ(u<=GZ0PXRcKj_dv;QK)i$(*tOBSJfDfh)xo)QqD(>vzs2yOeawx1aRFQ)_4)yLP zZYzPT)P#18Fq;dF>gdbJr$F21O99hh`MI{JHwvK8n}X@5rd#AXjxP%h{V1Q+`9`l= zPwXf_VOg!q!p7$8{2U?`g9`E%bvAISD}9b2ZwTag_-J#vTB zo)g`j;g92TD2xlEGG5j14h~(OU$`8K!?j8!Z4DIQ$xkhYxWR^b9u-fe&-$Q3k~a-e zzRQ`Kc2iY4WJP%^t1B0~Y2P}}M;(z7yj#C(EmH|2O-GczeO>$h{ZNAP&7Qd9S)$>? z8>^zU_onv~K~|J3#V)u8g|XNbCnG8B>S8{QWoP@LL{rmbY z>~WAKx6KCN`9;g~^q)^e9odc9j1Ffkg!udFM;Pwx_aaT4WQsT{s@FX*F=>CeW_biD zfZaM)^8z7o@YMqpCLH(h_RlG&OoQSNSjM%{ppDBmFMF zWs*tfh`4RJObtm0Nr>7*#T|#;Us7f@UA^!eF!@ue07Ba1#yh0>>TCgfR5DvWW(7CdE$lBe(cglpQy&xaZr@#a_s%R^V;0h@?z|7My{;cD&VjF zg@c^5Q<`>K+qNf5kq0jPmfUA&DC{bi;I><`8nfp^ioKsk@@bVD&+s8~>UizWjL?Wb~Uv zNDd2V(5UXQrM3}YKFYsmLt?Ef%zczduSmXYH(k*8@XtT{Zau3t<}CsmYd1t3NMpko z^R!oon&Tkg>7Vt(1vB>`0>udejuMc!BmJe@v8B1U6==lFf0qtUcrI7=!^tDnqhdFc zz*Ym-o{w72^-FLSXnRZ!hPA>}bIby~FsvleD8MzvZE6@*AjKKxLNAz|pfnYg4S-*6 zmBkvgNk3FGHFxWBdz4#$S(gc({LW>QQ&X0AJySFk%C?YGK$NGFB=c5>Ov5NCay$wj z;#sR6I(XBafnId!7t^ZoD?bObLTUD!Q?hmp7u)5CL3F=&UtH_)83{2^iD^~WeC7WkY!K_=V>*R=+d?siX^ z#VVJ&ANeks>y1!Y1*vE*`CnJg zYT^_qdbl$&qRdrbsy#c->d5mU>;SEC!}vIYmUU&l6J3{7nVpJm_13~h&X8-qNAri;kO~E>598(< zk$PSoIf^NVRIKXV-TC52iYsW4{xVYYjh9!dP8*&?Dkg;A$J0!Vi(AMkjbF7ECwf+{ z%kT!Gnn-8vUQZbCqQCk8o&0*bO#Q{O#l7L@@%iuM!{wbLOwTwt)v!2SE0oIh1A-J& zm+2V=5*1h*)qCFnL080C>>4(FAEK8k#7tyh`q8{*T|3AJf!#%1_wYOnA0GTnctF}y znsG}-*!etUbdWiu!ZV;8X3&?g)qb8>ERtqlf8w7O6{~bxqgS4$^1U`(zY@9g%CVH! z-A2FevX89*>+mM%O0YbILT_Tzb!U!tctmN_@Qt04Lg+fQd3R3G>j(nRNiC|gK@_0T z{XHhyEj|Y)jq~L)V9TvyGXd5`peC=6fzbQM79r=m{V@xo!rEoV2!3Cj?$$aLae9wS5GVbS##3# zGwu>SqXvdJxux#0QKHETNFwfXmmEo^G(`59Dsr28*mfF_UnOQ|V2cXn7%zqlYmO}d zv1zR@lOXaD>$3ba_Vc+2iwEUwfN0FpWd=f4=`V(D_vcY1Rc=P|X=s_A#<;Xbe1*t6>Cx3gBqX zDFTw5W=Qw9n*x=)Vh#r_pcAp#bgr0dq?E`v8#}u_7Z>ckTXs2xf53_lT@^Hh(g#*5#S zr(Ls8(SC(>X&q{_=L)p_lWjW^g&XPe`8j`l(3LX^s36$S0RBM#f(@gI9C@@Agm0VI z`{+1nff35WMPW;ixBXAU{}SETTrxzOKX*T`LR*bKdrVbDm{HCa$)514Fw}9utFKTmzBNy89QN9vvJEo1jP*|%yQgeUK^ek;-vQWvwDQV z1c*aNtl*|Eo63%%1|XJq;}qtep<|{!_$?jz_nh~+hd<|;8{p2Hh_34OjkHLE3fsxw z1x`lD_5yTW^M=(i?DWQ+csr3-T4s--o8z1NGbg^AeSCarlpV(x_HGLz)Xh9bm(^UC zm)R~}Dyb=^U(7;cJQFzmc4=YU){3bcE}NNN(YhJOrLjhr=S)0w6{99?(JR!954_Iw zoRwHg|6YJjoA3eHa`OBPYa32Gg@SW6HmbSN*t7-aGfzd1KjVzTo-vqGg*n%vjE!RC zyERhWxifMguofM@?C`cl{tzbhUUBT`In~8zIj$dPYsB-b;YZ4uqH(Ka0ALE+peoFg zv%QE&{>C7*LQO_uEf+U=1U%znlT!^s%B`o;$EeD8pRv@Lvj7rgt&HNYY-NMm58_~M z3ad{v2D4CFh2H~KO1Ld~UX=ajGI!9Zho}g{Pxsf?s7aIeF*U?N77^2w1)J>UOF# zOF}BUX|V%;C$R6bA0+tnT(#0Ti@mR$1HXkmevf~LR4eDhgi%_HK%zDN|9Ap`F!Q~8 zE{AtHAAqIHvU>79_>Uv#-Gr`c>Q(KI0$TB3GowlB{1<=!RI;9#AO9Y~r$k_g|33e1 zQPBUs(#hZN{NL_l`TwffpXk;vj=O64(wJuh#gfwIf=*SNSGVIqjB~Y~Z!PRwpX(5q z)ht8qEtYO#SF;Z)-Jd`;#rGPCCgnG>Xci69&k*8L?ni^37=MWV*0Nm(vV02mpz^1(afH|8=NZ0@OgvVJC_oyB zh-@W`Sa^Ca?k({nX^RmTK&d`=@TDeWP<}0}qi(~m^f)H-cIR2U(15%P&G31VTBY35 zo%Z+0bTl>~j5NSWX-r)8RF9D0l=ttY1?*?wUOD&ubAb~dbr%M~Nq(MEx7DcoM4W~w8&ShA(dO~GhCHdaS6Dy&f*dd_tkt0@}6*PL~Wya#iNFwx^hoiI4HpuIN8Oz zdOz@-7O6EU^*FKpGK{zW;;<9`J|L2FlwDC;99Twwc{olFkmBzP+)#b4$5%`n&=r}D zI|9;H>8~hdnF z)@8cW8`a6_`)z+6-()@B0Z4WQP-N^~Qi2}1q3?$cazd;f3B8>JY za6O@TI0(6Mhe+P?23~c}79$s!Nm*-T%?ej2mRcvn`%wUG_o0@@q=Nx2q1D z`OY%>2P&+>biVh$8|TFOC=i%QI_J!d2wTWyy&Z`qId^^7FuKLY99S3&%!wjPpPONI z>$>yRBYvjqwQ+GNB2z?y4|iD!UL*dVyI6%pUKIg4&&kFQfd{~Sd5EqO=<>;^JxzI2+Vnb0d0XhctOI> zYO}Ehih#LVx|mIxQKNVLm*D;K#lcK5EnJrR2Bap-OC$zQ278*>!(d;qspTH6b~L>} zM{xT6;UE~Se`0oil|M0FI!yO1eq~jytne*O+p^2TgIuG^*1J$E+5h=tU1meSvc7iQ zOHC5?gqp1Kd+tyOj;dz)=8O84d)@9x)AQ~+FB87dH5yeVgi}a%H<)tX;XV^-m4o>b5clbIK3n2Kxza zy{}eKj^0-xiP@M*7kAVOu1grO>l#LV*BFY;v{YaUr3?S8+my2Yl`pVX*) zf=R9@dmBU~a+^l9+TT#77TDZKJhQ!S#GTs1Vm{i#sR#)4p8iXc1u*zxZPEk|9aBtF z?waz7w*Jr%k(x_;WenV=9%CMc3eYdE1BX4qp0b31;Du3LjVK-uIeiUGUO<g=w^zsShZ z8te)E!NI}F$;m$01N5&913RLHq<}|SIyEf~p+wL^aP8VPX=!QTQ+*ykuS3Wn_wN1kv443V;OpNazr5b-**RC5 z4=Kl>5o`k2PI-c}y!NZWzW?+p70D`KLr*2}&G-;`i=ej_--btvU<0551vtUId@}K) zMGIVMp@EN-kKiljZ|reBgal?P__KrnI&%H790v8NJQz$Jbc1()oUqsN@B0@8L_U49 zXCyy$I+Fr{ntL=}TUPzs*FabPFQn@qH@=U5MWDpqLVg-b0F>qkZ0LGGX@8&U$5eVe zpKQ3+tvSTRX6NQw9TwWvDSjq(pirpxb_pFF9oR~Lf&xb{FeW^XvOfG@zI=&d6n9>2 z^;T0GGUZcY=&;fPp(X{MX;%kVOMEbM5CPIZzPI zSqo)Z4h^i=DL3Y=Ndkq~ZKQ_t!8mkypSga0XJGUOJacK2%BC_OwfSGwQUa>=?p-uc zEt#lHIuRGcvb=nyw3Y3t+Gr5Pkc` zggd)tsu^Ech}Xi1bjsIInQHzm9FJ?lv%+%9dahwZGdns#ZX<0sSjq>xuSIm`ZAf2*Xw}N^T6g~EbG3z6%$Hx(= zQv%b~>nCAEEUiL>n@y8{3!>A)$9d&eB3eFyEtcxOrfI*6!<2q#BbCk3Bp`wr|6#khZv+@Yr7tHEwQ zEs$U5(nIh@?YJL*Sk00QuB$O3PU85e+OG(KaecHFtXFQk@N~(8`2;N+H^Te!B~8BA zr2@FX>>B5)lp7k<&q4rs#2!ON4D1SIxXU@*)gy6}?hN6R2KUWp+A}kv1%9nBgoIXF zag`n)xCnaf@QdwKaw@2M+naI*#9ZbR&e6jDfUUL@w}?G!%D^Q;%Y}8qz_3jA`W&tH zPeP7t-IDHQuzu^xaYTR)tLOSI&jhF_W9?v+sSA8A*0A$r^A@x*jZ5{w?fgjM{K`1~sC%{~%7lKkM*RV-Vvc=%j8BIDu&;w=M3 znQr%Eq8R!S54tXE^|+dq5~H-p#Pb@{87A|5e-w!&kcG$;x#-?mr>ou|6nmOc9>%;# zcs2WUQYagxEp%!yE_%)v*ZSK>n1 zG|3Vp@??xFeCp@jTv7X|kg(aSJSr8V$oMeJ2P(_j!78Z_17vwT)a@D3%<#)b^)IWi zPSJ1d24a?pMfz>en0cWqFsck0F&iLjYu0UTv& zg;so9{W{4bEgDCrU0o72Ir=-S@6C*V?(Q^?_GCvPnl%}n-!eV#1#4%YL;5q_-rY-)4!m#ghSnPBGl^o#vmwf+{qkO7&M8mF`C zGU8R8_CGwG+1x_+FhDl4+IY{*M)&3NN@gy127Tt1Qm!6%B@MKlM(hr1w7XY0f^fxI zyF@pJ$6Vh0>chumgO2}AVD zplVCWanNF+m*IDBD>@pdie~Xz*HjJSFipdz3;8G3QpvE10dBUjgz!$ceMk|b(tbOf z-tBBhj7GO4y6rdt-HECubCP*fVZ<@*8!sIjtG~2Y?_((if+L#YjhQMmG4UbUzXya6 zH@Qj5G~Qh~jwJ%NNTV|S3$9lSg~m_smKmsCoV3RUx4CKt@ODkN+N%~VZl1#|&F^wd z2IFXIKI#>FUIx}^X>^nr>?tMOVF^63Wtk17;%1y_PRMZelycF8GUdv7<7g&Ix~A`C zD5R#Sd>nT=nrO}txQFg1$QQA-6uVt8Vv z>O>>BSW{t=5y&frDB$yX)PPI9a6t=vl9MpU>rdb*P=V+GqCes^%l}k_mBJd zRwFYs+qW2&s#&f+J-7WiYWlev+Hn}g(ekD-{eq1%t1Kn8hW?P`iGH_@4M#6a4H2dg ze-Idbn}`Aiz_uk7r!-DV}o5(V&yEVlix7E+Y%^rSQAJ?jn4-UfYRygpyq zCNq89{Cg;Av{Du?LaX#$Jh&WIem)};6Z+$HMn=nsp&;TJhqhK2 zQMM!*VNp6L>0PQyU54+Kny=O_j<|e+jqB?}klF8VAH*uWuq!CjrOy>2 z9!oeE&&O9B%_nNU)lYp(rmOjO_mQDM zgqr4q^#+BT6d;OpZp@Ld06jT5V~?{`m~QeT+UCPMA9WjZPRl*4Rf`v!Wx(mvjGAEW z#h~1vPJ_NL9|o^qSHvtk`eGN4i>U?)2{UiqoU-;ygjB5@qcsjc-9CXHJpcK_tHz8_ zOY+EoH(1tFpDUj;0j)2TcLkQ4EH3CSrIBWKOLP>3inTWxE?U7}zpSVV7h*M_SqIp>%Bl%Y2$7T189Zez|tz9bn-`+4S1%}6h(Dv{il}u$EbYXB)AOvUJ z2*q4rL8fm~*Cu9_`KVt2^2~sp;WIRCFMfX@Ot0|Fdu4wc_c8r(5#3x%$6Td!s36g7 z*oI`8W2HhrZ+h;PgG}!7dk)3tr02MT~-14v;^Lf(Sb${8O>=54lfw zvpIret|!I#mK>nReAh1P$cnewB*kk`xUA%*E8V&1{F>D$Gp>3vnOv~z;*!SxBYi+eCDOfV}hk9OUM9K$fQgw?^m%?}B z4t--FAIHeZOFll&%~G?F#ZrB~gx$EvRK8+8gQOS`!Cnerv+tqRPXtxmp5|pzz5DEe zsjlUD0{xQpqkjH?qGite$R~f0CzuH=adfw!ll>Q~HtRgRjdYhbvobzB(k< zeMUz7Tk0emG16>u8hsDJC8>*bRzyrm9b*OebZQvI8K0IF+zz86Hmz~Lg$bwVEpT#-JfWmyU{w;halgrlRq z{R+!AykD&w+2Xbx-niSHQ5Y#a)8K^pyy3R=*-|XG^`6n4(69k}GncC&LMcs6;H0vC z2zES2W9WY+ZTIUqXdT6?xynDF)ru-MU+i-;oSlXqlA_ka+Hy|Al-#+*oFi2n z=%m!EksKpWQ&RF=QSrchWjxy#B;0uOU`D;7rgW-Fj~srTAPWp^Ly+%FsEJ~5I=2)? zvbWZj(K51oqy5rygo;kbT{~%o_)!8;uB&F1tT6`kLI8WekT;8W)ASo})~MOusO1J5 z{q|W8c%%TwoME}V$L7npy`}Zmbw@1J2aRdaeL~2d8&eQcq&_Q4MhTu(#H=gRs^uw> zYh|TU8FSORVFLq;&k&;BZ@k_b<1WVwGFX$# z_et#*J&9!Yxil&Crv;k8eR$-E!AL-SZr^?{v8BuDANF#i2hI?R{=E4z5X4hnv*`!6 zma(VYU=K;Eai)A(VBG`bTX9>PhBs`dA7^NvtQqBc^zADdgj-KZY_N`W3Zch}s`G(k zcQBocnIU2|;IL8QVP&a-{9b&4SIjWw(ON=y{Twt}Aoq&O$%0y-?sddVpOl8G@FYK0 zp{2(Pr{~So8S(m0Q2laD$v%cVQp}sC)JOTq^)zn!4ENLijoH%o8w!xHvCo8Fbsz26 zCa%p_0fJ5Q#CaQ)*^zf+k%;eowYFIWP8P0XKG(k7zsPNP8Rj9pO1ZY{<~ntVE&|8J zU)z6ZcXg;ld7SYwIaF#%PU}7OQ$hxCe3nau!Rz?6k+!_YOmRH>d9410$0{N~fPl&w zzSp&De=!9-my0`<7lpvIUrf2a`eyhtso-)aq>h|Q0@#x1Cwt^+)<%n)Cr1~gIt?l$ zZZ6J1Jn|jQS(K#a_u<*RXgn9kPMbu9Bt+N*DMcbFFIp+ zQ8PI*3co$^uIX`YqJuS(NvygtIEJ^?{tlNHtc0VFxcn1HfFXJL@Yl*_2h`|q1ZxY32+n`3li!;dIWP^9wB zzBFX}d(fB#Lf9Ye;%H?Z;<4zIrbJ3cx?4-zcl1)FPO*i_z9Mh|rexlrJZh@ieo$We zH~QVqsfK|)`zvqu058)XF$&XXQp5Yl-U3CXOi0~XH<*kg;0(Se( zv{7qmGD=9zml)krVBT{iKiUkbGu}p~yJ_Dei2|!J*Zrx7Lf0*sLWAqvjcK+x50exU5S<3p>>~{aqjzKo!Ck=gH);;fHkxc&EY*B77AW^VN%Gci z;P^8uT~Vx7MM8FVIipB4D1>31Ss{jIwCv9}?XE?s0fmR$eg(G&?BiE2a&G~5Zf%hM z#W8})8RdQ_By=E42C54fab~w74EQbv_p2Kbe13>fYQHk#$?=*!@A<{;owenxwr|XG zu&al?EhW~P4~Od?>hNq-d?581eV@e38lx7#$IfMP6@bAI-zFjPj!Khj@mW-QeuF5W zv%9;W^cF#uFn)ufTp#8j9Hx2h>~o$a*ZgvH;#&gP{cK_M?IU%?M^8q&tM5Mr6TLMHb#an<^%66B6+hmsKmIrfz3@!Gt zbi#Od*a0EULrR1E7{pf_&{}Ddc&#bTgh;A$=lr}MV}4qA?-=OpBJTG7@$kwcXh~c86dcVk7=dtsbh|&)p6K=nM0=KmG<;(L( zd)%DxQNcwz5P|;eVK$>vSn#)1vQOaKcx`)ydX?9C-MY-{wd$AF?oSjl@{UJGkUqB= z#VtEW(kvd@4^MMXd8of{+jUb{fTcjhg0(YwZwXrEOf<(YWL|hh-PL?q>#_Nd3})TO zVD#tCm#*L9B==^A_SKP(pNs7E9rd(&*+Tg)WJ46ML>iR(SNQlwUjx0@$ zuD0^lomiYZeXUJQvWts1kj(uFn@Ew)NepNa@Yu8@MpQc~Re&WUg_*^?q0npG-1gMH zScXt4Fh>$kd>h{R9W9X^t$-Ic_Oa~5@WSKA#AcbSdHAosUE?0~HwJD^X)8)(`BPWh z6aJkCG;DsBzH*0r7Vx|$NJ;63xZ100lU5QbQBMqAZCR0)GM@&sB9+HiAA5N*T3@gd zm@xt_?N8vakdTn9tSt4S_)WbxKD_(fN=kDB6~i^wI>7hzYi-z*ZkWvLSe|%d9gJYC zX<%03lt(Mnnz&Mpx@DX00Nd48gyH;8i^nR9yt8_}KOaYU)&p7e@cCdS>FI}J&=2oO z>FVkl8m1fK-ZeBdu(GlO$zeZ!TvHLhbirNu!ZW2fv|zl@3y*zxo|UZT#l@1C9F=lE z2Vdw)%3Q7LmB_(s0<}Laz9zV>I8X@Ph09gMeW&&HbqY>c+=&taSv2S%Mp<=MBrsn= zw@--%IKORhJEG=~iof7}pikDH%Imp0nE-C!Z=Z@Hl?$%E4gggX3|ra5=M9~tZ#5>I zze~psAPD4mv@AgIKV0_VKu81r`QOv}H*<$Op<@-kynFZVJ~=s)4uKDKr%LnbWD=;a zm0S`4B|hUpiKV4$1eJn7eFQ_YvrTk$lMS!!ncv4l7mzZuU;v-E0U!$IS9tscP^Etz z83Q0Vgf*W&5nLVoa76R$#iiG}};EvW>pr^#RF6lr$% z^z?LhhqeYaLofC9@60>1WPZfsHV_B|z#wE~WQ5k`eK)!Q*ux)yJ$O6;l))|c-zWp! z6MP{@`e!T#%r$JB*Y^a;REqOng7zg9geD%~l=PV?VwrtO_bB$MH5ke09z++B1`RNf z5=7779si!OEhVF^YQ@lwL>PgX{qVZ>u%5Qp{|lA@;b9pDB)B+_XYSoki|5FylEYv1 z04DMP7@iSf0H=yB#TOPcd_h9{bCnbG_x&gM9>m~b9sGoL;%KS-1lInbTnD_DgCEuq zAn&LokP0dG4-k5{ufql1yxzi_f}6L1DlLCZ@ymVL%Id^5Yfd0Y132lIZG1wmW#yL$ zbuEGxtJ)-QTDB|IGaOS6TmE*~0h~Ik5^^_;T zkO=6YXii|ze_DWlos5C(aqsf~hhG+c3nxwjqde^Di86j!HWeci;2fldDD1R&&O~! z8lZQVnE`4lSYuYYNz3y0oDvyl9nABUT`Z-Xz^$IbgD<3%K<3uWZYH)F8`^3HVebs@ z*Znq6m9CLysLXh=-;VLF+dbFp2>y5EYI8TIj0pMHpE5nEqjsWJhtX#dfz8o6vQNC0 z!hwvCh7LVo2|HN@kDqbumyIXX+&cq+sJ>jricRv%2B)#92m?2I-XAL4B1>59y9X3I{2rQVQ&M?WHQJYc#^*~Heg5H3eSl=Uzkij@L?WW{rKo!$l@fx^ zO#22W#RU>*qe7qogaW@Z@&GzntFLwjHA#)Y0R^6D=<(eNU z{LVgnTGLFGk6v23oV(fHj7)DiISC*F2A}_3*J#(^T%y>cM+P|0o%S&WD(FRR>GYu^ zcIRw{+xUKe?Z!)(uP8_X5ldlJdN!w0izPv$`$UCWeGfJW7thw!y4mSIL~dOmpYDFw zg|iP;A#Po{X}h*m& z^be4|s*HJfYy`EDVFbHP}4ruK31q!+C7| zz`6BPUXZ%CuMZ;DjsBJ({8leXpQM*}Ul6qUc*j7T_Y9>OW zn1{P;mf1bYY%)FSyyv8mqhLXoI~uT&U^|KwoceS^1QlEWaVwP&`X}1}tkeOl{@^McZ0NVfR7w^mlEoCdPE?vj{65 z8enlYx7ZoIZZ^)mU}=EP(w$FG^S6&`WdGA7vrnq6nlZ1&UJX z9HGZ&83vFNNh9)Q>cBr9wAXT`9-u`A1THc<32#LYnMkv%;&{FTIdHUd&BE-e}g#6UI zhTPcA3%GMy@^=Z!eWDpBU*_8jn=4#Lp<1aAZFzkq+#N0R&oYGh*oztpVml(=9WnEv zRI)_ePEQl5A;r51>R~1tLA+(p65jqxMo7Dq@}7+SM`KbH((!z%nQMk^(9+zNOQ5;Y z%5_s7we56J&0AI8dXC)O`MmKr&UGChX`lf}qNx}N$`4Q&h) zboO3h5e;W=Hdm24_P897%9k!m@w!HE2PBNBqv-<$C|3cQ(7dJDHdI_YD()Dm$@iv4`rg9H4aN7|~I>7N~Als8P|wZfWD%!_it z^y1a$yUhZk5-@?aAC`j)glQJKYxx&keB99U7taJHP{SAX=JN5Xxbjdn%I#N9M~Aig z0TKS9A{V0(+;3)e%7o^iTkMG--!TawD9`u>Ts|k%>T~ezfs+8; z!mlD6=cJ?Y{U$cjvsqB>>s%pii^z;uUR#~WAma`6kN`Mc)N89GO6a7Iu7ga09-1jy zBEbzZ*Zc<2{ESBN*VXWoQ!V$|r#}6g{W|(DS@*+;>7R{0qtTIVAPBl~&6p+*p<6oZ zJAX0BL<{8U(-f7KYf@TcLDn5GJf~AOJoZ>#aY9ee+JaIio~0SfBn)8o;}(;uwKjQI zE+#d4-0hX8PV>VGwJM^?6Az`XjxeF(bU1PaFF3M=LGThtqFLWM^4m_yiCb{-Tk|;=N{^{Kgo`GrSU+KX&>}I^Pna-f17?IUFx?kyn zetUa9r^pj3A=~cf`!&!(YD@KJKnkU{YHIuR!87o&lGVFyUY|wKIVpW)ql1vsdBXx} z9WQOZZQk^h=)0mRdc)N?)CaObP`RuClAYFmD93neFK?@Zn#GPsZmHE_2 z0$3ql(pec=`32<~YpGh$18vEKKlkYQb&cO>TuiReQ*2r&cDJig{W_Dy=4cRF!)N7{ z)elJE1M-HWznI4CfB0gYLN#&ul&%|da~gK(*aOV228hcT>VR0pT9ekh?o1lZHnH=4 zuwTaBMWjDt60_=e4y(*>WJZJS2tfalYJ)V_d`k067vUL=-epc2HDRlh_f{cu$uTiLee+@k_H6r- z+e0(ETyxK}Sd-;R{z-C1uc|_APA2+r96Q6_YaON2fLxPBfGr1)B|{e6!Ku|PA&VJ5 zDu*tXZ%K(GCT+A+2^>)8$GOo|iM!fD5~%N;FX-a;4NCorz z^#L8RFd>kA12eqbXfC06ZexWrA)CUSjsA|8=?b5}`Rs>71!5`{T$njH?~Z3uZqU6H zZTL=@cYfXXA9ly@?ROi#yWB821X*i~ly5~Fq3=+*Q{0Zn#d(JXjdA^Hp3=`Tt;NvhQyRxn=;acie}7lKMXn;VP-T& ziX@jYGzQxx#*WAsmvJ|k{f@ocpU#)_<^0a`;r+7S-}63ey=(pd>si0`JWns1dq}a3 zx9{gLc+H!Wfgh-uj$MbVs?f0I*@d%3JHx9&kggev%)GGT5ap$RZZzWqB0sP)O`;Zc zlT!E(q3X#$znEgQxWfin96OxFD`VL64ps(;C=t%^Vr0Uo{`U97!g# zUYtupZFVpz%GnL%@>G|!<{Es930~RPukh=h!DtgKX8QJ}g9|r~?|&42vs&qQw%fWT zcXYVW_UWOx66T{Y-JJShARxN3l+yi6rR9XJt2b{7`@wX4Vhk@!`@KM?r7!OeQ0T|-*M09DcDdKhil`~#cu-JJrQ@GxOI$>w?O(+$UwjXQDf!!{ zmjrsfEqZxfP*7gZyt`^41vIpDD}U4$xN9GkaQ3f~$9m$*Kw?2ke9t5z`~8T(38>n; z-In(QfVkVT2XLpi=ia+F4maZY69#K$Ij^RBgufy#B%Az zHt12)g%2FWI>Y4hsRa7Fr)qcNI@OX^#&ix-^9ZvK=x^W=x56rW{hMlZj&*C3KC*tR zg?SJO`cZ=Hx3tB*#frIZrrsj7FXf>}y-)8>Il(KM(*2E#mx?u%itb~0WR89?cQ5{%$m>j>jHXU96Qdt>^TJbXpfm(MG|*Nb&1i7T z$wCoXRDAq07{+(6G?iFJH#MgXa+<_vs>L3=}!zGP#a?_eMUriFa2mntd=m$dWR>_K)1) zlWMWnL>2eXwHjHm5_%XYz?`v+@lCZYo22)H(+!O78MI4}N54ZP1x%Ia`R5}c2K z7=YC*XZUrJ`91^?RER^Zpr*d<`!*5h?C2QuJcVZAihcD=)eI|I>=1zAq@pJCu}_tJ zb-vtANm82)b*Py(+uJnoZ)7eBQa^s4hZCq0A;d7vk2B^uL|yr-CaQMxs(@i;e;UR% zj+}mdyX8Ujn0=%-K1P|QIYJeD?AU5=UdKd<9%eP(uB9%f`JoWk{T@lH5QIrR_t37$a@EUxLOt4Gs#-9k&<>#ZG(I)0-wdfLoLJ0vFD zr?FFVd>jQYs1z(uQf_nV3`1Dt5R1LHW&@!ah$GYMXFY?=8Al09Z@;ZIJYGufd$CpF z2wsqFx|f!=67A!+ZW4(y1B^lzZx7P7;}g6Sy%+AT_R^IrPkepGC7fEL51yV}>Nj*@ z-DrXM>NVvEm+R7T{xO*mE9mRct)K*ZHwWFXHRR6JD<`)Y{`ZjhbP~7%<-=!JY;G!A z42{6`%nD?JtU|&GWc)8etW2_oS`b=fNX$>d9#z@TOz_h0(`4ZF6^I?U$+ToV8X&p> zYcT``xU_yfw*Vtp_Q!Qltly?5&SR!@E1u|DDKTb91%?Sd26yAHhaghSWD^*H3(bxP zpEEXBcl&-db1l0QV{fzalcl_If|qS8yOnys181~$rMh)@(@cjTOAlMiRElj#K7Ca43b_fjukMR{~JM&VJ0nx%Zb}K9&d8 zllnHiMuhNZmV~!X6*yh#%OZw*e@ca4N+-&O6`wHBif>QR;uF-I$32<$(BpcZu|~u; z@@JbXO7yA6B+r5kN~WaWd@RPjd=%o)qhq3~6fvMuv*i?^US$|wKNpTZZX~>A|fJFR(hdB zM08_{i0Eq6KUWE#C}xId5dK|p(@}g*RMLBIh4ALOwVb*f5m9LbDem&;s~-f*GuD4pXhxp9|9Z=_5GXlwp~DC0)XqY(B755CC_u|L8(N{ZC#iX>d; z_<-L^XqQ=<^4v_3h8#2{9~uXgsvD6I)3UAMZtYIvW1`{L?7-JO^# z7gv9;eiXj^()5|u&5Ns_r2ilEkj!XElkZ}42*8lFW zrVnRPa)mswdFy3)Vfa`#MDQhO*D@<-97q4<1Sx$RlGlq|nK(mQL) zItuGaUrm_)c8{-oU0d8{R}tTvgUj7<)^OwHviQ3n+_(|- z2G*y`14Ok0kdOV&EIbr)>VD9oVo^E7JG?up&+FQhWL1ng9+v{OC$>s5B`=@DTDtn9 zFh6l?_41?!FP(4MR>Zy`c-)UOf||!A!Lc;z*B_?Ask~aJ6$-`H%Z+~Trn%vUJgWT1 zM#MVkV0CSdd0Y`aVQONs!Ssi9Sn_x{xC{}FGX!G1H5fvF9tG`~Y3k|gy);H)aT&}p zxyV9v+SJc%#KrWYlI5)L9!W?n6y&9H3GhS}J;=zQkUer%R^D#l#OJ`ArTF|$QjU(U zYinD*VX3oSrPeN}{{H|C6L3$>GJnCzDU}8>oGoY>x_Ws~?smMuHU2c)_6WD}Vq*-yr z@o_TX#n;}8wY}$H=a9-ZL-^xO&Ps7+s&bChp=%EwiSBhm`h0gcXuFnRt*R};(j+8g zv<5-J!H_i=JJK+J;iGXZsrk08uBukspS7XP2zr&>_AkY{3g#bYbU#CZX?jaB+q)#| z)C_(`cIzpANeV%>@A+?^HED|eXJK~-M~Ye2E%!UKmw zMyBJS?u}}^d_}58FUHpPStY^p?cu-TvUiHF!d_`tERGgNMQ560m6SW&dp1A0Qm*jqV-A1Dno7#r>Vba zdp(>h>$x$(t|FC?K=zcS-IQOB0kIoU@w+pt%1Nb=Ptx$moTBysi4805ger-%XoSN9 ztbsyoVmr6?_mHerV?@*fYc9vj+TmKaC|ZHi>$;YjPcyog_^Klq+;aRs`<)06FGd*~ z{2dGxpzZu@csK*AyjUaAD5mE`-=)+$84CGDDn9MLD>iCeq#h~_uLnMX-GNlJe;u1t=UcC1^8b8>gfwqdQ+=JU*qOAmX{ugR-NEA74IF4jVqh{3P}3hKEw-ab?2i(ywS^_YObh|jYBGx$%#5hVjyT#k zIxmXUwNeA#;=cwHr$baK>FK!w9jR$J?MRRY5^C9nCB;&8j~XrY!}F8GoZOmDlfLua z9%;5VwjXUu;W8W&{%+X|`c`X@UD^#|+xq%Jsf)lfx7q9|hucxbY~zTh!(p8nP(z41M4Dt-qg`m3rv#V0p$5yF*b?_GW)ImV|@^Y-p-(YRD`8&q+}CTs0bE zzfGxa`;U;25ZlYJhLv0+KGt*Z(dsxfC+zsr((SwMB}JhOtVzRv4Q!zFUOi>v6E^ID<_3BylHyn<^A<#B{byp%Ke$ii?|= zcu&K;hP9x(X%ntFo{npGFqNY+EQTAuK>@qd|KRujt886P6;fIJY`kLA(PD;}WS^se zMxQKD3mj=LCga>GYsG{4`RO0nyB)7mx6uk)KdT*uQCV_avZ>vKB&^USJ36}S(N@B_MZ|FYV*#Y{GsbiIGVe+s_I z;G3<>&Z)`5&C@64U-$!u`>=g?)O`R{)WMJ2=p_4s@!v?JfAh~?ywQ&7G`Qg~sHy@K zw=FE(MbFUa)E!_hdi>z=K`24Ume2tPu?vUu-v0D>-^VqTp6K;_#`vMrjIq!2Fw%+L z69y)35!VGSFLMFS9v}1lRbb{|I>9jLeQ(298K(Ha$~R9e{6@AT4(v7<)1KjXi8+(K zY^6`=_<{O*v6c&GwX(c+cA5Z>L3G3DP>9!}!EaGuD<>4P5L)O;Ea-9RkiW+D^b?03 zD=JsWM9>QjwXwi3{9 zE)*v()W_9Np^2W4YAgdd{IwAWmP#cu$S)Q+DY*Ywwt;Ko5a%`I_c5swV)+d&W&Oi1 zi1=cIME${hSv55VKFS9H<*WpIu>IEYIZ2yxoZuBiPJW%dXw~h_R6Ss{J7Z+3L{Qc* z*PCgkXw4*A+F+Q(3IYj^z6AkPS_cReus>wm6}#>k#5JDevwn?zyBH;AQW2zvGRr$N zNXsd?Ol1?JrW&T4ny{Ee`!)wq=Zy>7+W-4!{bbQ3IK*v7YIoRMOUBB|DkMtPu>|vB zLJ`^V+@~_Ut{@&HFts;Sir<8l6q2tvh9!?pa~d?DCra&B`g#H*pbh#`K##M2@>zY) z!9-Holi8MufPdZLpL+|Fa}B!+1GOPjnQtO9dcyb@TnUb)=|N8y`9j)L9fOJuuKOOze3~{hu%X{(iq>{vB=5zTdIdkX;L5^LW=0+|EkEo z{-#$b>Pc~jV6VTw+hUrEEcL~9{FLmPP7muAtblZfsq(obtVYLvMl+x7t~JwOTT$U( z8jmV(+YMAtV^0P5NOVl!^GA<9E&Ed+@{tmD4!MYkoO=u9wO(SV{;*1B7%9O=G6jkX z&(^8|1fP@A&0ow@kY7@GK;k&UC;aHK?884FW?n)h0+d)v4{OV-H4Bw>=vy0|0n~lA zyz})ygISOpcQy(@$QXAL{R@AXDFTEU10pgE)k1%^bPx=DmxAJe!di z{y8TB3q3vH%(A#x1kpN*m+LYWAnF@dYazW{n#h;`V*qyl-{Jn;_#%6H9QyEbnqF%8 zlcVdf^fP@B|Ji79A+oAfvPnfn=>zL{O8IN6YbpxA(cY( zF`6(7r}w#BToTKD0F9u_y~|6BkRz+8(KJPrBoTdO^%6Uu_|I*T7EaUbMRv%ylo!vb zdKqDvnb1>iRlSQ<**JSbL3LK68gb=>}1?Z64nYYUY_kxt*Bn!*KU`6+rq$^d>(V* zEt0jXR#aR51;V{V#BRv^@}@h9mj)s6HCba%)%Y@2{n^_l3k|xXAU*MoAtAq>zn9)` z8r{a>wWedB_f~2<(B=kXY5JdU0FCc2F2LaXp+2Bi+w+z;dYPTI5*B+YP(+T0v6{PH zk!<5T>f|G~6QvS^o1&m!4>F+N|Ap4Q*8`4S5}OvZa5pw6-P6+3iS8B-)Mo3uIyWy{F}a_UyfQrUkud#{E5l>a+}c`%bw{7dL^~p5#1LY@`%AA zYtGza$a6i>eHtyWf-1Rll@-2q@_0y zx!y+4d4jE<`nb8sd8Vqt^H2-+fVkL4c1f3|Z}F z9-n``;^Gy=+ck?P08U-G)|@e#$feMIf}{4W3<`_O9p+z|o6n`PZ<^{CF1GR>!wT~G z4!O^1dT9Q%jhupYZ45caY}F;RK{5+AV}kt?bT2l*`6m7CiM)fooDZi0wHTzf>ny;j zvDzB#57X`=Ec7tU_6b#cL*sf@1#|uHW-#q{ji6kqRY}*mo&doSWGfbMwziV<8GPdh z2inJL+S4sr2vooJ!k)P@Bt$AXmYwH9`I7xB^8|0)xM}x!u*?;fPG=of717brW+tZM zxF`CMN^A+s3Ey})4Ly{vE=Z$8NW6o1Y3}HTO%wMeNEzs@=F3jbsqyghLuXM6#Er44R5wta33Ku2Cyb^*aftcl{OW)#rl`CG#&WV5_e_dUvR)C@kX1dbsxL2xyAF z2d#xkY?Rju3L5RVMvV5<)xJfSBtPtIbQrjPbaIk&pe74f$Uign&~;Ygx+JB3CM>)@ z9_)B08O>^kK9+K}t`QD?w4dokMXc8;XzyEYrIA zYzudGhgMpr)+o@Ew*tnI?-0Qim||Hg+#RW=xF ztQ_|G=TG#Lo{S=ANYBj0+k|LdNULzZglAbx%35tv^%c_d#8~B{NX|5eF^J_YH_yJh zlY)l)RB&n}t~Muh&+uL9BO1iPo{`SrB6wabC5Ntiw@V^Ax=G&%`qs7L(PB^I$>Q>d zQJW;hF)rn0X`IE#Wajon`BosL51%I9*3lwpa5VU1b3+D8gUa3dww8L@XOuKFSPeIM znCN~_U>e_PncEx!gk2OdN5q@X^D@AI_0a zpmqdZxOVN@BIoSjpsNRs5nL9ZjWdH-L8xFiY!<)@3aq_IYMR6BsqLwgL(?JS;d{Iy z%r#YO{dHeLXRyM4T8ctT1KR;+Ud+A$pphEW3lD9>Gj%NP&&tXg@asMgk|=?-`mL%L zZRC~SyU_w$IiVeT_$K@s@tdjP91kXowI?#|#(=efJb=mJbm?!M(gp!vKRbWN17pDp z?G>e|B(@4q(&)g8(cgxBb!=2G@GuT*C@cOG@qFNV^4H(Jq$o#^*p-0{@rHVHlMi)V zUZY-Jw1T*$;k6TE&m2xepm`eO07$;V{sq&yDAGF%Wc;|F7B4m^ZVe%cQG4v8;P$qg zH+io86W4&>mc+T5iwd!13pVbedVgU=A1X^#IxMb<<4?Kyx#rcp%?}x>r)6`^;Q2wj zxR@(~Kn2cQ{z~X0obGOu440FXrPG1YU2_v7tK9YI*Dg!JiTv!Eg-tKzlTL7Qu|BW>xZTgyO~UsIStTz zF2@V)F`2LG^P=MReq?s^q5@(aEeq{E&Xa`>4mN)6a!m9_>)Q!on(|m{&0pE6jX~tD z^entX6-2ixK;LX)F$ciwt60$N?I)Dby*vWhzbPTea(QI`w zZf_&0WSNy_>tP3rm2h?^?;7O1s?Rd#Mt*A`_;~y4PwALc$V1YO8*ZBCwFx zktubauC%W%GP;hbMBO*76CQAZclt?-)gVF`1C^NIr|^vrR(~3bm`3Z&R!=Eltaa8H?W9Zg%p))}BnHEFH;g_k7ICmBAf-Z_&~Qm|Xp0BJ;52SB$8C zyon?`FeVzmyVpp5!Z27EYhpi1XNJ{Z>KK}KIM*TxX;f;{^<>hgN@h`HX+m2$C!Cdk z-R?~BNp4RvX#)1TeV9<-Nh>B<=5O=`aqPODZO~Mo&BevFcg9aP2l7~yZ3SJ^4@yq@ zesF7ce70ipq)?*HQVVCDiV<~=0*y$RB{n>mzvg#5_;bzrF!_lSMEy8Q&ji@81B+kk zhOlGG)>8ejrQxvWEmsQ`dIH-3I|r!@g80K(7r*Y;Nvr5XC;vB_<*d+Xual|3Gg(`A zgZF6$nc0HsDUXZn`nH@D6SJNMR{7%oHsm436DH@&9X{@>JWt9Om{w*!3m%h|AHYr|~v%H_XPAH($38_(>Vv985m) znEdpM3UKx&{?TI7)1|bCrW32r2Id(J3F_1|d1ao+lQ^$46xAWxQ}XUbw&5I{ep^cr zeYUm#+OH0MysWws=nnu|(Ig8UT_;a_*UsELKd%$b#~|z9V@~^aEIKI~<~Jf(GhLPs zS;q1!Y>RSF?oTsn$7I6{!wV0svi*|TLqZ~0eWdG;vh)A>45dX~_mtYJ zJvg~m4}jvouJm$_Tu8=%P=8|lS*Wn9NU=QRv%aSez=7B&sBfmlS>#QLJ{ABEo7mA( zQu2k&47g1YnGhq%w*Xurzs(joc@?ykjH=8-{g2e1*}Q=Jv$6QR-k#wv$}@u$T!w3j z=UO*y9@k3in*sKs^5URbX=l|yJtl1_CjMlP_Gmoz$VR(1pA?zgP9anMs_n1$z_~sW z{gBtX`M2LF4VL1ztE4@EMbyt6%Ts?wYvQ<7qTI`l;)}kz2!#Wto2cy}?FG?!?#;e9 zHgo_)|AL;EU;qi32IC6y{5{zDy>R)e{I@+-#PNt2Idc+(K~dx3paXBH(aB zwQJv45LPtp1jo;3vlq^dX7!OBr)bvAul_>}9~z0<}&eM{wVy@ zR_}Ygb?*jTMQ4JPwYXX>Xl{ZQCNi?GXG9Q4+Q+4tvx*-WW*vX!(-L3n)&oB#(3FUaQhLB^VOjo%;E0BItGjT4 znY<=7JM0{`aRU5h6j$jne@pwK-ZA!o>za4|;PH(y&%%h)IREw*?vr6;dxZMQXeNsN zSkfr@xL#ho;|IsqB5f(lRy&r_GTP7`f`PKa42k<>y- z$hlg)XTC^j{z-2BPo5$%=r+-?=*B@D0v=EmX_q1?p2qp4;;KV}b;q2|^aV(D=iRcqhb49ZN7Wrcp@dc*Y;ETO{khR(1rMV2pCN zTZ?D4bz3~2n&|1vW6JdVew};}`8HzaW`l^Zk{EmFimq;RKeGgP; z%GylQix(Hzknt)Z`G41rz+z6!!te2-8YfmPA+~3cX5z)ZUdS<3MRmwyebI|0uYTTR zezFxn%@f(>WHoo-S7Y`}XJVl07L+r}I za!OVa#D8x8yIP6n@5^-RrdnFh)@tarFI;YV@O`cT4!Hf21_fHWTyx7(`+^rq=e7xx zODGjbpH75ouE=^ZwNiR9L+&1*FMXn}#Kz2B?lT=Y zwB@UH!Uy(RAD~NPOuEM(SNnkTv}?snwv>PjhFw6s1c1C5D<0H!@lc6z9N(HM92V0u>5suy-i;mj*FZ%qp6iei@sY53)CxM?0G*N zZ)p>6`YY3f)_u2F2wr3a>;0BH{Zvmhi@T@<nVnoieb_=4@>O$u#Qt_=Q5HR+mlY$B$PxQ-xq2#@&-$vW07p87OdBc;(+0ae#=T zBO-c7KF8QuoJ-{0(fvNz+1LE70;_UgVM?_SWJ}|NNV-08#CKau9p2iU$JU2aSYCL_ z{f$e^NS)mv3MSe`CHa4#;{)b~<@EIiX>NMj-2AJ%C%Nszbc zWq(LmBpp1-nZvPK7f32jC?>tjB@8K}vVShW**O{YjDg6yJ@#bwUf&ll+a6!!CGXoS z0+9qYIoAY54rRFDv5vq}_7;D5gZJFBaQLnKqWilsnqM$nDgdBB(IR*Eq?gtN_8DI@ZX; zWU2k!lMZA#${Ut2PIzVKY^q!2O38hGqk{gRmc@cL_2>Q8wBI zfs1J+#`t}&%sH91g^Zy(Ww&8D@%N@CX2B^~?*y(3gVXXNiMTl;tn=n{6wQx!w=!w3 zUGAKG5)~BoNI|py_0QgE9|f`t%lPSI$G|;5!B*}VA<|~Ih%R?a^Y;7za#VZnnB-?I zzPr~%(EU!ST9P~eG5csr7P4X^Eeq|l@A)GL5G70k9SkDblRu;)~Dg2pI(7Gu>g_Kq1Go==Q^0@lPAfbG^U9F*WXA{*X z#V$|iS$y5(UZH0*-}D&GQ?Ej%bkX5}wWeG*h22@Sw+C0qM&GYo022KvCmtBWNEotL z`@aNm856E9{QzkX1eOacw2~!6#0Tpzi?N=4h1Tfhp3u3Xdv~wtmcW&KdIT3W0w#1~ zcR?!?hIZOdo*rRx!1cl{Z7TrS8YmjcGgwmi6bgDaNk4szyagoYjPt7pNzWAom>si+ z$7atjhEnDRPm}0yjrr+9>}T{cf09Rf_+@CvSJ}Z--5jB_wosG;5|cN4 zh5O1*)tlJW4=-Bn^o2`)&HSG1-lO?QY$_CXPu4=tB1Gt&`Xij2Lp*4P^BT(0o z$;D1=bi|jhOXKaB6hd@$Fz#Ykof+fiFpAsZ=NoOcT1~2 z?7%F!ZP8Hrq|qiuP{01{)_x4x>Zq6S#_k(T^dLG%Kx5?hxK#ySW5b_3f?;CDX(;VY zol(cenpW-%y*$6Zj+f0OYsyO~nFpOpJr)u3I}q78_+;zpyKW(K3K(229kn%O&z%u;$#tY+jK zT&a^ZXFfmYBx*nzSt6jw}K{&`q4ZF zyJM^a#rB0&nvepgIaRP}QfM@af#sWPw5LzknqFM29gG~ITc6h4rE#p=jPiK=G7-^a z{;)&5IJg7$L&R1Gw+|jn}r={3!8CN)RW~= zuQ<(ToyvqSW+~9E2?U-Zxs{Mie|^6AOS5|Cv#?)1jA zX$#+Sa*IOE;_BP9NA??!z0MqJcSbp3X}7yHm?=vsup~XzIeYO~v)X+Vof!O4yu`>s zG{PSa-E&P_(~Z__Z?M@YNM_|PjVjX=5F-Y~d<%K-xQ()XJ6%7otXkztT!pCqK1XUq=2u%3$ysQ!-lSP5k|hI{1371^mDZrnZ;se+HWRfAwt;zc_xE@?`; zKd*}<`TQBKp5HH$Y;d%Le%$vN0gQD?7b78PUs|#F;eC+Q>W7aLg^RPY`~gK8xHlzV zn673c-|mVa<^@aQ$~}*KT;vn1(x5QEGH}RW{Q&A-4Nzb8?)?Bg(7xga@TBQM=VJ)K z<5l^RuFQOBn*&5UKL4*pa)5|?ADND3x8R#93eLtmMOV48OL$y|CbdwdAhbE5909v98Crd_G_f4Gg1% zMOpR=UD5A$-Libi$YKNIK(WL{K=vRlDzSW#B739LKwp7hO({OLO@$+N2kEjT#K(EX zVotzU%K8H?&J5O%K_cxUP2!6rW{pEDF>v^2!XDLa0BUN~wv8Y|6N z)FiL2yb%evR)00Ohu29im=af&O^z$XJQf}DJraK)R{Nabbxe9bEsoPTyLhD_XI3BI zUaCac5<=5gN&@EJF68N{w%nAcZmcVQkv6W~oE*lci+?a-g8W0S2+<+yXw)sw?A_h5 zfCR`U*f%TVQ2BB>$MoDc+KxWA40C#QewlDs3?42oDrD-N{}|x&OqpJr*sP|`apZKH zEch-vop@~b3g}79_xJ~v4Nl%MQtL`R4kQfl+bV?))KNax(^|%<#r#%PiLP!c6AVW= zN+Y9k%t;msDTp^USryz?o2iA#)%SA5#{c4O)zs7vf8w5eoZQ9g&wn9u&_jaAO;@uy z#fGL-2LIl#Z@H2X*Lug&6o&mPI9%{D*5y{u3kx{L1g=l&IPk`Hf9tqG0J|uSy;A(- z25KZ4v}{=BT~U~nm#a@u5fTxsXR1?l(?VE}ivyQF_aAeBg%7`)R;^Rx6fa=D71c_< zr0k-b4n_J*EDjzQ$V9$-2aN6iVxMq3G~^-nFQidg!DHhCmjb?=1OX>dJe-E#lllRBJ+ z1C)I8u>E7!vM^Kwfc8b&{( zpY_Cd$VSn7N%IrhEN33k+4-pBUI%T@)jwXDY24x~GxF=>K%nbOK28BB?&Dd7=HZXp z)YYPT1yIoL+8Y!rWI%a3rf~}*?Aj>OmMG|1h+XRXi*OnorLl+RTtkbh)L$knmp5k0YrJ8j)=+O0$(JF1~ zK?0}qcC4^Twjjj!hB2OJ(aG2j5}AF{W`g@`lFCiUbPc;Mu=#(_!|D@Lt`KPuTh)Z@ zY_1lqGM=|IJY$RbYjdB#Y7p5l^663sOA%})QR2KAUR3A9yr&_@(DqvW6x!Qm5w7`U znD4wL?61`2*T9_C)i5Qb8t;|h03Her_jv~u!nZcUPt$aB<#9LS_cjMA-SP;4b8v7+ z=EUCsfvqQ`^sVOU%C9y4&BqCIr#l(E_>_-oCDfD960@r#Z3a;2q94Z&bkP-|szLDI99*J1aR6*oOXf`U%(@+VA`fU@T}Mij6(EBDVVyeF84#E`+HjU%Rs6wFL-fXd&wu?Mw-YWBgNPITU{*-a~yh+ARKs~{` z=dWD6;-|{J!^0u)LfNUJ(IYrpytLQ)oqP00%^&2!SxIL+Q3A}e{mc44=Cx97C^MIu zj{5hTiOmQQdaIO`9K$nj{D^y@YW#5excaW^JWX@`CfMg_hAz*lWLhU%K!k&$(QDyz zlILl5$wA+)ir*OoABI=~tw|hzsV`YQFZfR`DX`^{iN+a*^E#c44Xal9mmdnk!rKT% z|KadyA8E7U40H~6U>P|b0ruV4{0{{s-hIzmCFXn^iSp~?#gFTgn%tG)ESY+A*8~xI zF{}5tYO$q_azZeZOyk2ON&WdmxqD-NMoyZlh8T5C& zrt1cd6f*>r6Ud(kM7@xpMOFjWdIciwxwZDhI~|+;z~amP46arGb7n|xM%~2yb9*8f zqGO()MgqCrbKMkb%zD#OFR{M0Qbg0yutQdTCx6?5>RgUle906{pl>okJ8??J)N!5G z@VHKL*JctaXl(ry#rV2Y8Sn4quhq9;Q#*CRoz$^Szo{T+gcGc@-dfF`nQr%>EP~HX z;{xRO+lc)R_W#^RD+5qv78U`0jr#P$^eGyrk{hWEtMCZfjj&Bnapdah84^x4=F&i) z*he^;EDS14N0*H_NQmrCq@V5GWuV}=q=El*+c(HD7{1m3f&7)ta zBO+o3y{r~5*^tay6N{lSpS{OfddcyuqAx4BqJQUqmg<~Lc<{9r5)@Oa?qmIg5phn8 zV1<1|E)Vk+-FG$36c+?k~oMn9Ch?RR2P$o5?|_#&+OU2NWOkM)R_heH$H(%CG*Kiugjw*|Dl^6pLc^8L_81+i6G#J#&YFSp7S`ZN){za&GOsQ+yj z9BLzUEn0Ic?SOJ}7B8FKh=@ck8C%=FCe|!ABLzi;kUAz$YusNSsxzAZa>t%-us%+QAc5I6%yhj z)egdE;X9k?KsSy;X)i`K=7N7~I_+GFRqr`q7U}>J`1I7}dq$^cJ&OT?4=;Hw=Xl&d z+yEuyB`?TMsD;e2o@!EtU&!72nSXK&E6*ZFBnn-@#62u0ENy@mtcXU*rPfN^cMD5Zi5~b^+2@NyZ8f;(ooJLDL zyuA&|-<)M$GWSFRC~>Z#CiUbr1bW7gn6~k7_E&LpY;3Ight=T%-OErxM3i!T)9Sg= zr(^ot3ZFkCPUBJteR`i2S_vAMW3`%BvEF{oOwNP8~FxtAW){_sn}^dvF0JkNlL;6IU0dTj8GtbG4|htZDpPC;w<^vPWP zqslvCo!_U}!BOTGd~ylj76cr;HfMP4SXN|?3kmj_i^l#b;-&ca1mJs-E?9aP0)fu9 zZ=aT0JT~G8?b{g2^Q`0g)z_z4oUuB^i0_T3qX4C00JwcNKE8mW1*<06|5SBWg70Oi$d(6qpODG(Sd=!3A zsy&EgPoqm}7HwM)cz{K)bZ}Q?-|tLYdV_vNGCuX>WK*s)UN*17Xpx;zd@01L6zqTq zHIACeKlY2OV+;S38qB^5JT@+z$#VKp(d9FwlfUrNfhwu_%99x4A4#O^vQJ&lP4j1M z5xE<3OldQ2ucJ7@M9O1tcenmg&Jx>UaG61IS@1wjo`l~PT&9kDAepx9h<;`4Lvq7Y zx9_>u)xCy3BDZ3RkSX9>Q8=tKTr`hEfJ)SL?1o$H!IImH(1tWeFLb}p0FKklq;?+# z(c}Xk&To3KBANW-{EdreEdvSBJTyFH7^Jj0Soq@BEg-L}xF=z|w+=3fB;pXQt_ z5v^NGHp~Jfq8((4>zTMEH_e6Sf260xoL~e3iI#e z)Nyrp@`64x3&lzLQLG~g)tlCWjA+8RgrbPfnGmO-Cr@Rn3x^GkEUSD3=6*LaQWE{z z#q>OtSq`n^jJ?-`F7+{ctL9E>Q6Xni`#)Xf3BZwNJNpnt-4(EQs@%}_4+R8 z!6+>EjRH5@i{cSD*2~AC?92NmrvhHyETxLsM?JClk$Vd9uE6LwIqFw4aY+b+M>jL_ zeGKfrd8O9qIaV_WvN_RwlTeC!c-1{gLbB3Sj;ef-SIffkgS*PnJMcaACa;K*ko}@OK7I5sD;Lp2c z{klH(r8KE3>u{HWZmgyvLjFWhWBzY(kB@(gYuN~b?yKwR=~p_{D|E!jwPlTQMK7IW z-jz8^{$!JvCtJgtOV0Iv&;=MD@gUOCn0TWtzc#8)Ub;zWZ&b@Gqaq2p9X0DO_YLE5&I9Mn4v269H##~VzqTdW+RC0|wK$QNUnAkC|B|qI)Uh!jeSI1l z+Oq?rg9u__JRctqKh0W9T%5yXsnL3-7!I#3O4yfI3-4ZQn0i=FrcMiT^^WC{ zDUk%f&9S0H<+=~x6b1s|mHs_d*EBryZx0544Gs^!rGkzH#>BG@u@~qcW*f7AR?+@m ze!{};cTr!IGrqZhXE{xHMNllS0Qct|>-;_@PyoAPWsCY13|`f>GPF$J7YTdm#{~wb z?8a4r6?!SmKk$C=cc4PX>{Dq5$Nhs)QXUUT)Oi5V#VYw!FvqdtW&n%2U2f?ai}%fJ zi|TB=fe>f6{Q*_=-2?-EBW+nhWhqlrw-@?(7>fpFBtqUqv{dTP z90n8E`G#3Q`!JmaY{#1Jv+6d+tJq#SW@_j>#`zQ|oo|LtT2Ne0v@tkytYkgEAs+tW@a2%*=C)U8X!H<V)*EkN_#UG;^mikU(iQ?{W zJ$}4+98xhm0YZIL!{j*cW%Gg>n4RL3 zQ&YaGc!94)$~48Dlp!`FV!+9~hGFq-cK(CeIZKZ(#Mx8X(=$6#u~s?YqULk``CAr! zHtg)d92SXzZ_%P#O2PGNrk2=;!zWWS&ggVw5BWt#B!KrcW!~am;dIUpyi!*G=j=2v z64BmYR>WShaLHSDR+bwRc!=Z+9-n?xy8iJD*Q zzU-34>mAG#HfpUVER>mr;N^y_Ub*V6d`O=Aom^~OfLb&G=WHRP=m=3H@tRO81j}xB z<*(NNGx}T*$l^^6g)w7#?{UQD+H1?w=KXW0jKP(ppJhp1c93N^DX=qQOP4 z-6)ceJsbSR0!P(1G=KQ_9TWRN0)diSNqw1^sJa;yMHwwEqspgKc~;J-V01UiS^m9V zf`dQC3)hNr9U&)>z+C0)yYNa)km}ttyKle8J6ExuESBGpg=bt^_Jk@q1IJdL2HKWC z*)%YepOY7@v1scQocS&u6u)Sf#|E8$5~+on&~ZZ#KQ>Hwkj?QC`m877sH~MW9 z=)8@wGJcb@x6`FXKl+u!wH-osc0$>eQ-<_AWZv=d=rELIj)a({9OlM|KJ(cfs5Hr5 z>K^(f5qUU;qnIZ)EAr8dCd-r7yCx)u$q;H?@rk&>>qf!5%~%_KUeummI7dISROQ`7 zEI*#E8bq?z5sFlVgJE>nbH`*uivG6@zM(31K-S*x>eWgG;jaIc!1pC%XL!*bU<%wc za`JbKfoEjw^bZ#9ys9j`e%CJZrpFr(ha&v9KK8L{H^EVT6u5_Fm~-|SKBf`_yX9-N zYQ~iJe@Ee?_#S`mf)(r0E+x)O1T3iYki&)wk?D`7S?>9}RHjtUqeGeO99^07IE(Ub zeozR7YnEH0nZt5sZR~O}(598iKHdsSAxTAt#5F|(M7yJdzfT14Z4q+JFi zh??Qmk1!gf5*a%t=eCNgJ+)3_Z(Yfhq~SiZor+zAuZ;awsyuxBoZ4cO;m&M@o>*W; z0tQLVA7`8~3tY4^|G<`Na)r)olwh3X&CaGLNrX(Qp9=-x(iOSlOaNR%j>(Tbhx>n> z3Nm|RbB6LX8a$_H%o*O-75)FZm{91Xm0MF&QCBB*KOJJw6(>{Y={QrIV34k|8TUwq zXjLO+FZ)fRkuoETWxT}wp)z_tILKBON9s|iZI%37WMHfaGP;KPXjEYcKCPF9tNLR# z{E3RxCGc;xrj(`0Y(Ys2H)AFw@K()O-+i7N}ngZD?UMEC>a{UmQ=5O36j`mG9?KS9KJf|y2wD?X- zYqM8hzykPoeyYJp#>iVX!^%C?p>rjY(uOjD zUj-1llW74br=|TILjxv(vppWc3YF6UmHgJH5fQ#u(!3(Ui&z@x{USX~?sl+mOI}X4 z#&(hOL9>@%?&>#I3e%70igA5doNIST5D^m-x4)8t>+ln)cVAszYRB7OWemfmXEutk zzfk-2_e~B&z*Hs)g=-ENwti(5V(2!X7wcnM)K&k95R4lORqWsmP5P(wDhMw&4+X!Q zx)J%%&d`^B%Q_s{{VXL@PsL2(};gtK5f!&Tqr*6(L8X>QR}XHR)=F@q+(B z+gnG)6?EH!Bt#*Qga{4+f_vi*!KHE6;O_28Ai*KHOK^Ah1b4SagVVSNnufUz`Q*); z_12sDebaxk=-X9wt4^KT`|MpliveEQbBfW&F&wEK03)>;tSYT}{pTUvG<8qco(HIg zh)JyYkfg&_aNQjw4b9$(uA$u>X-IS%uw z@jr5<<&Q=8X~yim%9TBe=&aM--qJPS4ctgYGdjJJD%jgRe$c2$@u1lgqBukDEVlks z(<4g?b4!vy=iXF{#074wy7v36W?M`?kJww{Y7%q{JrkHNG|uGyKyGl_+uYwL#KEAs znfh3I>DLNG777V|lF5HSUPQ;XDiusfty^i&ROA-4Je9Nr6@e|p#1~QJvo99ka?N?s z6If-R;--7HaegXUzu&uF3z%ayoV?eX(}x}tpYD8 zX_o1%b09!-q=nw^+AWvLUI9p~mc!*Z%Pxal^)e`NVve^=k%>a%)D7v}HbcW4yS ze>E~np(uF)u-U)|Dscx}EzhR@*}eS>cP5YG(Z{DQ%K$=#MRVJUgHKoNlzfW44;PO$ zs>c&NJe~|M&Aq|ExxD3Iy6Hh4k!zSQ<=?n#SEZJi+yegybkbBIdkCJ&y6D{gPQ{g! zH7$3Wy6hH01FBfNZhs5u-^`T$cL3lQwO>B!Zr=xJ=E6Y3oF(+Uz3Cc54fK{v{rS^Y^J4R$yz6y*z)_8V=s`;F~| zW=*5)I#$`>7n_f z<>i<+$CH69+fMs%fq*^(_*|1DIm~fE^XNMDt8n^W3bi%dJuG*$%Rx6PorKSoRcd{C zRnlD(tI>6LJt;jh!od!%iB>{+eBl0Wk?F#bq zg$mwRv38#AC*okdPO8?AHL*iRFqpu{IIiIpygDGLY2(aP;4@1G2Je+YM?H8kFwo5A zrgb1bDh}JiN_^{Hh+Zh;eWm~=n2)W4j5~T^-+%E zWCdG_W^g(NJ&VCCbldm})OR%;O}dMcR*fnT3``$v;x68-{B8gUn04+$=^q#TM7_Tv z=CQl#Nvk5xt91gW2zk~5d!V0^jqTfsyuJerwpiC{jCOtUzVg%*VKMuSABT*Go6nw& zBebp>Lo+DU;x#w%ngUOY*z&gKFtnh`y<`Bn#$ z*jX$OyRyR9<}hGW2nGi=dTEh>%7^2+Ux@ei47_oqmUu$KeB7J2nbx{6YVd)YQmdY|#F<2J`!c&zu%@x(Uz&DpHK z0P>|)!t(xpPFT0aH66I{w#u46W7SGFn~~vc2Ztouqx6pg?LwLE>7^cJt#O!371^3m zW}d!GpwrHUaKx8oP3fKVU9$=3{)GAh6=|G2*D;ZSaBA_34BzVg-tU=9aae{)K}=}V z_M7k&`*w_2?D3WoUg8Y2q}zy-Hev@L&}4MgRgB7tiulXI3z1)e5o&IX&aEw?U~5s0 z2xeRtB22E0qmYw-v%YF}599gtPA_t2O51|!LGYEU#S#C=swVOo9;<_G{I!q)V$hMm z{u%<K4eqfH_?6QQ{gpl-Z;N$YH$%Q8;6!XJXc=^0c|bb!A`EwmnnYjF+X0Qo*RNJ zijEQ<3LiT3Xsw!VVaiZNAz(SesCvs|CPrTMuCuh=87%)?Zfs9X+{!B8im$1j+|&^h zr*y++^E`=!(5qbU*jXdsjxf4y4#0{AMI^WTqSA?PQQoMG@77LF#{Wvva;B_Uc*K*v z{y2~F1Etx6_xmI9^sj}T+@vk>qTg@nBx;c#5v6z<3rdahJKjV(P4rVno?)5cd&E;eT(rAsJ6ac^)Tx9*bX;7@-+D8em!{mt^iLH&8YS)MC@3CggYe69-G;o% z!kwRWLjd3u;HIC|JdnWO!?7FOgyBB(ZC-q|IrDR*kXFt{rEA}YW`)O|- z@!Vc|eQYd3SW9Bzdw9J0#4UGe(=lH|@0nV?qI2W5^|LF*>*L;7N#n)fRjdw-XTx9B zh^(f27x8PZU|0A&wpEiEggRYlZh=wM%zlalz=i6U^Whhtw;ic8G zn(94DIJ#7?lk0VNSclgn@Cg~nT5p{ae|&vx~#JO%!ah0hJc+ z<8m(y(u&2M&OxM&Q2Y{Wn(+Ix?|n@Jxck5SjcwmbT5{lfCSaDe{-B@`w8zIa9Ll0x)iF3V0V)K1SJl z<7*yhI`w+zylYrZXNtuyIf=7^L;&#q(2 zkci)ERXo;+B&fZR^fBbaEq$A>k~+&fBPUL_Z6Je zI%gmCSEW$t2{1Zr0u*K(bPPM`Z-~W>56cGjYcIY) zBlKOlgydpUoC`A%vOs(O07Hizwi({BE;m5sWeS!gJHZ5h=y{}tWGLvA;@B-RLPkYQ zT-H)uccsQ?Tz(w;JGMx*8pK}Ka%zK18kp&%y7P0?kKUM;lCjJA+h8h&#pNlwZIpYqKNsIIqVk|KR9Lv0qxgg^4 zeA0_~2E88ojTy|jA?rKj$D)bIe+q(qNFk+ZxRG4eTCm`Xv|GY53E|Y4BkVATBq!gM z;va!57=b|o8jbZ&uz#+}$KJ_)TKn2I!wN;$U2sDt;Ctu|k7+5_A-Z2h{gu;6t*3M( zDwlRD{M;~+^DNOKWmXj%78%7YOgrm#?K2_QJGfR6v%qRBhM1hKb%0=Wcxkj|7gnX; z5)Szu^W%6C_q)lPsDR-$7|kpL-v`b;5O;W^UE9WBOyp*x^BIbR| zu5CiXXySd(8>awDFGMqvj)4xN$s{+ksSdFAdeNz=mtTBRP2=>kQP3V2vuuvO7clxd z9I~P)TM!x(Zw`t9S4sWc{>}+-UJC0T*~NS}sIwn#V{sm-&sfebSPAj3!@htr*2aipAF2%^GVGa4j9H@&MGF>cM(QNgYIy{kO45gJLw^$(lQ|qT% z*4;4t0qU#cFeuav!F?bov`0QaI$Squ0s{xHcWqqNIP8H5Yl8jOcE@BP)jR30{KY2n z|6V3Q%>KUeV2JN7Cq}C235$d%VgV<%=Zqq^r-`L|eJk((X&?K)JMsK4y=ec(hheGL zSms1uA^II6)cYpV8hcb+@xAE;7yXsBXi6>;8Z8ys*=?Z-5pS9PZfCQA`HeqG7*ANwIz5>~{eX z%Mi%a$TB@{rEi{O6-YdG@Vn21}Q36`G=?e2;#iO6gGN=>KZxh==SxYHA z%M%GviQ(Cy2|y{6tyDxPTn4`&A~BX;T)=j{HDued5?-PXZ<>ok_wiAmHDVr$T#8n)! zF~ZjTVxa2SMQ}Wbj&3aVPOFyo)YNxNQh1+ETaKp6&EjWMOl@%4Mk%$sNI|^7&na+I z{#OXQ&1%{UdlY#eF^$8l#v3^j`jPz36i?b$ld7vbe}Lk)p4^_u%4TeLr&!cI3;!|E z|CU?_cCzP4=?)e*81a??_AQL0xMCOFtQ@3lH9`?b1j!d4*&<+id9m$Hn^p>_UC;{pZIQw=)-M&d465=> zNHbIwvJuP8y-mAi6C;+>UuCbX8LrvIr#(wM_}glJ&+pBDSO3F*p;0g1yef!Jc>KWR z;S!#K#r(Iwt1P}V7x23d)$r}56L_|WwC<{^gB#$3?rc^5PuPDc~v_fgECpbK& zjZw!6MH)Rh9;g1-cE;l}u@Ln4&R&{M>LT&E>i%c%RGJ)XB=gnL4W{-KWnF%OYoslV zdV5(XELL|4*Gk+KV;SIRY5596x4Z2&AUAZ;>w~h$d&v78yep#O8+yJuf0S{q9BX#^ z=mo3my3km46+NAc+OeHhR8UOWWMA$jAtv_1vEmW;ZALi^2rOJWD;;OU$D`gCrE?Ei&nl-W;e3#F zaY-|Pl(Q%l5J2klLt9d96jn`RYRfVaQ=j;S+B(KrnrL0S<5)*MmJS788^{d!ZtTZF4HSR%BS)5nu&p+JOv2}-{0*&b`rGnu;s-+>x#H;JRc{4)sdmYp4>&?~i z`wf9~GbaM(H4gGK!+W(ygIsS7*23x?1M4lp#^bMxY@rFPTn@JH;^AtuC^DO)KR-ee z;@`Yp$P#p^TPx?{*e*Y)^{s6MHJp$ZZAd(>EKX<K|lhVl667>X`8i zcb<%lR&_44?I&@JfNWNVCnxrcXN-CwWcwbH*=OAQ7RRNyMxm-okq-9J^{?vOSNbXC zBwvXHj~OqUV5>rlSHaZ*%H*7xXJOB&x!zwc1-J1|+u6;vhQFepss{H(h{Q8htNTfo zBs#Ny6!Ny;*COnmP}6V_^B?M0>?FnS?FV&|vtLclahZShkXH#4Q*Uw@kuWTb^g-_y z4cMQbof}Hyr4)DXphYaqB|O0F<3Llu=o=*sZ-3N}uVP<*g>;sdD;11YX8BcH^@gjP z+5fCv+A(dce0R*k^4_+se^xXHBJH5}+u*o-LXvOd7qi(Y_@ke1>4ES23g>v zQS^-A@`G;$O|;$p4sn5RN*)AQ#qyCvGX#RMx>J03b6MQax{79;WFV}RQkw1a8e^4_ z(Ni?2=GeJ0Kz~9il=UdImPu(L>3bZ?H$@YNg;{B(zPM!C&9+-0+LeCY$0zV{D_mzi z9>jbd1U)ePbVqZFEW5otUdK5{(n+lBl3Xt;^F-0c?FiNq7ZDy&4UlInX)we9)Xj z&0oGwVbD79PPdv+M_*koWU+nv^mDHbZeI#AZNg zGdnPvrA26h9#h%mAAelD^Pee+U1&05@vLR%qUpzyj>wW`9N)JqV%WA`5GGa|BZ^EE19cXwz3Ox_r zCZz`T53q}*EDdv!P$7FEhg$sLEBP~KzKMb@T6z4`S@TY}?s*0Y(KCaD@%AKC+J>r^ zDc|+b{kxkSy=%jQ6zqo|7v-zuti;4WCUz79Pdv2N#P2>;>)WaDGn1^&kKRgtC`oIJ zy@AhQXe90_k}aWz4JpNO#2cnw=e9&ahJCZWHM($yGQx#&sXgLs7IUsswbur>zFrkU_ zl4clks_>+*S1Oy1ge-rTO~a_i)(2?l^ka=Z!kTwXB%*;6N1r&w9@d{~BGyb_ef9}m zUvD`ZO5 zL#=E9QqJcx$HX=@X@fOOp*^Wj_pMQ6#OgRnue8X2QvuIFGH&YRkS8-*__aai z5V;p;JEVAeyg%XUF|Z-P>lR=9e}W-&cEjm{jZv^2mZ z^a&FrRI0Qa-c6z?12E{?kW;?WTH1L?Fj*{g$KEN69A(m7xrR_G+&6}uPLV8zVjRf7=afL;I3)|&GwkOyGI2(nY zIrhw# zu4_E^rGzxYY=#KtS4TT~w3u4{^d_zS&1vd7F{640`?+yECZ-9}7hVL0?o~8)i(jjf z`P+_74oymEHMEN}>VKJwe39Q~tIR_W^%r zaHB~4f-vnku{&&UId@QDF6d~wFp1MClQU6jFO+9gY0OX3y_!A2>(<`|DQysEB?E zNzDS|JyLZXn%ppf5aI)RE>D(v6+JfT#;35tuD|4)@$}5`S=N}icy#Yvliy!|2os72 zhGvXbm+&A=-nWr8S;b?>w>cnEE&r`0%~QnFY`!td-;rYfe^v1R>uQz%WwQ7M^l2l- z%UjDzy80sW;-u1`tQB{4@G7aSI44(}Cuac0L9VfQ3%{Bu0?-11dbRHPpTW^LowZn} z6$Zy%WlPLB5#f<#b0I;;A3rU&RFci++kzLkO<5rx%wwFaXl3VU2kc-*?Z zmA-7)g9~~q@tBPiE$RK;V23;h6ao655y0SUOG-iY%1W}t2gIJ83V>1BDgqkn8<>F? z9xWzB$68KXYJ&}g^9pD~zh~Zi^|o)9uCtG3wlp{SWLt6DKlr*9Qf=lHsni=i8TkWI z)C+{xCPYgvpeeV&RN+JALbRT4wWKnO{FuqgQ`L4mtz1Ki7Jo|O6;rd%klaA57@D3C z_3H3O^@vKDS&!cGN=kcP7dQ2z*b1_qM7-F{5NHZ{|D}Y5*=`wHgMtP=p>=ZA$q8(m zCSF;k-;><&*RtWU-;N7>MMo-vx7b17Ze!)~xhJI=qA{Pwg3_KX5z$Hzlb#K+gt?Az z!>{3^Kfy6+J;#KFF~1<%JhXKH0G}7|6M%LtLft$jT8tPZXY!wG4@*GJMtRnSbq3vZ zTO2przqXbJMZ6-ZF81tnBZw!;xU8x&P~Ua&Je&0Cij2q+lfN7@AA>mVSrT+-yf;v* zzYSCD^T1%79x6p}t_!0L__wFB%DsGPmMH=&jAP=EwR|pVLK5!=l~BE{|BJu>?& z-;Qy-UdNH@_-xEcwgEKu`Ay>UXK|&nsZrzhkWU{eZF-Z2XtZ#?WBs7Lw402;VEOny z)(=1}cPia)`}<8)_E9ciP#CO#p7@2(#XCiLvE>4v_M-brJa-JQ)i4`5CYIAQ_4Cxw z+di}7%Tu3Ni)*Dm7YuOV%6byzA>+`!E2Yk#7?xOrAw9{v-=+}{MUp1@0^;`(8{$$P zrOFS{yxiTi)smJ)Q&_iFCy31D`n_&O2SCFB@+O(xua+4yH*(#> zC&INku0wSEU1;*jXYnaGag`R4a$hKK{RAGJT=x2I?%qrXb(zwUv=N@ldbXUlo2BR6 zj-1@rdWZZIOV|Ku0(G>AY2!1ksIkP;~Jnh^u-ea47k6qV<=54$?P=FbN5XGJVl{eu-BWA) z*dOvs0Fy3wpkt@pVwQ47Dw5bKEKioar5B&A@^;F+-IFAPFr`5i)ni(UFvE7-WTqu^ zw~nE>>s3XGrV&eWh{y0OZ;5Hye7l=8_DGwB){q7T4DZN#mgTo6K|kfI7g)m}8e;bL z#RffECMQ?tix9!*6A28v&PF5x`Z|cyevaBH%WKPxL90tw0uDKKHsyo zgb;EisQlR=1Vk8rl~5bc9ti@NhV`2_$?)0VN99M^j5$(TZ}8`H)?SzrI)8cRBZ@}3 zP|>lDFN4SdsN7wuH^$uu?8i)w2TeYNjQoUpuzk=hc}Y)+tXsLlYpzy$T2UoGMQ1DD zAE&!plardUuf^L}`7DUtiFqF|qJMGgMu$`@?&TXr_zC|#~qvbXKi&{XJRT~Lb{m_&taj5e!g$7FZ}Mr)=R7xw(^N6D>PV?kzDSb6^w0U zX8YU;!?gLs^&MBs?=^VLlwAhn&ZAFPe+K@V%Yf%B6cit*0;%J%ChJA&epF>q_Vb{t z8ahjfm?rqFR32OsI;uS?=UFKdzgJ_vfzRa}#F@a`c}Grje{@Z<2)6HK)~mMftsOGD z)MaLMOjaQKZ(z3ZMiM9_?1*fqf8PBs&yIv95I<%$cLJ+=K%rfKmju#?>D+&vd6yCo zVvWY$&Kjh~|HMTF|3u9Gqs3tV>4z;5`TYDVIIbNyV&B5Wz{!sWv~-nUYQ8S;Zv37t z#`j#y)jcwnm05At*}s;gu~R`zOCIiv%@q8ZujIn7mgN5l-Z4Laa&UMEFKE;pzumrV zh5&9QDigY#{!0Wa$~0y4W)%NM>)-^J(7W19(gVPA=}=TtQ%K*|=NyPl8W*r)gnx!X zaiTaJp$5Taj)sP7JuOfirrURwl=19Q8}K*_U`eGquL5$yZ@F=*mIBA0kcyTpfe;%X zU2!=fQ@zoPI6m#+V#5#SI=(_aES)e^TiLTyD=5zWBR&ZEvfMT=S4rG<1VB)aK%|H1 zrVkDE)@d8rL@33m-|`g{rCDEH2Au}Xr>Dp($KaCFCjooRXyHz&sA8!m6=(fWBXyl^ z=b3P7XgrVBJnPNNGSHyhKef1FBD^Ek{lR%YY(?7U=oxb{u#V9@roDL1m6;R;YsnDQ zDk~3~N{xIBx3Y901HGf_T}uW%}|JDgJ;nMs&MfxnIvq6%mNW9L z6C7Y2<)sh02Vzlg&t>XTMGXlwcyhaiBAHm=TqJ?&P%%py^q{Nk;=K#a4ea^HQDr>s z7}1XUn@S|G+^;w@vn}(k%+nwpDd=>8{?VD$0NrT*-48B|cKd+j&(2tYQ|LKm#qs)J z%ArMnX@L6W=x@TDRnZBv6Fn$VI88s>+6zm z$U*dU3Xn(Wi99!D)x{jgFKNF`zms%BJTs-VDE5aI%Is?p6{5h0Sr53=2Lk&+%E!l7 zG@R-CZ!1G3wBBHB*|_T57};?GxU1%xAzr8h%~M1gFYZMpIg{u(X`Udoy8uSMvzVwx zgaf6wIO^AUl$n{Co#_Df&vhL7{giTyJcPjzpMs)51{3Nu{0Q@Pip^NjtHk*7aiBfb8#O;|B(hVIgKuJ=Q?+dG)7tXtv*Rm%JMx_19dA<$ zm-f<^v0OKkt0npf@Oz+2wnt^_vL(xCa&2~Gp5Vv2C+A$<3JI&>vb-pkG+u9Un|0$o zmTv2cJ+8y+0Io59??Ry0gO7}rE-{wM>T~ERhl({~>9`bSsZ>_|R&mMBu1$W9-nZ06 z{(jCkQfax{TO?5efS$3Snx3u*LL(7UvHe2nF=aE8(C7KI<}a$wT86pge(d zdaLm;9$SqpPw4F6#{Jt0lg9cS?rSN^Uw>?m|oUWjZc zuuHd%0o-qv!1+Qa^I05|X61uO22Tkwmk+%xdA4SwA&p0W(m<(YfLJ>w;(J~nb{<6} zXIAF)D!zj(C^O=ITkz2!n#7l|pKixV=7pp30Iu$@7`dNuCS*XK=UyryZ|fo(-Bx5a z$gIS1DdS@?)Zs+ka6Oyd?Mv>k^^w7HpdSTgf0U?kZZu@_sn(ZXyMCn`bgc*!kaKUM zUl(cn!I9dLuEvp#);zxJzApU ztv~efT1ty%uuNX8%gvOewX`X4`r?jTnQU(k_5Iv6v#+U?##h;n&GMW-x?E5GvrP|3 z$9FzO1r=HIP2pEJvD?L3VXx2PI8x1%H`_Ff6U)}*IvF88L~@ZEMN>;70<=&jOl z!o2&zu(Ej9*}yrhXS>^*dgq32V%Gp)i!R=dW`6NrpAOO2i-n3C4RYlQ4otRYLl`gD zW;Fbqn2nP&BVlk>^R3@vcm@J^9>)~G=Kn1qD-r?JI9ww)R0i#hl5p)7v@Hi%eTbu6 zsXFJKNvJNOpwKOJVW@VyL_-PNenFS_oa#uyGc45hoAaXTWytPL2Yp{Z=ZJH=4ZKZW zz{x1PXBhIGWM9YgtaL{6rj(YCwcWFFX&}UO76iUl32RWaR3TvP1p!W*hY3ZdYQVTs z7q~7=va{!&`Rmn0U)5^ZXOjuasyTU?7YwpbLuJ>)CgC*`v{fyXOknNz)+rFu!6Sz3 z=RoN*egwvTjqL!dtNWBde0iGFRM<9rV$`|XfWXn^?Tq0y@+Eov5VOf}_s){*Tz+AX#lwD$Ombd>e{YU%l|S8*N5GcI)+z4ycTHt8IV*6(f4mQ12+XvL=; z_?D3@^Swx>iIwW5+X;cU?1hSXX;UJG;|$XlZ)W^oNU=Bqz4xn#;}w5&6dn zw?%~l*(PBhiJL+G~WWDDly5)QnNmVfF^U?gl9q=raK{OZOO{yN!=NO8! z^*4Gtn0+y+TKUVIjOLAdq_(0z)&2ccTx6^JQFyXJZ$DLq%>8n{+_s|CPL~2E@K~tf zRxnprIT7EnLqIHgo=W*UrV1JwW$&$J_vbD4>$^wB2KzDl^^7axvZ}w(UePa_q?~l= zdef+gsOiA>Dl?{p2OJk7tHCG=Za^VqpQd$|<4}Ye8DZb`XW3>Gbx3TZT^c3z*z2^E zgou~QUB^eO>$c4wW;$6C#~XeDY@&?XZ1ZIE%L`r222HBRqKf7$a~I`K*5ENKzSC<{ zfUilmwG|okiimiPRqUj;&@!n3BY~VSNpY5URi~owVQ=r(IJ6~{;2wO|nXRj6k+e!F zZ0jRsb%e39(~B!vKSQ!Yse?P*pOkC%2Ri z^bfPrzZjj%Tapbn&6Ayc-DY3Man z_BPqd$+pL;ZxOtW{4|*5^BDYYRn6R`P@XYkT5aG%cTqsA%Dq@l@18c42>l#xO_NwL@*++TgZL zIXZo8Mho)b_)(4Y+MrDNSqszIVh|jA;xvJ)kqj7-Qvg>o5&K*Phc!NyK(({OIbhm5L%GGV9bV7>(M1Nx zG)#^}q0-E6{8~4g#T)7&?{o0SfE^NBVy;a+;!Pp zk_-CEoyn9oA0Yl@mkF8$f=@Ch^=C?Dlx$);ms!WSP-B3)=Ruf$Z-2yPuvBz8s8W{& z+ETVS`h!eF*4UB9Ib^$NHH(b0a8iSUi;0P*+4+O2nv;O3sX(Z>*$ip3A^tvIJULsK zv9aa)lE3;y!fvc#8^|m}(9Epp9IOQxnnDv(+tyMmus;4i@IT6cX^#xih$8dgmF@*= zcoWl%b2+649b_E?5oab9SOSJ)x!3r)$BUd?RUUnAgUv#){f-griQV$){U6ufi0s$^ zaKoYCPS{j=Nf7&jF22766)$4M{C)+Sgo17LDq_%DcTgKvdCoD>t^5ASY$_JWuCD0 z=oC)!RAkGCjaQB}ruQt@NDL37av8Vd8|{5 zfx)ylWj%Fa+2!Rb8ol%rl|_HWKF63XE8IR_d@H4jaqhRAl}JPzo-gxCH0sJlHPHzPHWAI zg+{`@EGM_2+C%uHhtB0I%4fmmC5;vRtS8xAw#MGEP@D22Mj%Hh5INm%4fC77)Y$l= zX$FtkJzGgrd5GSE`Pg`M(y+`!G-u~*>Bt(OnLafMBu&^c*|Zk!x5 z@@D~2_u;Q(HPCc_jj**u`lf7JP2xi4O>q zPW{kGbZdB(x)|?=ht*7sLME-OK52|e_tvX$NI~1#$;pC#vD6FN4{}!ZlLa!PK}OfG zuVag^Zl7T@Y33jhuZ&OsEIPAeqywz7#Jfe3DUAaXOo9r>50K{jG90I!O zOJIw^5C2fo>=a!6;n#t=#e!+kI-U4T%guXeC@AiBT##^t^zkUi-20#HJ`^`sZk&&;r3;J8vFT z6+te!aCz!{R27QLr7R{G=8}zwh{*A;CoWV{{Rogva^}R}2EDC2lFVa<@NxBnBl!Rx zF-MU`oG>m*b|{?d)gz~dG$Owq3f+?Trm2%k7ZrzT;xrtVtJ{K`h?mOm!%po3;+x-D z8j#=@M9^?6^6eokD|!*`&Q|82cw!7L4T32ID@TOy?LMiJz92LgH|Kh@e#3jrji|;#f}WbcA@i&`gMlbr zmMRU3wmMe_yPofj={>mgeFyW+2Ba?=G_cN#bRz6@t!o2bOWs@wY(710gGf0g!P$zo z@l2;=_id(%{-}BAWXaYi50O6Bo$91mWv6L-HkHUN2$N3gO=_y;(8vcuoQmb;gl#9f zx2DPAH`Fz%gK`D=gtiWKhCRO5M*^`FA;GbTS1(Hs#O0w(>)CU2n*tMB7enozwNSht@kt!hE&ip_b`LQds!8 zw58Q6r!#$Qgjh?P541FoflJ4>op`JG&6T)FvW6mXtIUZ@jKqWGOAa2($VjFDgkY3m z;`^=IdRI1)HN=a_eGZWLe!P%cFp9Z*73ZgA;AN0@9XaKO<5jWF_D(Mw@lLgu{Hm zkFyh)0IRF71KqIz1+M}6anotYM8hF5*(`zan<&RepfV#4M#f_U|5%5tuC`LR!J1Ua zYM|H>#7dWFi0}VJ>+$23M=5^$%J~A($a!kkY^qYm9zB>cq)P!LL(Tm7uf_p& z(=l~flJ;?_FYEe#M@%ylK#CKKio-l68T)^Kyp8k+c!>H1JYYZGH)>$YuXZ!r{uNmPd%?RO&TBWACD*v+GF+m4s{*qlX_fS z3en8Gh0^&25f@2W2|8(iRBiusuSv51)uqI!3bq81yk4OEJbD13j^a29^0la@I`bylYBsrYA1*xi*|Olk}Fv03>9` z%Fz47_%JXly~OqT?i9OZ@rXO1@A(-3rq01I(LS#vMDDuxkHwXmm2?8)a~dSh@xxy9J0r$JRuC6xQI>-b}zRCpivZvIBC z3Xp_|w}i^zSnv%5qDd+W&;E{hnw*36sy`Cq6>X42MS8|YDCvqE4nJW$BDik0Q0Ph! zS3UW4VxK1lyG}Gn*`NHCjoh?``jKbEng@8x1KeY!SMe?VfIkeT1{hDl9$!<_emj|- zSC5A)OzpBYbK5H%Qp~W=eYvT;i^}5X$h#7KTiO8a`PViEq{+6Hv-k)N(0KCMeN)&0 z*k_Y+!P+x9a!MEx`uHOeHR7i#$Jcpd0_Rk>90I#LRg*?9l`W&-MoUe5i1RI=!ygG;TqpY?DlY?}=pzwQ zb9Hw>7BMimi(f%P!k%?3cbEPd69VA}DC9&8_|h|=+?s;>N$-OPj{pw!3HsAFj)=b_ znJLI?I{b2IY%J8I1MV|>?TCi>m-Z@C?;R8E->Ke;Dk?#@q9&vyCah<)gIq02iVkPi zetmttt}u+HfEeAEU7_pni>{}{|GgZ)R|#kV`4_Bjhlnw)mJ4h*c?oz@}*{wBz?!fex_E@q6zFWJ!sY1woG}=HtSqNKc z{G&`d!y!!G@xdI>BpZbUF4i8C!^}p)QkJRb=_0fZA8Q_QRgD*$a zMTvd@H-Sq54i=^U%GqC5{#}7&l?fRBO65B2cvCx>PE@`j%vpPFYS{V%5bGN2c)6Dw>Ae=Go~(JxmtDUjp~x42l3$c87zvAR^m`&OhycAp|-VupxKXA@s2Lhn1tiZzy+LxbGwvxD@4UJlRnCHA3W&m!Q)5#*lD;^92?&W@%Z&8Tzo*~fQwt< z6^6c4uqNH$&F_@>^F_1kM!l4$wLg42g}(WxW98d#_hY(AH3!v7MW%&yd!$wiR^Fa7 zhW%N7)zCY(2!Pe(`wkDoM?;g zJO$aJd0dU#OAs_j5=gl2B37nZ($a$+Y67`&nr?i65+V&OrhoiE=+Eiw5M{FkZhq>d z#9$sj@ID<6V7QPTnoUAM!9ZNNWH z$XNE|qzBn~4?<#QA}=A?2gSat`B>`biWTkky#^d9>V+SfM+1f){Mn;*!FP@@pO{1d z;}_4TK#aUSc_cCZ6A3{3KM^SYCuK@X`iZQNFaO#Cc#@(isrH^ec?KvkU*cg!IQ_*TLTBAv*${~&>Lw@2D|=7&Qq zV{ul^?F0q2&Tx*JYymmbQGhBh6npKzv%D>kQ0pRqK-s>DNl=TG%!jE94!|60?Ty*& z$4KcMeSiDzO?ip{a32sU(0a}03we3ly3P*12+|(j47z|ilf*rp;7Pm z^Wz@q1e$G?`4qp-Hul6jow7CglDPy|vVH8PM|FQLlWXmXFJEDTLqxQOfW?re3rK&t z^&$1UVrCK%%3BGeT*kB2_lrhf^h%LY(oAA-ZGL}Y4T^WrP54yGkk_h6XBHxUr=ZgF zI^nEzjGRhs!qtt%!M)hSAWbsb*vVQa0B9i_|NlWU1)X+glvv~hcT%B}{6j4z)?@ka zYANQB!1ALw-}gOHdF%gIabFqM*4DJmIX$I@w1rckSPO;V6nAJ#i@Uo8E0*F;YCtJg zG`N)F?gR*-rFej%K@%*&9fE|w8`|gj{=YxI>)pS0_Rh6s*39fR_uMm!`?|O4T9d;7 za+q`O&bR<>;g)%Gm|;jd4j5YF5SIkJ+pGT0Jjonm#yZ`S5{|+9u8vec;s%eE>yG&l z78r0-#w`uZ<@8safZATgqCc5pHqO<`XX@D>$in*1eD#Ga6ICB$GO9MR?BpG|9XBR- zs=^ZZdN(Vx%CKeQdFR4CIH{UBl>sx=+hjr}JBmx29#h*t*{#(HH24a>(Xu-JX^(qm zL2wF9-MkDJl_1*`0Sog0m-%nnILicoZo)n1xsfpo(s7WMmcXNYdr&GBQ$5{ksi|Vw2-}$}cspplOa2n_^5q9@NVQa+CV;HuG z9!^-97+;4H;{s)_e$i9P%@IB#2EAEVNf8-dM6COYLZa}t&eIKFb3Rrt8jr2N6XKAr zkZ<}XOHWj4BIrkl3I?u;HC$FUzqtK*`ymZSS5BoRi@X_W62x5Y;l1@QKmoIgcTlq_4ZNev;G-thae3};6=eaZt*~9b zVjB$xhhU!xzOQ6N)<6X_5?`9Xz$Wz%$&kc7F-F+_`%-Psreze&-n zGu)i8KIWlsUWIosu1jpjhsgJjCuC}2sF?_KMjDfY@QuKVtXB5NRCeE!VW zmc!UPJdIH7zU1^Aw{MQ-<@QDV!Y+#7VTdtBCZ@8>t)%(dN!&>v9qx&y0NEn}Y@SW; zzu}>F-i!h^&s2VNi}H^A>a!m6X^!9Ahse;$Uu-;eTvQGoLj-r53p zLrGYctCmVVjKd~PrXUaP~6hh$DE701M+t9m3*)O=vhd-EVba@ zlH;;)bowN$P!T?j4t~VKzLMFt{8F=V$>`zf+xc7n1icU@{lM}dI?@k(qNc^yzvL$~ z>^Me~u}0&75>>?fe6jFS_g7^yRiGsL7qHltqC-G4L4Y+nQ9};pY2+830mVr88kfMn zk8d4s?wnUIiRbtU*Ln9-2K=z`?xwQL;up#&@J(k>rUticb7!9;T`U_LvUtdq6 z542g(a{0=YCPsT#o(Iy=nbD}VP5%Vv%cxZ-I1zked$RjHSYa7v4@EJ=6itlWmN4#a z4m=cLEoE8A+|=%O^GNI|55D_jN!K_TkAx1$e&x-!aK|>uWOt%qw&>h*HQ8Q^fw^fkMmwily)vKaN_O|<*ZlaLDpAh(sF^}9m>C8vXnpu{ly`r zkn=>u@O}5jztwl-gWf7PsgqO1Do}NGyMdfEED5WtwbD#}t-ci-ev9erl_FmPywqNf zR>%7ZCs!fvx~wFxWZ3QTm|TP^?Z2BC$d$?D_uf8g_Im|;cDzr2K(57 z5*&>`OCoi4DL~ z2M*7>jEB3}gnTGTnR7yj0Nnh2_#^iQ${X!c%W2da{DklK(%gU8wl26lmn)*mR}bm_ zg~n)^$4IU1Y#YE-TMtYho<%=@&;fk%`87<`Y<<=*C+UcXU(PuBB@dSTA55p2m0A0b zQ=384qD7T*YA^hy93$HpwDef{j@jC;p56M?OW~`)oUgZ1 z@R;shx>ry1xAH1M4C&y`U9zKO#9-2le=py#Rw-K`F)y9-LLlfCt!Q%#{_`?_Nr~k< z^3scY3YG7!V}<;W-Y~LJ;SiNN`$+#pq-e$$6TB6y;^Xkl+U1&(Y=(bqs<}U%&n0g0Qmo&6fBmj!^ zccUA}v{CtobiQvmWtlB3H3{y%a9oEVBekyVMz!rB6{;0^u%^{R?|3VKsxWVlC`YOU zhwJP#`?l%53xA}MsW-m`lrt7BwxCx(SXl|SVR8z?7gKex7x+FY;AVJ$ItId69e?9D8mmJ=xqv}mDt@Fl4WL*mY z)TGops57r_s4~rQ=?kSf6U6N&`#i`CH52;t?b^~B2lGoKxU;iF`0vSntw5E2JsW+# z^Y2lDwT_eF)ExuC;XR^GaCDOkmh=Ry*js>N6urbd8xri;vO5X~O`2X3;x+W2@SIy@ z%5OXu@}=!hx8|M_3|hOu>l22k;__Mof4C_o>d9Wix1dzE0s7-ampaiuWb%blI$Eg2 zW^T$neB}DjOw}@fnRJ1!x80MH^y6&Jf5&zuA#6LwgVV9{6<=dkI&)uM*H>Lhd00$<)?LvNpr^R=tSs>x*9j)p#w5;-}x)d zeEg6Wrx8==Rxd)$nM2lG1%x@BJ1fLLJ0NfNWTBjKXhZW;G9-$T^yk}<$fXM~@84+u zbxXNQ)CMSvJ0Nyi&fbQsID7gHmoy9GeB1~A$z~Rg20&qBt9wUVhc^4c<+D;yH+SHE z5NB-MqWNfb&~ftM3(2I}Cje)qSbiM7b5f8izN>b*wZ~y8{|sw<2hP@%!80W88pLple6*(}G{R z_H}yT(}nQAdsF2Dn;=c2lQP%3WetggF5t=it38r-S|Zr{doSl7VwUpv zmPqm+6yxpv|CW+!{r%tC3zh$tOFX;vUkU=pUsuZ*UTti|n)=^6|MwSjQyM*Q0QpGA zo#FS-b537O?Nun-?^lP_DhPL{jXF@OjEz_86VIps3?uP3hc zo|%nKlt6e(;OB4fRkrrx!skBY?76Ka?Hfi90vB%_&)|MrTy(Tw7>@HLWK1@a+jt0Lb=(Xb}hP@6iD)P zUpY1xy1r^g+B4Xa!Dr)P#S7#P0&~xOsnP#>{p(Qo(u0NO5tTQcf0NvPsYS|>wzo>; z3==MrnOETu$>V@lC{st!cLRt+dYrwqR42vG_~|Tjj95u`3d3ncSU?*bqevKTf`(kP zZ)|w^1MuNFK78zrpqy0m+4M9sS9(fh@uvhf=KCniLkFzNgwycysA^DGIn|99F{K6) zG2$Wfl?5(%#K%Z&Pnh&FNBHZk=ZTz+GV_PeC9`4a*^B?UR%uzeu z3{}`=NUBwFxzTlhT9~ALfT%25->$p%zdG5)$;e=v@9sISGV_#T@}^e~K27Gj9RO{> z$M38o8mPPI$vzRS@YSPv9J#sh7)?w>K@QqXu^_g9A9l2g)l6~5jei!sZO8e_8w1Do ztFSzvLXk2SC8fQ8XT)QFYgFwp;FYjXrQ5P-gN{UPt_9Zh2k z|0Y}SDWQ3Tgk)`dI+47!;1ao7F9{8U5yUEM;m|{{efR;lr$qqRy}O<+w*P1U=!b@J zTcp=%Wr$%j5;m)opVHE7fge8>JX;nE5{JEBY=7=KP~#K`oVXV~xE`_Qj2048I*Rrk zhzL*ytjkRHFfxr+va4olNMtibSjlX^KH~LtYCJRNocL-?IrmLRxLaNchnRF??KKDt zbM7h%ABAUn!cA&vPWQhy#`DuHcbzry;eF>l1~%S_M)z`01TYrxFP&=8%=9!;#wn?t zv{H?m)DLktw@|RhTR${r(A-44W{ukLfftUrUSrZ-o9K|h8hfaUt)&Zlrs#(u$Ty9> zD?5Z?H;xRIx8Bac@+v)#Rss^A1aihR?63K#N-_>kQ?JTMIf!&7!^);>x3mlkzR@sv zI?LGLI)NYsGZhCJx96^D^>lIhN?Uy+d)>UinEWpcLvcViwnndndU7x56B3d<;)=Ce zWWbd8)0{;mS*zJ>2EH*mvk>_CCo~%7pom;BI;m2Vrv98t&qc_6latSOdRiLrd;>ux zGv^KjQ2GOd1EPj5s*JV{%i?=Sr`OTB6lrt|pI{SEB)SH1=Cz#2+}Ids=*xr7WODM# zo{o^G8}Fh^d;gcr?sw6bKT2L-e0?i6oNWwPD)}7R8wkGn-En*^V**!CO@^L?c!P+h5?tKkiX}ZkjTFf;7Ce!0$f@xQF zz;kw#=-ry~65j#8oj97q7TJ0*J{0fMr`f(g?EGfi+&3j6ecYa|LLGb}@4lXFOG6lQ z!qI2O7)gmcwVrn3nH2AukqFO0e0VYTvqxLp15~H~mLbt_`bcONqt!w!Ep4n=4^ZOa$Zc;c13;K192OKNox^=vYF9Prwd;_u-_n+Lj z1!~XJDOYF<{&XBZwEEqE7@ixNi8DrbUMW+ClK3}@6+gI?C`G)fHRC}>Ay2AU+lNIH zQqu$Th^@DH7;D8u$X7o9L^NN31xlPx(VZi7aY98LeKZxWTc-ZS*`U69tf1F^*=cL1 zSeRAq;FiECuzr8xFhA=Za#jdybh~$t0g4e-HuqB4{1H3r}~tV@z7?T z9j`1OsVHwcK03Ufffj9Pm8mW~ASVca>2rE)r$>lbX3gJwN<|*3gN8Z(#j0ehXOezT zpjw7avIs|N*odx$A9M$7CILwqoON91_lo8FU2K_GjeKMI>id?LHwzbeXaMkP6}kI< zF)$lF-&Qtx3xQlXb*+ zl$c~q8}i3vz`FaQWAwd}FB)pn-rIeLfnzn#+tN~`#ZTLh57&PJ(WNEkyV?qUUAJty z^bdv?7Xg%IhTSsmGs7}jd72xGvyxRqCWgf|W`-@zbJBI%`H~<>8T{R;RP7n5v9NMn z1ro&!+mT8@H7TxiSNBZl<_UetU}6Egs!CLz9tL~0u2k(lmbiJ{W;CO3$rV3Rt#J3OZotz!&9m7L(I16|uKP?#Q3%27-^Xc1j~c zbSd?ib}}~*S-YBbq=&jH6lpuJx(AvUeBs3*lq3F3a&4WjM~OPwmV!8S2&P z8yLuv7Q*mZ5qL}HmLp2Auf{Kmf8 zivuG4PHkdNG~JenivUZHo(QvMS^r-DT4wy(ce*oYRGmA`4}I*{g@CP0W`_bY?Peag zmOl5JruJq|9R`8*aPxCSd2b=-Kyi-)I~jw98u%GsenB`#Spzw=^3)}C4$F#?ExH%O zls%N5HhNrlj^qX%dY%8kg!sBK5Uoza!I;7AT&=T_Of+edV!27Ng8o|0VLfc6M1}KQ z(12lI;#&vj8aoFlQXqMnNV#RMe8E*iPUpL4$xgPZTTesXOK7o0K&fM9J<@BbaiVWN zLH;2|7>PhF1t?J8U^m|z11U(0l&4yoYit1OYTEuX>z?!|+sVA7kvLGo!x;yfqS8XY zCjjFUUv7TezEycS8!%UR=)3XyIqV8Sk-wL@%DFl-ovSS|bTq&GbJPB73Ob`dKgdov zq%Oa(RH*)YOhpH3u~1f|&F{E@r`G4d9q|^Zz8>?!iATPLmv8A#LxnTQe_&R*aA}2L zZ4>giShxqXM-wd(vD|Gl9@x6ow@7G+?KH#&EiLH=>Yy#A4Ln-~m?0Tve!7&;eu16d zYVsj@(czRy9K^OUsC zysBwFY+54<%T$TnOJ+XBJwN8S6_D^arKz%q zl)RpdGiioG93|CdrgR)U>TEIT+F11RHAi%qrz|5#xv^uaX%5Q?EK(P;X+b(UTvSW| zX1=)7Xj-JV?RTuHb`z0*<`#27tl4cbH>fv%s4l%xQ06s~F!fYYyJet{6%L_{O%t{2 zj9=?MBSnZj;$+g4zWe}Dkl~}cfG1Hx68#Rb>*y9~esr z?2Rcx9(V@@mFPk^S{n2#Z_l(gbebLP3&!LF76#p=Vy*BwYsQAsYfRh|xLbtiJum$21V8z}CSQ@Yu{f&PAgp1W)S?amyUQzRii` zG2cz)wR`_`3Oeu@cpf*|w;Lox7`aUB`_vC0Vb!uTA2avgJ!N@#Xcf+WPaS6P2J@{q zcG`(Tsg}9PRsWN&$Tj#VVVr*u*57z=Z|WkZ#ZR!!53V8|bhL;D0BVKJD$U;v2-X+6 zj_*YqD|#(`p?a*bO1U8^vjGtov>;do=q8CD^kzCAF6i~~qLz6`{ipg^d}U9gvR=(A z5WYM=g{t6R%dRYx<=8`!S3sIp3r)ed8$K-(q7-gs2b=L(Hn2U$e@6w4^c|(vPuWJ>SfX^l!?Fuk>6I!Ptp0`3!&9vWNe$DUz@4%Q^^T3j0fbu(yi7t){8@yS!QYMfqe#i)_+L zuA^R(GhR!%FNdPPkdao^?XxI3$6Fy&BkiV)>{HOT z%VQ@4UT2x6spB&yKGm`O}l!2MqrddNT-UC516`y?0vl|WCZf??l+S0YJav$VGogBrrsV>4SAHwWi z24&}}p&`8=db4##9P`!Gzx7r5VGInoU7x#XWEBs;h(FXsY;!-8&qXldE zx2V|Hug|mV$<;6DiQ86Cv)}WxsydxP0#J`j5Q#Mh)wI4CSQ=_4;74UD=kFJ(d9F#9 zLmSb44Nf>et_o*_T?0Dr@X{`Ub!MGmriM4Z%H20#6IWgsp$PglRd6#De@hH0E3b zFe9hFS^hrw8v*$w7ubAyWZlK;6+Zo3=oZH_!dRH@!WXE{+uGO+QONDxMdVNL&6NHT z%9fwZgjq8t5#0unv_C_yp=L15wHY^p_GY*Ay9v(SE(u%KtI@~s$sgC$W)~9F~ ze&S1Rvp}kye-T0}I{I5OVo*WmHsK~ukfwSt;fd$JMbZcBKhccT;RumEMX>36x8DHe zTR}Zz&px%OIt&ytjxND++NTN3uD#AY*-t(S0n(pVexuO=`8SVS<~2YVaHGv0(RP6L zH)lb=%&PqNoEMiI2H;edLR6aPD~B~c+s4E7eMUyR67CXE0+izDH+wqACdpsRz7OcU zedPh1G*XF>eVNoTw$#aJ6zlIiQT!-U;4gIolquDk4;;$V zlqvYiOMy>a&c@HjH%DWWLB+>^cPtHC2nfwH$e;dr;;ACK*1_EnyZxIa9uiivFN}=uAZam7+bf;~pjU9}&?k(cs?jkvrZTMAZs*{z2q7 zB}**i0>AO2r1>9Ft&DXxwzX=qeamwK^pLtQFYXmNM(fP8V84tO{^ zn*A@JV=Y$`ec_;O2rx)<{%rw$x)W4{?d`})M^i`kTke5#bKpz{pE?^XqvYPm>A`lw zS(j#^!#QJAMkKyaBT01%fBMMdFm=U8=46aC)rOAtI$3(aIP6upeOX+hXYYcNQ0YLT zyc}`atX7G|*WXuuW{xk;fSq~pAhCei)o2X*(oyvOZ&gVuEhGBLpwoW^4p4fD%S64s zcp3e41k9fTGxvBZ1?LdCI9u;&+392JCIsD;IQv%J?2k$VMD$yI90T1AcHc7>v5U&m z!YWltFen!kww};?{xiCs(`weqCuF>-?v`<;3j62k9gD>6WD5~fs75r2$x~g=0{gV2J)N|c`>>M*@6PD7mnW*EOeFbJ zo)EiKMSG(7Xx0(ps$7z+u#@`3UA8TNb@h3zP07=OlW~jlXn}~REHb0`7Rkg@R`ox_ zse{S34>^^+@(NE4)TV6D4@V9V(~&R6G+o92r68_RB#!TS?a7(#IrLf5b9lF-;?NdW z3_1JpLinb;0zzlBTj{JZStD$ebEHq7PPaG6j-vxo;NmR&r+pOWT@INQEi^|h^*;#- zOu=mUpB7UjQOZDZ4o}?c=H9C(A$jZZO7lS)>zsXF2#Qf{HJr#zjSWTQ zA_@`ZslQ3I9Q67ZuSX9@KHj1_`uh!4$yJ(-LD2B(HYBwE`tQ*AV9$F^5p&vdp?`Sv pR>H*G`tQ^K+Z;-J5!K@5wPrx~L`RkV@zo`iUunFocwzPae*nZ+6&e5l diff --git a/docs/guides/create-webshop.md b/docs/guides/create-webshop.md index 359124319..974ae1c1e 100644 --- a/docs/guides/create-webshop.md +++ b/docs/guides/create-webshop.md @@ -21,11 +21,11 @@ Get the latest version of the [Orchard Core Commerce NuGet](https://www.nuget.or ### Step 2 — Enable necessary features -Certain Orchard Core Commerce features, as well as some prerequisite stock Orchard Core features, need to be enabled for the necessary functionality. This can be done under _Configuration > Features_. These features include the following: +Certain Orchard Core Commerce features, as well as some prerequisite stock Orchard Core features, need to be enabled for the necessary functionality. This can be done under _Configuration > Features_, but if you used the _Orchard Core Commerce - Development_ setup recipe then they are already enabled. These features include the following: - **Orchard Core Commerce - Core**: Registers core components of Commerce features. - **Orchard Core Commerce - Payment** Provides the basics for online payment. -- **Orchard Core Commerce - Payment - Stripe**: Provides online payment implementation using Stripe. +- **Orchard Core Commerce - Payment - Exactly**: Provides online payment implementation using Exactly. - **Orchard Core Commerce - Session Cart Storage**: Provides shopping cart functionality. - **Widgets**: Allows rendering widgets in zones. - **Layers**: Allows rendering widgets across pages based on conditions. @@ -106,17 +106,17 @@ _Any content type with the Widget stereotype can be added this way, but let's ju _Name the Widget and select the Layer where it should be rendered._ -### Step 6 — Enable a payment provider (Stripe) +### Step 6 — Enable a payment provider (Exactly) -Having Products and being able to browse them is great and all, but customers will also need a way to checkout and pay for their cart's content. This is where payment providers come into the picture. For simplicity's sake, we'll use Stripe as the payment provider here. Ensure the **Orchard Core Commerce - Payment - Stripe** feature is enabled, then go to _Configuration > Commerce > Stripe API_. The Publishable Key and Secret Key fields need to be filled in for the Stripe Payment form to work on the checkout page, see the links on the settings page for more. +Having Products and being able to browse them is great and all, but customers will also need a way to checkout and pay for their cart's content. This is where payment providers come into the picture. We will use Exactly, the default built-in payment provider. Ensure the **Orchard Core Commerce - Payment - Exactly** feature is enabled, then go to _Configuration > Commerce > Exactly API_. The Project ID and API Key fields need to be filled in sp payments are directed towards your account. -![Stripe settings.](../assets/images/create-webshop/step-6/stripe-settings.png) +![Exactly settings.](../assets/images/create-webshop/step-6/exactly-settings.png) -_If you don't already have the necessary API keys, follow the links on the page._ +_See the links on the settings page and the [feature documentation](../features/exactly-payment.md) for info if you haven't got your keys yet._ -With that done, the Stripe Payment form now appears on the checkout page and the purchase can be completed. +Once you're saved the settings, feel free to click on the _Verify currently saved API configuration_ to test your API access. With that done, the _Pay with exactly_ button now appears on the checkout page which redirects the customer to the payment processor's site and then if everything went well sends them back to the success page. -![Stripe Payment.](../assets/images/create-webshop/step-6/stripe-form.png) +![Exactly Payment.](../assets/images/create-webshop/step-6/exactly-form.png) _This will allow customers to pay to their card's content._ 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 48d5831f9..dbb697948 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 @@ -66,7 +66,6 @@ "OrchardCore.Commerce.Payment.DummyProvider", "OrchardCore.Commerce.Payment.Exactly", - "OrchardCore.Commerce.Payment.Stripe", "OrchardCore.Commerce.Promotion", "OrchardCore.Commerce.Inventory", From c4cd964231e4427d03d4bd07651e47f0c7d30f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 18:45:47 +0200 Subject: [PATCH 80/90] Add note in guide regarding Stripe. --- docs/guides/create-webshop.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guides/create-webshop.md b/docs/guides/create-webshop.md index 974ae1c1e..618f2eb15 100644 --- a/docs/guides/create-webshop.md +++ b/docs/guides/create-webshop.md @@ -108,6 +108,8 @@ _Name the Widget and select the Layer where it should be rendered._ ### Step 6 — Enable a payment provider (Exactly) +> ℹ If Exactly is not available in your region [try Stripe instead](../features/exactly-payment.md), the other payment provider OCC has built-in integration for. + Having Products and being able to browse them is great and all, but customers will also need a way to checkout and pay for their cart's content. This is where payment providers come into the picture. We will use Exactly, the default built-in payment provider. Ensure the **Orchard Core Commerce - Payment - Exactly** feature is enabled, then go to _Configuration > Commerce > Exactly API_. The Project ID and API Key fields need to be filled in sp payments are directed towards your account. ![Exactly settings.](../assets/images/create-webshop/step-6/exactly-settings.png) From ef9e5322c112ba99fd4a06323de850a1639dbcf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 18:46:40 +0200 Subject: [PATCH 81/90] Link to the documentation portal in readme. --- Readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Readme.md b/Readme.md index 67ac4f271..8a2b1acb3 100644 --- a/Readme.md +++ b/Readme.md @@ -56,6 +56,8 @@ If you have [Lombiq Analyzers](https://github.com/Lombiq/.NET-Analyzers) include ## Documentation +Check out the complete documentation portal here: + - [Inventory](docs/features/inventory.md) - [Products and Prices](docs/features/products-and-prices.md) - [Promotions](docs/features/promotions.md) From 5797a9329bb9c9067733d1120fb792b9d7450aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 19:34:24 +0200 Subject: [PATCH 82/90] Fix formatting in mkdocs --- docs/features/exactly-payment.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/features/exactly-payment.md b/docs/features/exactly-payment.md index 21acbf715..45445ec6f 100644 --- a/docs/features/exactly-payment.md +++ b/docs/features/exactly-payment.md @@ -36,6 +36,7 @@ There are available test cards for the sandbox environment that can be found in ### Technical overview As mentioned above, this module uses redirects to communicate with the payment processor. This means the OrchardCore.Commerce site never sees the buyer's payment information, which avoids potential liability and improves buyer confidence. Here is a broad overview of what happens when you click on the _Pay with exactly_ button: + - JS script sends a POST request that only contains the contents of the checkout page (i.e. addresses). - C# backend creates a new Order content item from the checkout data and the stored shopping cart. - C# backend sends a POST request to the Exactly API including the order total and the return URL. From 085834764e2d48222f3a75eda07455dcde39d82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 16 Apr 2024 19:36:39 +0200 Subject: [PATCH 83/90] Add missing cross links. --- Readme.md | 1 + docs/features/payment-providers.md | 1 + 2 files changed, 2 insertions(+) diff --git a/Readme.md b/Readme.md index 8a2b1acb3..6d177d30f 100644 --- a/Readme.md +++ b/Readme.md @@ -62,6 +62,7 @@ Check out the complete documentation portal here: Date: Tue, 16 Apr 2024 19:49:49 +0200 Subject: [PATCH 84/90] Update nav to include the missing features/payment-providers.md --- mkdocs.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 8e0abdcef..b87f8d116 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,8 +80,10 @@ nav: - Inventory: features/inventory.md - Products and Prices: features/products-and-prices.md - Promotions: features/promotions.md - - Exactly Payment: features/exactly-payment.md - - Stripe Payment: features/stripe-payment.md + - Payment Providers: + - Overview: features/payment-providers.md + - Exactly Payment: features/exactly-payment.md + - Stripe Payment: features/stripe-payment.md - Taxation: features/taxation.md - User Features: features/user-features.md - Workflows: features/workflows.md From cbfcf8363018d2aed3c6f966a90972dcb49fcffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 17 Apr 2024 16:31:04 +0200 Subject: [PATCH 85/90] taxation clarification --- docs/features/taxation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/taxation.md b/docs/features/taxation.md index 0a81a9e6e..5ee6e2c96 100644 --- a/docs/features/taxation.md +++ b/docs/features/taxation.md @@ -11,7 +11,7 @@ Enable the _Orchard Core Commerce - Tax_ feature. This grants you the _Tax_ cont ## Basic tax support -Filling out the _Gross Price_ and _Tax Rate_ (percentage) fields, automatically updates the product's regular price field to the calculated net price during publish. For products configured like this the shopping cart shows the _Gross Price_ instead of the _Price_ field. This is suitable for stores that only ship locally. +Filling out the _Gross Price_ and _Tax Rate_ (percentage) fields, automatically updates the product's regular price field to the calculated net price during publish. For products configured like this the shopping cart shows the _Gross Price_ instead of the _Price_ field (so it only works with _Price_ part and not the _PriceVariant_ part). This is suitable for stores that only ship locally. ## Locally maintained tax rates From 601961bfdeb74a4387f8ed4bd48d547c449d48e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 17 Apr 2024 16:51:00 +0200 Subject: [PATCH 86/90] =?UTF-8?q?Add=20=C2=AE=20and=20fix=20formatting.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Readme.md | 2 +- docs/features/exactly-payment.md | 5 +++-- mkdocs.yml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Readme.md b/Readme.md index 6d177d30f..85f52da9c 100644 --- a/Readme.md +++ b/Readme.md @@ -62,7 +62,7 @@ Check out the complete documentation portal here: Date: Wed, 17 Apr 2024 16:53:40 +0200 Subject: [PATCH 87/90] Also in the button. --- docs/guides/create-webshop.md | 4 ++-- .../Views/CheckoutExactly.cshtml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/create-webshop.md b/docs/guides/create-webshop.md index 618f2eb15..ea206f386 100644 --- a/docs/guides/create-webshop.md +++ b/docs/guides/create-webshop.md @@ -106,11 +106,11 @@ _Any content type with the Widget stereotype can be added this way, but let's ju _Name the Widget and select the Layer where it should be rendered._ -### Step 6 — Enable a payment provider (Exactly) +### Step 6 — Enable a payment provider (Exactly®) > ℹ If Exactly is not available in your region [try Stripe instead](../features/exactly-payment.md), the other payment provider OCC has built-in integration for. -Having Products and being able to browse them is great and all, but customers will also need a way to checkout and pay for their cart's content. This is where payment providers come into the picture. We will use Exactly, the default built-in payment provider. Ensure the **Orchard Core Commerce - Payment - Exactly** feature is enabled, then go to _Configuration > Commerce > Exactly API_. The Project ID and API Key fields need to be filled in sp payments are directed towards your account. +Having Products and being able to browse them is great and all, but customers will also need a way to checkout and pay for their cart's content. This is where payment providers come into the picture. We will use Exactly®, the default built-in payment provider. Ensure the **Orchard Core Commerce - Payment - Exactly** feature is enabled, then go to _Configuration > Commerce > Exactly API_. The Project ID and API Key fields need to be filled in sp payments are directed towards your account. ![Exactly settings.](../assets/images/create-webshop/step-6/exactly-settings.png) diff --git a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml index 3c6c967ef..7f7b7717f 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Exactly/Views/CheckoutExactly.cshtml @@ -9,7 +9,7 @@ T["An error has occurred while trying to connect to the payment service. Please try again later."]; } - +