diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets index d9842b22c9..080c78d497 100644 --- a/SmartStoreNET.Tasks.Targets +++ b/SmartStoreNET.Tasks.Targets @@ -39,7 +39,7 @@ x86 $(BUILD_NUMBER) - 3.2.1 + 3.2.2 $(StageFolder) .$(Version) diff --git a/changelog.md b/changelog.md index 6078e44583..81357cca18 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,4 @@ -# Release Notes +# Release Notes ## SmartStore.NET 3.2.2 @@ -6,6 +6,12 @@ * External category links: alternative links for categories in the main menu and in category listings. * Page Builder: multistore support for stories. * #1639 Shipping.ByTotal: Add option to calculate the shipping rate based on the net total instead of the gross total. +* #1537 Implement "Ratenzahlung powered by PayPal" provider. +* New Heidelpay payment plugin. +* Shop-Connector: mapping of product SKUs that differ between client shops. +* MegaMenu supports configuration of menu items + + ### Improvements * Amazon Pay: PSD2. Strong customer authentication (SCA) upgrade. * Updated *MaxMind.GeoIP* library and country database @@ -15,15 +21,25 @@ * (SEO) Activated GZIP compression for all dynamic and static responses. * Set max. file upload size to 2 GB (globally) * (Dev) Added a sample block for the *Page Builder* in DevTools plugin. -* Various other minor improvements +* Various other minor improvements. +* PayPal PLUS: paying by instalments can be selected in payment wall. +* Payone: send more information to API (e.g. shipping address) to stay as frictionless as possible for PSD2. +* PageBuilder: Added keyboard shortcuts +* #1582 Added structured data to reviews +* BMEcat: added support for import of specification attributes ### Bugfixes * Fixed wrong XML sitemap index numbering. * #1646 Setting ManufacturerItemsToDisplayOnHomepage to 1 will not show any manufacturer. * #1647 All manufacturers page: link of last character missing in alphabetical letter list. * #1664 RTL: CMS menu issues in backend. +* #1677 Page Builder: moving and resizing blocks via drag & drop didn't work correctly in RTL. * Do not adjust inventory twice when an order is cancelled and deleted. -* Various other minor fixes +* #1500 Fallback to fixed VAT rate instead of rate by region for countries that are not subject to vat. +* Fixed "The entity wrapper stored in the proxy does not reference the same proxy" when copying an export profile. +* Various other minor fixes. +* BMEcat: set TaxCategoryId & MainPictureId for newly imported products + ## SmartStore.NET 3.2.1 diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs index a5c489371d..719cd1d6ff 100644 --- a/src/AssemblyVersionInfo.cs +++ b/src/AssemblyVersionInfo.cs @@ -9,7 +9,7 @@ // // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("3.2.1.0")] +[assembly: AssemblyVersion("3.2.2.0")] -[assembly: AssemblyFileVersion("3.2.1.0")] -[assembly: AssemblyInformationalVersion("3.2.1.0")] +[assembly: AssemblyFileVersion("3.2.2.0")] +[assembly: AssemblyInformationalVersion("3.2.2.0")] diff --git a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs index 06449caef6..db54bf8936 100644 --- a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs +++ b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs @@ -31,7 +31,7 @@ public TrimmedBuffer(string collection, T newItem, int maxSize) public TrimmedBuffer(IEnumerable collection, T newItem, int maxSize) { - Guard.IsPositive(maxSize, nameof(maxSize)); + Guard.IsTrue(maxSize >= 0, nameof(maxSize)); _maxSize = maxSize; _list = new List(collection ?? Enumerable.Empty()); diff --git a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs index c6d22dd2e0..f516fc53da 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs @@ -88,12 +88,25 @@ protected set public ScheduleTask Clone() { - return (ScheduleTask)this.MemberwiseClone(); + var task = new ScheduleTask + { + Name = Name, + Alias = Alias, + CronExpression = CronExpression, + Type = Type, + Enabled = Enabled, + StopOnError = StopOnError, + NextRunUtc = NextRunUtc, + IsHidden = IsHidden, + RunPerMachine = RunPerMachine + }; + + return task; } object ICloneable.Clone() { - return this.MemberwiseClone(); + return this.Clone(); } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs index 1d79ba018b..28fd36b269 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs @@ -12,7 +12,8 @@ public TaxSettings() DisplayTaxSuffix = true; PricesIncludeTax = true; EuVatAllowVatExemption = true; - ShowLegalHintsInFooter = true; + ShippingPriceIncludesTax = true; + ShowLegalHintsInFooter = true; ShowLegalHintsInProductDetails = true; ShowLegalHintsInProductList = true; AuxiliaryServicesTaxingType = AuxiliaryServicesTaxType.SpecifiedTaxCategory; diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs index d3615b73e7..8a934935e0 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using Autofac; @@ -26,7 +27,8 @@ public IContainer Container get { return _container; } } - public T Resolve(object key = null, ILifetimeScope scope = null) where T : class + [DebuggerStepThrough] + public T Resolve(object key = null, ILifetimeScope scope = null) where T : class { if (key == null) { @@ -36,22 +38,26 @@ public T Resolve(object key = null, ILifetimeScope scope = null) where T : cl return (scope ?? Scope()).ResolveKeyed(key); } - public T ResolveNamed(string name, ILifetimeScope scope = null) where T : class + [DebuggerStepThrough] + public T ResolveNamed(string name, ILifetimeScope scope = null) where T : class { return (scope ?? Scope()).ResolveNamed(name); } - public object Resolve(Type type, ILifetimeScope scope = null) + [DebuggerStepThrough] + public object Resolve(Type type, ILifetimeScope scope = null) { return (scope ?? Scope()).Resolve(type); } - public object ResolveNamed(string name, Type type, ILifetimeScope scope = null) + [DebuggerStepThrough] + public object ResolveNamed(string name, Type type, ILifetimeScope scope = null) { return (scope ?? Scope()).ResolveNamed(name, type); } - public T[] ResolveAll(object key = null, ILifetimeScope scope = null) + [DebuggerStepThrough] + public T[] ResolveAll(object key = null, ILifetimeScope scope = null) { if (key == null) { @@ -130,7 +136,8 @@ private bool TryResolveAll(Type[] types, out object[] instances, ILifetimeScope } } - public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance) + [DebuggerStepThrough] + public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance) { instance = null; @@ -144,7 +151,8 @@ public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instan } } - public bool TryResolve(ILifetimeScope scope, out T instance) + [DebuggerStepThrough] + public bool TryResolve(ILifetimeScope scope, out T instance) { instance = default(T); @@ -178,6 +186,7 @@ public T InjectUnsetProperties(T instance, ILifetimeScope scope = null) return (scope ?? Scope()).InjectUnsetProperties(instance); } + [DebuggerStepThrough] public ILifetimeScope Scope() { var scope = _container.Resolve().GetLifetimeScope(null); diff --git a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs index 18dcfc4222..0029454b28 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using System.Web.Mvc; @@ -148,6 +149,7 @@ public bool IsFullyInitialized set; } + [DebuggerStepThrough] public T Resolve(string name = null) where T : class { if (name.HasValue()) @@ -157,6 +159,7 @@ public T Resolve(string name = null) where T : class return ContainerManager.Resolve(); } + [DebuggerStepThrough] public object Resolve(Type type, string name = null) { if (name.HasValue()) @@ -166,7 +169,7 @@ public object Resolve(Type type, string name = null) return ContainerManager.Resolve(type); } - + [DebuggerStepThrough] public T[] ResolveAll() { return ContainerManager.ResolveAll(); diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs index 30c8af9046..e5bc5e5e48 100644 --- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs +++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs @@ -45,7 +45,8 @@ public static class SmartStoreVersion new Version("3.1"), new Version("3.1.5"), new Version("3.2"), - new Version("3.2.1") + new Version("3.2.1"), + new Version("3.2.2") }; private const string HELP_BASEURL = "https://docs.smartstore.com/display/"; diff --git a/src/Libraries/SmartStore.Core/WebHelper.cs b/src/Libraries/SmartStore.Core/WebHelper.cs index aff6e9b188..3a94e1a417 100644 --- a/src/Libraries/SmartStore.Core/WebHelper.cs +++ b/src/Libraries/SmartStore.Core/WebHelper.cs @@ -91,29 +91,43 @@ public virtual string GetCurrentIpAddress() }; string result = null; + IPAddress ipv6 = null; foreach (var key in keysToCheck) { var ipString = vars[key]; - if (ipString.HasValue()) + + if (!string.IsNullOrEmpty(ipString)) { var arrStrings = ipString.Split(','); - // Take the last entry - ipString = arrStrings[arrStrings.Length - 1].Trim(); - IPAddress address; - if (IPAddress.TryParse(ipString, out address)) - { - result = ipString; - break; - } + // Iterate list from end to start (IPv6 addresses usually have precedence) + for (int i = arrStrings.Length - 1; i >= 0; i--) + { + ipString = arrStrings[i].Trim(); + + if (IPAddress.TryParse(ipString, out var address)) + { + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + ipv6 = address; + } + else + { + result = ipString; + break; + } + } + } } } - if (result == "::1") - { - result = "127.0.0.1"; - } + if (string.IsNullOrEmpty(result) && ipv6 != null) + { + result = ipv6.ToString() == "::1" + ? "127.0.0.1" + : ipv6.MapToIPv4().ToString(); + } return (_ipAddress = result.EmptyNull()); } diff --git a/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.Designer.cs new file mode 100644 index 0000000000..be8c91d1d2 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")] + public sealed partial class V322Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V322Resources)); + + string IMigrationMetadata.Id + { + get { return "201908150749388_V322Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs new file mode 100644 index 0000000000..12a38b031e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs @@ -0,0 +1,69 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class V322Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Languages.NoAvailableLanguagesFound", + "There were no other available languages found for version {0}. On translate.smartstore.com you will find more details about available resources.", + "Es wurden keine weiteren verfgbaren Sprachen fr Version {0} gefunden. Auf translate.smartstore.com finden Sie weitere Details zu verfgbaren Ressourcen."); + + builder.AddOrUpdate("Checkout.OrderCompletes", + "Your order will be completed.", + "Ihre Bestellung wird abgeschlossen."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.CheckoutAttributes.Fields.TextPrompt", + "Text prompt", + "Text Eingabeaufforderung", + "Specifies the prompt text.", + "Legt den Text zur Eingabeaufforderung fest."); + + builder.AddOrUpdate("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Fields.TextPrompt", + "Text prompt", + "Text Eingabeaufforderung"); + + builder.AddOrUpdate("Admin.Catalog.Categories.Fields.ExternalLink", + "External link", + "Externer Link", + "Alternative external link for this category in the main menu and in category listings. For example, to a landing page that contains a back link to the category.", + "Abweichender, externer Verweis fr diese Warengruppe im Hauptmen und in Warengruppen-Listings. Z.B. auf eine Landingpage, die einen Rckverweis auf die Warengruppe enthlt."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Title", + "Title", + "Titel", + "Specifies the title. Please keep in mind that this title is not displayed in all design templates.", + "Legt den Titel fest. Bitte beachten Sie, dass dieser Titel nicht bei allen Design-Vorlagen ausgegeben wird."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.InvalidRouteValues", + "Please check the link data. No link can be created on the basis of your input.", + "Bitte berprfen Sie die Link-Daten. Auf Basis Ihrer Eingabe kann kein Link erzeugt werden."); + + builder.AddOrUpdate("Admin.Packaging.IsIncompatible", + "The package is not compatible the current app version {0}. Please update Smartstore.NET or install another version of this package.", + "Das Paket ist nicht kompatibel mit der aktuellen Programmversion {0}. Bitte aktualisieren Sie Smartstore.NET oder nutzen Sie eine andere, kompatible Paket-Version."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.resx b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.resx new file mode 100644 index 0000000000..a3562fc0f2 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index 7af454f8db..18a610b862 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -50,29 +50,7 @@ public void MigrateSettings(SmartObjectContext context) public void MigrateLocaleResources(LocaleResourcesBuilder builder) { - builder.AddOrUpdate("Admin.Configuration.Languages.NoAvailableLanguagesFound", - "There were no other available languages found for version {0}. On translate.smartstore.com you will find more details about available resources.", - "Es wurden keine weiteren verfügbaren Sprachen für Version {0} gefunden. Auf translate.smartstore.com finden Sie weitere Details zu verfügbaren Ressourcen."); - builder.AddOrUpdate("Checkout.OrderCompletes", - "Your order will be completed.", - "Ihre Bestellung wird abgeschlossen."); - - builder.AddOrUpdate("Admin.Catalog.Attributes.CheckoutAttributes.Fields.TextPrompt", - "Text prompt", - "Text Eingabeaufforderung", - "Specifies the prompt text.", - "Legt den Text zur Eingabeaufforderung fest."); - - builder.AddOrUpdate("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Fields.TextPrompt", - "Text prompt", - "Text Eingabeaufforderung"); - - builder.AddOrUpdate("Admin.Catalog.Categories.Fields.ExternalLink", - "External link", - "Externer Link", - "Alternative external link for this category in the main menu and in category listings. For example, to a landing page that contains a back link to the category.", - "Abweichender, externer Verweis für diese Warengruppe im Hauptmenü und in Warengruppen-Listings. Z.B. auf eine Landingpage, die einen Rückverweis auf die Warengruppe enthält."); } } } diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs index 498ee9031c..25ec8fc33c 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs @@ -9050,6 +9050,77 @@ public IList CategoriesSecondLevel() #region category definitions + #region new + + var categoryFashionJackets = new Category + { + Name = "Jackets", + Alias = "Jackets", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_jackets.jpg"), "image/png", GetSeName("Jackets")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Jackets", + ShowOnHomePage = true + }; + + var categoryFashionLeatherJackets = new Category + { + Name = "Leather jackets", + Alias = "Leather jackets", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_leather_jackets.jpg"), "image/png", GetSeName("Leather jackets")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Leather jackets", + ShowOnHomePage = true + }; + + var categoryFashionShoes = new Category + { + Name = "Shoes", + Alias = "Shoes", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_shoes.png"), "image/png", GetSeName("Shoes")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Shoes", + ShowOnHomePage = true + }; + + var categoryFashionTrousers = new Category + { + Name = "Trousers", + Alias = "Pants", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_trousers.png"), "image/png", GetSeName("Trousers")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Trousers", + ShowOnHomePage = true + }; + + var categoryFashionTracksuits = new Category + { + Name = "Tracksuits", + Alias = "Tracksuits", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_tracksuit.png"), "image/png", GetSeName("Tracksuits")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Tracksuits", + ShowOnHomePage = true + }; + + #endregion + + + var categorySportsGolf = new Category { Name = "Golf", @@ -9179,7 +9250,8 @@ public IList CategoriesSecondLevel() var entities = new List { categorySportsSunglasses,categorySportsSoccer, categorySportsBasketball,categorySportsGolf, categoryBooksSpiegel, categoryBooksCookAndEnjoy, - categoryGamingAccessories, categoryGamingGames + categoryGamingAccessories, categoryGamingGames, categoryFashionJackets, categoryFashionLeatherJackets, categoryFashionShoes, categoryFashionTrousers, + categoryFashionTracksuits }; this.Alter(entities); @@ -9197,11 +9269,37 @@ public IList Manufacturers() //var categoryTemplateInGridAndLines = // this.CategoryTemplates().Where(pt => pt.Name == "Products in Grid or Lines").FirstOrDefault(); - //categories + // + + #region Jack Wolfskin + + var manufacturerJackWolfskin = new Manufacturer + { + Name = "Jack-Wolfskin", + ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "manufacturer_Jack_Wolfskin.png"), "image/png", GetSeName("Jack Wolfskin")), + Published = true, + DisplayOrder = 1 + }; + + #endregion + + #region Mey & Edlich + + var manufacturerMeyAndEdlich = new Manufacturer + { + Name = "Mey-And-Edlich", + ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "manufacturer_MeyAndEdlich.jpg"), "image/png", GetSeName("Mey Edlich")), + Published = true, + DisplayOrder = 1 + }; + #endregion + #region EA Sports - var manufacturerWarnerHome = new Manufacturer + var manufacturerEASports = new Manufacturer { Name = "EA Sports", ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, @@ -9214,7 +9312,7 @@ public IList Manufacturers() #region Warner Home Video Games - var manufacturerEASports = new Manufacturer + var manufacturerWarnerHome = new Manufacturer { Name = "Warner Home Video Games", ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, @@ -9616,10 +9714,12 @@ public IList Manufacturers() #endregion Ubisoft var entities = new List - { - manufacturerEASports,manufacturerWarnerHome,manufacturerBreitling,manufacturerTissot,manufacturerSeiko, manufacturerTitleist,manufacturerApple,manufacturerSamsung,manufacturerLG,manufacturerTrekStor, manufacturerWesternDigital,manufacturerDell, manufacturerMSI, + { + manufacturerEASports,manufacturerWarnerHome,manufacturerBreitling,manufacturerTissot,manufacturerSeiko, manufacturerTitleist, manufacturerApple, + manufacturerSamsung,manufacturerLG,manufacturerTrekStor, manufacturerWesternDigital,manufacturerDell, manufacturerMSI, manufacturerCanon, manufacturerCasio, manufacturerPanasonic, manufacturerBlackBerry, manufacturerHTC, manufacturerFestina, manufacturerCertina, - manufacturerHP, manufacturerAcer, manufacturerSony, manufacturerUbisoft,manufacturerOakley,manufacturerRayban,manufacturerAdidas, manufacturerWilson,manufacturerPuma,manufacturerNike + manufacturerHP, manufacturerAcer, manufacturerSony, manufacturerUbisoft, manufacturerOakley, manufacturerRayban, manufacturerAdidas, manufacturerWilson, + manufacturerPuma,manufacturerNike, manufacturerMeyAndEdlich, manufacturerJackWolfskin, manufacturerMicrosoft }; this.Alter(entities); @@ -9633,6 +9733,7 @@ private List GetFashionProducts(Dictionary var firstDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 0); var fashionCategory = _ctx.Set().First(x => x.Alias == "Fashion"); var specialPriceEndDate = DateTime.UtcNow.AddMonths(1); + var specOptionCotton = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 9); // Converse All Star var converseAllStar = new Product @@ -9730,8 +9831,8 @@ private List GetFashionProducts(Dictionary Price = 29.00M }); - var shirtMeccanicaImages = new string[] { "product_shirt_meccanica_red_1.jpg", "product_shirt_meccanica_red_2.jpg", "product_shirt_meccanica_red_3.jpg", - "product_shirt_meccanica_red_4.jpg", "product_shirt_meccanica_black_1.jpg", "product_shirt_meccanica_black_2.jpg", "product_shirt_meccanica_black_3.jpg" }; + var shirtMeccanicaImages = new string[] { "product_shirt_meccanica_black_1.jpg", "product_shirt_meccanica_black_2.jpg", "product_shirt_meccanica_black_3.jpg", + "product_shirt_meccanica_red_1.jpg", "product_shirt_meccanica_red_2.jpg", "product_shirt_meccanica_red_3.jpg", "product_shirt_meccanica_red_4.jpg" }; for (var i = 0; i < shirtMeccanicaImages.Length; ++i) { @@ -9787,8 +9888,8 @@ private List GetFashionProducts(Dictionary DisplayOrder = 1 }); - var ladiesJacketImages = new string[] { "product_ladies_jacket_red.jpg", "product_ladies_jacket_orange.jpg", "product_ladies_jacket_green.jpg", - "product_ladies_jacket_blue.jpg", "product_ladies_jacket_navy.jpg", "product_ladies_jacket_silver.jpg", "product_ladies_jacket_black.jpg" }; + var ladiesJacketImages = new string[] { "product_ladies_jacket_silver.jpg", "product_ladies_jacket_black.jpg", "product_ladies_jacket_red.jpg", + "product_ladies_jacket_orange.jpg", "product_ladies_jacket_green.jpg", "product_ladies_jacket_blue.jpg", "product_ladies_jacket_navy.jpg", }; for (var i = 0; i < ladiesJacketImages.Length; ++i) { @@ -9855,7 +9956,7 @@ private List GetFashionProducts(Dictionary result.Add(clarkJeans); - return result; + return result; } private List GetFurnitureProducts(Dictionary specAttributes) @@ -10160,6 +10261,132 @@ public IList Products() var categories = _ctx.Set().ToList().ToDictionarySafe(x => x.Alias, x => x); var specAttributes = _ctx.Set().ToList().ToDictionarySafe(x => x.DisplayOrder, x => x); + + //#region category shoes + + //var categoryShoes = _ctx.Set().First(x => x.Alias == "Shoes"); + + //#region product Jack Wolfskin COOGEE LOW M + + //var productJackWolfskinCooGeeLowM = new Product + //{ + // ProductType = ProductType.SimpleProduct, + // VisibleIndividually = true, + // Name = "COOGEE LOW M", + // MetaTitle = "COOGEE LOW M", + // ShortDescription = "MÄNNER FREIZEITSCHUHE", + // FullDescription = "

Du bist immer auf dem Sprung: zum Kino, zur neueröffneten Bar oder zum nächsten Stadtfest. Der stylische COOGEE LOW ist DER Schuh für dein Leben in der Stadt. Denn er verbindet Funktion mit Style. Die sportliche EVA - Zwischensohle ist nicht nur leicht und flexibel, sondern dämpft deine Ferse mit ihrer Erhöhung im hinteren Bereich auch besonders gut und ist sehr komfortabel.So sind deine Füße auch nach vielen Stunden noch fit – du wirst es merken, wenn nach einem langen Tag z.B.noch eine spontane Party ansteht. Die Passform haben wir nach dem Vorbild einer Socke gestaltet, sie ist locker und komfortabel.Dennoch bleibt der Schuh in seiner schmalen Form. Dank des atmungsaktiven Obermaterials bleibt das Fußklima angenehm, egal, wie warm es draussen ist.Gleichzeitig ist der Schuh sehr pflegeleicht.Worauf wartest du also noch ? Ab in die City!

", + // Sku = "Wolfskin-4032541", + // ProductTemplateId = productTemplate.Id, + // AllowCustomerReviews = true, + // Published = true, + // Price = 69.90M, + // OldPrice = 99.95M, + // ManageInventoryMethod = ManageInventoryMethod.ManageStock, + // OrderMinimumQuantity = 1, + // OrderMaximumQuantity = 10000, + // StockQuantity = 10000, + // NotifyAdminForQuantityBelow = 1, + // IsShipEnabled = true, + // DeliveryTime = firstDeliveryTime, + // DisplayOrder = 5 + //}; + + //AddProductPicture(productJackWolfskinCooGeeLowM, "product_wolfskin_shoes_coogee_1.jpg", "jack-wolfskin-shoes-coogee-low-m-1"); + + //productJackWolfskinCooGeeLowM.ProductCategories.Add(new ProductCategory { Category = categoryShoes, DisplayOrder = 1 }); + + //productJackWolfskinCooGeeLowM.ProductManufacturers.Add(new ProductManufacturer + //{ + // Manufacturer = _ctx.Set().Where(c => c.Name == "Jack-Wolfskin").Single(), + // DisplayOrder = 1, + //}); + + //#endregion product Jack Wolfskin COOGEE LOW M + + //#region product Adidas SUPERSTAR SCHUH + + //var productAdidasSuperstarSchuh = new Product + //{ + // ProductType = ProductType.SimpleProduct, + // VisibleIndividually = true, + // Name = "SUPERSTAR SCHUH", + // MetaTitle = "SUPERSTAR SCHUH", + // ShortDescription = "DER STREETWEAR-KLASSIKER MIT DER SHELL TOE.", + // FullDescription = "

Der adidas Superstar wurde erstmals 1969 herausgebracht und machte seinem Namen schon bald alle Ehre. Heute gilt er als Streetstyle-Legende. In dieser Version kommt der Schuh mit einem bequemen Obermaterial aus Full-Grain-Leder. Perfekt wird der Look durch die klassische Shell Toe aus Gummi für mehr Strapazierfähigkeit.

", + // Sku = "Adidas-C77124", + // ProductTemplateId = productTemplate.Id, + // AllowCustomerReviews = true, + // Published = true, + // Price = 99.95M, + // ManageInventoryMethod = ManageInventoryMethod.ManageStock, + // OrderMinimumQuantity = 1, + // OrderMaximumQuantity = 10000, + // StockQuantity = 10000, + // NotifyAdminForQuantityBelow = 1, + // IsShipEnabled = true, + // DeliveryTime = firstDeliveryTime, + // DisplayOrder = 5 + //}; + + //AddProductPicture(productAdidasSuperstarSchuh, "product_adidas_superstar_schuh_1.jpg", "adidas-superstar-schuh-1"); + + //productJackWolfskinCooGeeLowM.ProductCategories.Add(new ProductCategory { Category = categoryShoes, DisplayOrder = 1 }); + + //productJackWolfskinCooGeeLowM.ProductManufacturers.Add(new ProductManufacturer + //{ + // Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), + // DisplayOrder = 1, + //}); + + + //#endregion product Adidas SUPERSTAR SCHUH + //#endregion category shoes + + #region category jackets + + var categoryJackets = _ctx.Set().First(x => x.Alias == "Jackets"); + + #region product Jack Wolfskin KANUKA POINT + + var productJackWolfsKinKanukaPoint = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "KANUKA POINT JACKET M", + MetaTitle = "KANUKA POINT JACKET M", + ShortDescription = "SOFTSHELLJACKE MÄNNER", + FullDescription = "

Sportliches Design für sportliche Touren: Die KANUKA POINT ist so gern in Bewegung wie du. Die Softshelljacke besteht aus superelastischem und sehr atmungsaktivem Material, das sich unterwegs jeder deiner Bewegungen anpasst. Deswegen nimmst du mit der KANUKA POINT jeden Pass mit Leichtigkeit.Und musst dir auch bei Kraxeleien zum Gipfel keine Gedanken um deine Jacke machen, denn ihr Material hält einiges aus.Auch bei Wind und leichten Schauern bleibst du gelassen.

", + Sku = "jack-1305851", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + Price = 83.90M, + OldPrice = 119.95M, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = firstDeliveryTime, + DisplayOrder = 5 + }; + + AddProductPicture(productJackWolfsKinKanukaPoint, "product_jack_wolfskin_kanuka_point_1.png", "jack-wolfskin-kanuka-point-1"); + + productJackWolfsKinKanukaPoint.ProductCategories.Add(new ProductCategory { Category = categoryJackets, DisplayOrder = 1 }); + + productJackWolfsKinKanukaPoint.ProductManufacturers.Add(new ProductManufacturer + { + Manufacturer = _ctx.Set().Where(c => c.Name == "Jack-Wolfskin").Single(), + DisplayOrder = 1, + }); + + #endregion product Jack Wolfskin KANUKA POINT + #endregion category jackets + + #region category golf #region product Titleist SM6 Tour Chrome @@ -12291,10 +12518,19 @@ public IList Products() #endregion productWatchesCertinaDSPodiumBigSize - #endregion watches + #endregion watches #region gaming + var manuSony = _ctx.Set().First(c => c.Name == "Sony"); + var manuMicrosoft = _ctx.Set().First(c => c.Name == "Microsoft"); + var manuEASports = _ctx.Set().First(c => c.Name == "EA Sports"); + var manuUbisoft = _ctx.Set().First(c => c.Name == "Ubisoft"); + var categoryGaming = _ctx.Set().First(c => c.Alias == "Gaming"); + var categoryGamingAccessories = _ctx.Set().First(c => c.Alias == "Gaming Accessories"); + var categoryGamingGames = _ctx.Set().First(c => c.Alias == "Games"); + var manuWarnerHomme = _ctx.Set().First(c => c.Name == "Warner Home Video Games"); + #region bundlePs3AssassinCreed var productPs3 = new Product @@ -12676,6 +12912,38 @@ public IList Products() #endregion Lego Worlds + //#region XBox One S + + //var productXBoxOneS = new Product + //{ + // ProductType = ProductType.SimpleProduct, + // VisibleIndividually = true, + // Sku = "Microsoft-xbox1s", + // Name = "Xbox One S 500 GB Konsole", + // ShortDescription = "Genieße über 100 Spiele, die es nur für die Konsole gibt, sowie eine ständig größer werdende Bibliothek an Xbox 360-Spielen auf der Xbox One S im neuen Design – der einzigen Konsole mit 4K Ultra HD Blu-ray, 4K-Videostreaming und HDR. Streame deine Lieblingsfilme und -sendungen in atemberaubendem 4K Ultra HD. Spiele Blockbuster wie Gears of War 4 und Battlefield 1 mit Freunden auf Xbox Live, dem schnellsten und zuverlässigsten Gaming-Netzwerk.", + // FullDescription = "
  • Die ultimativen Spiele und 4K-Entertainment-System.
  • 40 % kompaktere Konsole
    Lasse dich nicht von der Größe täuschen. Mit integriertem Netzteil und bis zu 2 TB Speicherplatz ist die Xbox One S die fortschrittlichste Xbox überhaupt.
  • Der beste Controller - jetzt noch besser
    Der neue Xbox Wireless Controller bietet ein schlankes, optimiertes Design, texturierte Grip - Fläche und Bluetooth zum Spielen auf Windows 10 Geräten. Genieße individuelle Tastenbelegung und verbesserte drahtlose Reichweite und stecke jeden kompatiblen Kopfhörer mit der 3, 5 mm Stereo - Headset - Buchse ein.
", + // ProductTemplateId = productTemplate.Id, + // AllowCustomerReviews = true, + // Published = true, + // MetaTitle = "Xbox One S", + // Price = 279.99M, + // OldPrice = 279.99M, + // ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + // OrderMinimumQuantity = 1, + // OrderMaximumQuantity = 10000, + // StockQuantity = 10000, + // NotifyAdminForQuantityBelow = 1, + // AllowBackInStockSubscriptions = false, + // IsShipEnabled = true, + // DeliveryTimeId = firstDeliveryTime.Id + //}; + + //AddProductPicture(productXBoxOneS, "product_xbox_one_s_1.png", "microsoft-xbox-one-s-1"); + //productXBoxOneS.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuMicrosoft, DisplayOrder = 1 }); + //productXBoxOneS.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 1 }); + + //#endregion + #endregion gaming var entities = new List @@ -12691,8 +12959,7 @@ public IList Products() productInstantDownloadVivaldi, productInstantDownloadBeethoven, productWatchesCertinaDSPodiumBigSize, productPs3, productMinecraft, productBundlePs3AssassinCreed, productPs4, productDualshock4Controller, productPs4Camera, productBundlePs4, - productGroupAccessories, - productPrinceOfPersia, productLegoWorlds, productHorizonZeroDown, productFifa17 + productGroupAccessories, productPrinceOfPersia, productLegoWorlds, productHorizonZeroDown, productFifa17, productJackWolfsKinKanukaPoint }; entities.AddRange(GetFashionProducts(specAttributes)); diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index 78be6614f9..9f7c8ce217 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -682,6 +682,10 @@ 201907032251575_CategoryExternalLink.cs + + + 201908150749388_V322Resources.cs + @@ -1221,6 +1225,9 @@ 201907032251575_CategoryExternalLink.cs + + 201908150749388_V322Resources.cs + diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index 0ff2e926fd..5fc429449c 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -598,7 +598,7 @@ public virtual decimal GetFinalPrice( { var itemPrice = GetFinalPrice(itemData.Item.Product, customer, itemData.AdditionalCharge, includeDiscounts, 1, itemData, context); - result = result + decimal.Multiply(itemPrice, itemData.Item.Quantity); + result += decimal.Multiply(itemPrice, itemData.Item.Quantity); } return result < decimal.Zero ? decimal.Zero : result; diff --git a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs index 1602eeb0b1..ac5bda82b6 100644 --- a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs @@ -105,10 +105,16 @@ public virtual SpecificationAttributeOption GetSpecificationAttributeOptionById( public virtual IList GetSpecificationAttributeOptionsBySpecificationAttribute(int specificationAttributeId) { + if (specificationAttributeId == 0) + { + return new List(); + } + var query = from sao in _specificationAttributeOptionRepository.Table orderby sao.DisplayOrder where sao.SpecificationAttributeId == specificationAttributeId select sao; + var specificationAttributeOptions = query.ToList(); return specificationAttributeOptions; } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderService.cs b/src/Libraries/SmartStore.Services/Orders/OrderService.cs index e1c872264b..0d9d8f965d 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderService.cs @@ -10,10 +10,11 @@ using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Events; +using SmartStore.Data.Caching; namespace SmartStore.Services.Orders { - public partial class OrderService : IOrderService + public partial class OrderService : IOrderService { private readonly IRepository _orderRepository; private readonly IRepository _orderItemRepository; @@ -48,7 +49,7 @@ public virtual Order GetOrderById(int orderId) if (orderId == 0) return null; - return _orderRepository.GetById(orderId); + return _orderRepository.GetByIdCached(orderId, "db.order.id-" + orderId); } public virtual IList GetOrdersByIds(int[] orderIds) diff --git a/src/Libraries/SmartStore.Services/Topics/TopicService.cs b/src/Libraries/SmartStore.Services/Topics/TopicService.cs index 5f7a0ab4e2..07152bf79b 100644 --- a/src/Libraries/SmartStore.Services/Topics/TopicService.cs +++ b/src/Libraries/SmartStore.Services/Topics/TopicService.cs @@ -49,7 +49,7 @@ public virtual Topic GetTopicById(int topicId) if (topicId == 0) return null; - return _topicRepository.GetById(topicId); + return _topicRepository.GetByIdCached(topicId, "db.topic.id-" + topicId); } public virtual Topic GetTopicBySystemName(string systemName, int storeId = 0, bool checkPermission = true) diff --git a/src/Plugins/SmartStore.AmazonPay/Description.txt b/src/Plugins/SmartStore.AmazonPay/Description.txt index b74fc61bc0..7449e587bb 100644 --- a/src/Plugins/SmartStore.AmazonPay/Description.txt +++ b/src/Plugins/SmartStore.AmazonPay/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Login and Pay with Amazon SystemName: SmartStore.AmazonPay Group: Payment -Version: 3.2.1 -MinAppVersion: 3.2.1 +Version: 3.2.2 +MinAppVersion: 3.2.2 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.AmazonPay.dll diff --git a/src/Plugins/SmartStore.Clickatell/Description.txt b/src/Plugins/SmartStore.Clickatell/Description.txt index a7cf314f73..003d6a572e 100644 --- a/src/Plugins/SmartStore.Clickatell/Description.txt +++ b/src/Plugins/SmartStore.Clickatell/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Clickatell SMS Provider SystemName: SmartStore.Clickatell Group: Mobile -Version: 3.2.1 -MinAppVersion: 3.2.1 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.Clickatell.dll ResourceRootKey: Plugins.Sms.Clickatell \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Description.txt b/src/Plugins/SmartStore.DevTools/Description.txt index e708fffa2f..54399e9fcf 100644 --- a/src/Plugins/SmartStore.DevTools/Description.txt +++ b/src/Plugins/SmartStore.DevTools/Description.txt @@ -1,8 +1,8 @@ FriendlyName: SmartStore.NET Developer Tools (MiniProfiler and other goodies) SystemName: SmartStore.DevTools Group: Developer -Version: 3.2.1 -MinAppVersion: 3.2.1 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.DevTools.dll ResourceRootKey: Plugins.Developer.DevTools \ No newline at end of file diff --git a/src/Plugins/SmartStore.DiscountRules/Description.txt b/src/Plugins/SmartStore.DiscountRules/Description.txt index f1745c1985..b7b46e9263 100644 --- a/src/Plugins/SmartStore.DiscountRules/Description.txt +++ b/src/Plugins/SmartStore.DiscountRules/Description.txt @@ -2,8 +2,8 @@ Description: Contains common discount requirement rule providers like "Billing country is", "Customer role is", "Had spent amount" etc. Group: Marketing SystemName: SmartStore.DiscountRules -Version: 3.2.1 -MinAppVersion: 3.2.1 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 0 FileName: SmartStore.DiscountRules.dll ResourceRootKey: Plugins.SmartStore.DiscountRules diff --git a/src/Plugins/SmartStore.FacebookAuth/Description.txt b/src/Plugins/SmartStore.FacebookAuth/Description.txt index e354110ad5..447d4bf86d 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Description.txt +++ b/src/Plugins/SmartStore.FacebookAuth/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Facebook Login SystemName: SmartStore.FacebookAuth Group: Security -Version: 3.2.1 -MinAppVersion: 3.2.1 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 5 FileName: SmartStore.FacebookAuth.dll ResourceRootKey: Plugins.ExternalAuth.Facebook \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt index abfbc07820..ac4936f475 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt +++ b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Google Analytics SystemName: SmartStore.GoogleAnalytics Group: Analytics -Version: 3.2.1 -MinAppVersion: 3.2.1 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.GoogleAnalytics.dll ResourceRootKey: Plugins.Widgets.GoogleAnalytics diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt index 84dd7bb72b..3b87cd8d04 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Google Merchant Center (GMC) feed SystemName: SmartStore.GoogleMerchantCenter Group: Marketing -Version: 3.2.1 -MinAppVersion: 3.2.1 +Version: 3.2.2 +MinAppVersion: 3.2.2 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.GoogleMerchantCenter.dll diff --git a/src/Plugins/SmartStore.OfflinePayment/Description.txt b/src/Plugins/SmartStore.OfflinePayment/Description.txt index d546d3326d..d2ed286f27 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Description.txt +++ b/src/Plugins/SmartStore.OfflinePayment/Description.txt @@ -2,8 +2,8 @@ Description: Contains common offline payment methods like Direct Debit, Invoice, Prepayment etc. Group: Payment SystemName: SmartStore.OfflinePayment -Version: 3.2.1 -MinAppVersion: 3.2.1 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 0 FileName: SmartStore.OfflinePayment.dll ResourceRootKey: Plugins.SmartStore.OfflinePayment diff --git a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml index fc3ede7e9b..aabebe0d5a 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml @@ -34,7 +34,7 @@ @@ -55,7 +55,7 @@ @@ -97,7 +97,7 @@ @@ -118,7 +118,7 @@ @@ -151,7 +151,7 @@ diff --git a/src/Plugins/SmartStore.PayPal/Content/instalments-sm.png b/src/Plugins/SmartStore.PayPal/Content/instalments-sm.png new file mode 100644 index 0000000000..81b6f59820 Binary files /dev/null and b/src/Plugins/SmartStore.PayPal/Content/instalments-sm.png differ diff --git a/src/Plugins/SmartStore.PayPal/Content/instalments.png b/src/Plugins/SmartStore.PayPal/Content/instalments.png new file mode 100644 index 0000000000..6a942f97cd Binary files /dev/null and b/src/Plugins/SmartStore.PayPal/Content/instalments.png differ diff --git a/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css b/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css index 8c2faa5c54..cdd0819284 100644 --- a/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css +++ b/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css @@ -3,7 +3,3 @@ font-weight: bold; padding: 12px 15px 5px 0; } - -.paypal-standard-public { - margin-bottom: 10px; -} diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs index 61fda2d379..9cfad9c611 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs @@ -19,27 +19,28 @@ namespace SmartStore.PayPal.Controllers { - public abstract class PayPalPaymentControllerBase : PaymentControllerBase + public abstract class PayPalPaymentControllerBase : PaymentControllerBase { - protected void PrepareConfigurationModel(ApiConfigurationModel model, int storeScope) + protected abstract string ProviderSystemName { get; } + + protected void PrepareConfigurationModel(ApiConfigurationModel model, int storeScope) { var store = storeScope == 0 ? Services.StoreContext.CurrentStore : Services.StoreService.GetStoreById(storeScope); model.PrimaryStoreCurrencyCode = store.PrimaryStoreCurrency.CurrencyCode; - } - } + } + } + public abstract class PayPalControllerBase : PayPalPaymentControllerBase where TSetting : PayPalSettingsBase, ISettings, new() { public PayPalControllerBase( - string systemName, IPaymentService paymentService, IOrderService orderService, IOrderProcessingService orderProcessingService) { - SystemName = systemName; PaymentService = paymentService; OrderService = orderService; OrderProcessingService = orderProcessingService; @@ -143,7 +144,7 @@ public ActionResult IPNHandler() byte[] param = Request.BinaryRead(Request.ContentLength); var strRequest = Encoding.UTF8.GetString(param); - if (!PaymentService.IsPaymentMethodActive(SystemName, Services.StoreContext.CurrentStore.Id)) + if (!PaymentService.IsPaymentMethodActive(ProviderSystemName, Services.StoreContext.CurrentStore.Id)) { Logger.Warn(new SmartException(strRequest), T("Plugins.Payments.PayPal.NoModuleLoading", "IPNHandler")); return Content(string.Empty); diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs index 3932a4a301..b965919ff3 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs @@ -24,7 +24,6 @@ public PayPalDirectController( IOrderProcessingService orderProcessingService, PaymentSettings paymentSettings, HttpContextBase httpContext) : base( - PayPalDirectProvider.SystemName, paymentService, orderService, orderProcessingService) @@ -32,7 +31,9 @@ public PayPalDirectController( _httpContext = httpContext; } - [LoadSetting, AdminAuthorize, ChildActionOnly] + protected override string ProviderSystemName => PayPalDirectProvider.SystemName; + + [LoadSetting, AdminAuthorize, ChildActionOnly] public ActionResult Configure(PayPalDirectPaymentSettings settings, int storeScope) { var model = new PayPalDirectConfigurationModel(); diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs index 1f09fdbfda..4c053bae44 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs @@ -38,7 +38,6 @@ public PayPalExpressController( IOrderTotalCalculationService orderTotalCalculationService, ICustomerService customerService, IGenericAttributeService genericAttributeService) : base( - PayPalExpressProvider.SystemName, paymentService, orderService, orderProcessingService) @@ -50,7 +49,9 @@ public PayPalExpressController( _genericAttributeService = genericAttributeService; } - private string GetCheckoutButtonUrl(PayPalExpressPaymentSettings settings) + protected override string ProviderSystemName => PayPalExpressProvider.SystemName; + + private string GetCheckoutButtonUrl(PayPalExpressPaymentSettings settings) { var expressCheckoutButton = "~/Plugins/SmartStore.PayPal/Content/checkout-button-default.png"; var cultureString = Services.WorkContext.WorkingLanguage.LanguageCulture; diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalInstalmentsController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalInstalmentsController.cs new file mode 100644 index 0000000000..2c8babaeae --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalInstalmentsController.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using Newtonsoft.Json; +using SmartStore.ComponentModel; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Logging; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Models; +using SmartStore.PayPal.Providers; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Directory; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; +using SmartStore.Web.Framework.Settings; +using SmartStore.Web.Framework.Theming; + +namespace SmartStore.PayPal.Controllers +{ + public class PayPalInstalmentsController : PayPalRestApiControllerBase + { + private readonly HttpContextBase _httpContext; + private readonly IGenericAttributeService _genericAttributeService; + private readonly IOrderService _orderService; + private readonly ICurrencyService _currencyService; + private readonly Lazy _paymentService; + private readonly IPriceFormatter _priceFormatter; + private readonly Lazy _pluginFinder; + + public PayPalInstalmentsController( + HttpContextBase httpContext, + IPayPalService payPalService, + IGenericAttributeService genericAttributeService, + IOrderService orderService, + ICurrencyService currencyService, + Lazy paymentService, + IPriceFormatter priceFormatter, + Lazy pluginFinder) + : base(payPalService) + { + _httpContext = httpContext; + _genericAttributeService = genericAttributeService; + _orderService = orderService; + _currencyService = currencyService; + _paymentService = paymentService; + _priceFormatter = priceFormatter; + _pluginFinder = pluginFinder; + } + + protected override string ProviderSystemName => PayPalInstalmentsProvider.SystemName; + + public override IList ValidatePaymentForm(FormCollection form) + { + return new List(); + } + + public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) + { + var paymentInfo = new ProcessPaymentRequest + { + OrderGuid = Guid.NewGuid() + }; + + return paymentInfo; + } + + public ActionResult PaymentInfo() + { + FinancingOptions model = null; + + try + { + var store = Services.StoreContext.CurrentStore; + var settings = Services.Settings.LoadSetting(store.Id); + var session = _httpContext.GetPayPalState(PayPalInstalmentsProvider.SystemName); + model = PayPalService.GetFinancingOptions(settings, session, "paymentinfo", decimal.Zero); + } + catch (Exception ex) + { + Logger.Error(ex); + } + + return PartialView(model ?? new FinancingOptions("paymentinfo")); + } + + // Widget zone on checkout confirm page. + [ChildActionOnly] + public ActionResult OrderSummaryTotals() + { + try + { + var store = Services.StoreContext.CurrentStore; + var language = Services.WorkContext.WorkingLanguage; + var targetCurrency = Services.WorkContext.WorkingCurrency; + var session = _httpContext.GetPayPalState(PayPalInstalmentsProvider.SystemName); + + if (session.FinancingCosts == decimal.Zero || session.TotalInclFinancingCosts == decimal.Zero) + { + var settings = Services.Settings.LoadSetting(store.Id); + var result = PayPalService.GetPayment(settings, session); + if (result.Success) + { + var rawCosts = (string)result.Json.credit_financing_offered.total_interest.value; + var rawTotal = (string)result.Json.credit_financing_offered.total_cost.value; + + session.FinancingCosts = rawCosts.Convert(CultureInfo.InvariantCulture); + session.TotalInclFinancingCosts = rawTotal.Convert(CultureInfo.InvariantCulture); + } + } + + var financingCosts = _currencyService.ConvertFromPrimaryStoreCurrency(session.FinancingCosts, targetCurrency, store); + var totalInclFinancingCosts = _currencyService.ConvertFromPrimaryStoreCurrency(session.TotalInclFinancingCosts, targetCurrency, store); + + ViewBag.FinancingCosts = _priceFormatter.FormatPrice(financingCosts, true, targetCurrency, language, false, false); + ViewBag.TotalInclFinancingCosts = _priceFormatter.FormatPrice(totalInclFinancingCosts, true, targetCurrency, language, false, false); + + return PartialView(); + } + catch (Exception ex) + { + Logger.Error(ex); + } + + return new EmptyResult(); + } + + // Widget zone on order details (page and print). + [ChildActionOnly] + public ActionResult OrderDetails(int orderId, bool print) + { + try + { + var order = _orderService.GetOrderById(orderId); + if (order != null && order.PaymentMethodSystemName.IsCaseInsensitiveEqual(PayPalInstalmentsProvider.SystemName)) + { + var str = order.GetAttribute(PayPalInstalmentsOrderAttribute.Key, _genericAttributeService, order.StoreId); + if (str.HasValue()) + { + // Get additional order values in primary store currency. + var orderAttribute = JsonConvert.DeserializeObject(str); + + // Convert into order currency. + var language = Services.WorkContext.WorkingLanguage; + var store = Services.StoreService.GetStoreById(order.StoreId) ?? Services.StoreContext.CurrentStore; + var targetCurrency = _currencyService.GetCurrencyByCode(order.CustomerCurrencyCode) ?? store.PrimaryStoreCurrency; + + var financingCosts = _currencyService.ConvertFromPrimaryStoreCurrency(orderAttribute.FinancingCosts, targetCurrency, store); + var totalInclFinancingCosts = _currencyService.ConvertFromPrimaryStoreCurrency(orderAttribute.TotalInclFinancingCosts, targetCurrency, store); + + ViewBag.FinancingCosts = _priceFormatter.FormatPrice(financingCosts, true, targetCurrency, language, false, false); + ViewBag.TotalInclFinancingCosts = _priceFormatter.FormatPrice(totalInclFinancingCosts, true, targetCurrency, language, false, false); + + if (print) + { + return PartialView("OrderDetails.Print"); + } + + return PartialView(); + } + } + } + catch (Exception ex) + { + Logger.Error(ex); + } + + return new EmptyResult(); + } + + // Redirect from PayPal to the shop. + public ActionResult CheckoutReturn(string paymentId, string PayerID) + { + // Request.QueryString: + // paymentId: PAY-0TC88803RP094490KK4KM6AI, token (not the access token): EC-5P379249AL999154U, PayerID: 5L9K773HHJLPN + + var session = _httpContext.GetPayPalState(PayPalInstalmentsProvider.SystemName); + + if (paymentId.HasValue() && session.PaymentId.IsEmpty()) + { + session.PaymentId = paymentId; + } + + session.PayerId = PayerID; + + return RedirectToAction("Confirm", "Checkout", new { area = "" }); + } + + // Redirect from PayPal to the shop. + public ActionResult CheckoutCancel() + { + var store = Services.StoreContext.CurrentStore; + var customer = Services.WorkContext.CurrentCustomer; + var session = _httpContext.GetPayPalState(PayPalInstalmentsProvider.SystemName); + + session.PayerId = session.PaymentId = session.ApprovalUrl = null; + session.FinancingCosts = session.TotalInclFinancingCosts = decimal.Zero; + + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, null, store.Id); + + return RedirectToAction("PaymentMethod", "Checkout", new { area = "" }); + } + + #region Promotion + + // Widget zone on product detail or cart page. + [ChildActionOnly] + public ActionResult Promotion(string origin, decimal amount) + { + try + { + var store = Services.StoreContext.CurrentStore; + + if (_pluginFinder.Value.IsPluginReady(Services.Settings, Plugin.SystemName, store.Id)) + { + if (_paymentService.Value.IsPaymentMethodActive(PayPalInstalmentsProvider.SystemName, store.Id)) + { + var settings = Services.Settings.LoadSetting(store.Id); + var session = _httpContext.GetPayPalState(PayPalInstalmentsProvider.SystemName); + var model = PayPalService.GetFinancingOptions(settings, session, origin, amount); + + if (model != null) + { + return PartialView(model); + } + } + } + } + catch (Exception ex) + { + Logger.Error(ex); + } + + return new EmptyResult(); + } + + // Ajax. + public ActionResult PromotionPopup(string origin, decimal amount) + { + try + { + var store = Services.StoreContext.CurrentStore; + + if (!_pluginFinder.Value.IsPluginReady(Services.Settings, Plugin.SystemName, store.Id) || + !_paymentService.Value.IsPaymentMethodActive(PayPalInstalmentsProvider.SystemName, store.Id)) + { + return HttpNotFound(); + } + + var settings = Services.Settings.LoadSetting(store.Id); + var session = _httpContext.GetPayPalState(PayPalInstalmentsProvider.SystemName); + var model = PayPalService.GetFinancingOptions(settings, session, origin, amount, PayPalPromotion.FinancingExample); + + if (model != null) + { + return PartialView(model); + } + else + { + return Content("
{0}
".FormatInvariant(T("Plugins.Payments.PayPalInstalments.NoFinancingDetails"))); + } + } + catch (Exception ex) + { + Logger.Error(ex); + return Content("
{0}
".FormatInvariant(ex.ToAllMessages().HtmlEncode())); + } + } + + // Widget zones for promotion. + //[ChildActionOnly] + //public ActionResult Promote() + //{ + // return new EmptyResult(); + //} + + #endregion + + #region Admin + + [ChildActionOnly, AdminAuthorize, LoadSetting, AdminThemed] + public ActionResult Configure(PayPalInstalmentsSettings settings, int storeScope) + { + var model = new PayPalInstalmentsConfigModel(); + MiniMapper.Map(settings, model); + //model.PromotionWidgetZones = settings.PromotionWidgetZones.SplitSafe(","); + + model.ProductPagePromotions = settings.ProductPagePromotion.HasValue + ? settings.ProductPagePromotion.Value.ToSelectList(true).ToList() + : PayPalPromotion.FinancingExample.ToSelectList(false).ToList(); + + model.CartPagePromotions = settings.CartPagePromotion.HasValue + ? settings.CartPagePromotion.Value.ToSelectList(true).ToList() + : PayPalPromotion.FinancingExample.ToSelectList(false).ToList(); + + model.PaymentListPromotions = PayPalPromotion.FinancingExample.ToSelectList(false).ToList(); + + PrepareConfigurationModel(model, storeScope); + + if (model.Lender == null) + { + var cs = Services.Settings.LoadSetting(); + model.Lender = $"{cs.CompanyName.EmptyNull()}, {cs.Street.EmptyNull()}, {cs.ZipCode.EmptyNull()} {cs.City.EmptyNull()}"; + } + + return View(model); + } + + [HttpPost, ChildActionOnly, AdminAuthorize, AdminThemed] + public ActionResult Configure(PayPalInstalmentsConfigModel model, FormCollection form) + { + Action additionalMapping = (x) => + { + //x.PromotionWidgetZones = string.Join(",", model.PromotionWidgetZones ?? new string[0]).NullEmpty(); + }; + + if (!SaveConfigurationModel(model, form, additionalMapping)) + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + + return Configure(settings, storeScope); + } + + return RedirectToConfiguration(PayPalInstalmentsProvider.SystemName, false); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs index d3433312a2..55a230d466 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs @@ -1,10 +1,18 @@ -using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web; +using System.Web.Mvc; +using Newtonsoft.Json; +using SmartStore.ComponentModel; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Html; using SmartStore.Core.Plugins; using SmartStore.PayPal.Models; +using SmartStore.PayPal.Providers; using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Services.Catalog; @@ -20,12 +28,6 @@ using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; using SmartStore.Web.Framework.Theming; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web; -using System.Web.Mvc; namespace SmartStore.PayPal.Controllers { @@ -47,9 +49,7 @@ public PayPalPlusController( IPaymentService paymentService, ITaxService taxService, ICurrencyService currencyService, - IPriceFormatter priceFormatter) : base( - PayPalPlusProvider.SystemName, - payPalService) + IPriceFormatter priceFormatter) : base(payPalService) { _httpContext = httpContext; _pluginMediator = pluginMediator; @@ -60,22 +60,7 @@ public PayPalPlusController( _priceFormatter = priceFormatter; } - private string GetPaymentMethodName(Provider provider) - { - if (provider != null) - { - var name = _pluginMediator.GetLocalizedFriendlyName(provider.Metadata); - - if (name.IsEmpty()) - name = provider.Metadata.FriendlyName; - - if (name.IsEmpty()) - name = provider.Metadata.SystemName; - - return name; - } - return ""; - } + protected override string ProviderSystemName => PayPalPlusProvider.SystemName; private string GetPaymentFee(Provider provider, List cart) { @@ -96,14 +81,29 @@ private PayPalPlusCheckoutModel.ThirdPartyPaymentMethod GetThirdPartyPaymentMeth Store store) { var model = new PayPalPlusCheckoutModel.ThirdPartyPaymentMethod(); - model.MethodName = GetPaymentMethodName(provider); - model.RedirectUrl = Url.Action("CheckoutReturn", "PayPalPlus", new { area = Plugin.SystemName, systemName = provider.Metadata.SystemName }, store.SslEnabled ? "https" : "http"); try { - if (settings.DisplayPaymentMethodDescription) + model.RedirectUrl = Url.Action("CheckoutReturn", "PayPalPlus", new { area = Plugin.SystemName, systemName = provider.Metadata.SystemName }, store.SslEnabled ? "https" : "http"); + + if (provider.Metadata.SystemName == PayPalInstalmentsProvider.SystemName) + { + // "The methodName contains up to 25 characters. All additional characters are truncated." + // https://developer.paypal.com/docs/paypal-plus/germany/how-to/integrate-third-party-payments/ + model.MethodName = T("Plugins.Payments.PayPalInstalments.ShortMethodName"); + } + else + { + model.MethodName = _pluginMediator.GetLocalizedFriendlyName(provider.Metadata).NullEmpty() + ?? provider.Metadata.FriendlyName.NullEmpty() + ?? provider.Metadata.SystemName; + } + + model.MethodName = model.MethodName.EmptyNull(); + + if (settings.DisplayPaymentMethodDescription) { - // not the short description, the full description is intended for frontend + // Not the short description, the full description is intended for frontend. var paymentMethod = _paymentService.GetPaymentMethodBySystemName(provider.Metadata.SystemName); if (paymentMethod != null) { @@ -113,8 +113,10 @@ private PayPalPlusCheckoutModel.ThirdPartyPaymentMethod GetThirdPartyPaymentMeth description = HtmlUtils.ConvertHtmlToPlainText(description); description = HtmlUtils.StripTags(HttpUtility.HtmlDecode(description)); - if (description.HasValue()) - model.Description = description.EncodeJsString(); + if (description.HasValue()) + { + model.Description = description.EncodeJsString(); + } } } } @@ -125,17 +127,28 @@ private PayPalPlusCheckoutModel.ThirdPartyPaymentMethod GetThirdPartyPaymentMeth { if (settings.DisplayPaymentMethodLogo && provider.Metadata.PluginDescriptor != null && store.SslEnabled) { - var brandImageUrl = _pluginMediator.GetBrandImageUrl(provider.Metadata); - if (brandImageUrl.HasValue()) - { - if (brandImageUrl.StartsWith("~")) - brandImageUrl = brandImageUrl.Substring(1); - - var uri = new UriBuilder(Uri.UriSchemeHttps, Request.Url.Host, -1, brandImageUrl); - model.ImageUrl = uri.ToString(); - } - } - } + // Special case PayPal instalments. + if (provider.Metadata.SystemName == PayPalInstalmentsProvider.SystemName && model.ImageUrl.IsEmpty()) + { + var uri = new UriBuilder(Uri.UriSchemeHttps, Request.Url.Host, -1, "Plugins/SmartStore.PayPal/Content/instalments-sm.png"); + model.ImageUrl = uri.ToString(); + } + else + { + var brandImageUrl = _pluginMediator.GetBrandImageUrl(provider.Metadata); + if (brandImageUrl.HasValue()) + { + if (brandImageUrl.StartsWith("~")) + { + brandImageUrl = brandImageUrl.Substring(1); + } + + var uri = new UriBuilder(Uri.UriSchemeHttps, Request.Url.Host, -1, brandImageUrl); + model.ImageUrl = uri.ToString(); + } + } + } + } catch { } return model; @@ -158,77 +171,45 @@ public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) [LoadSetting, AdminAuthorize, ChildActionOnly, AdminThemed] public ActionResult Configure(PayPalPlusPaymentSettings settings, int storeScope) { - var model = new PayPalPlusConfigurationModel - { - ConfigGroups = T("Plugins.SmartStore.PayPal.ConfigGroups").Text.SplitSafe(";") - }; - // It's better to also offer inactive methods here but filter them out in frontend. var paymentMethods = _paymentService.LoadAllPaymentMethods(storeScope); - model.Copy(settings, true); - PrepareConfigurationModel(model, storeScope); - - model.AvailableThirdPartyPaymentMethods = paymentMethods - .Where(x => - x.Metadata.PluginDescriptor.SystemName != Plugin.SystemName && - !x.Value.RequiresInteraction && - (x.Metadata.PluginDescriptor.SystemName == "SmartStore.OfflinePayment" || x.Value.PaymentMethodType == PaymentMethodType.Redirection)) - .ToSelectListItems(_pluginMediator, model.ThirdPartyPaymentMethods.ToArray()); - - return View(model); + var model = new PayPalPlusConfigurationModel(); + MiniMapper.Map(settings, model); + PrepareConfigurationModel(model, storeScope); + + model.AvailableThirdPartyPaymentMethods = paymentMethods + .Where(x => + { + if (x.Value.RequiresInteraction) + { + return false; + } + if (x.Metadata.PluginDescriptor.SystemName == Plugin.SystemName) + { + return x.Metadata.SystemName == PayPalInstalmentsProvider.SystemName; + } + + return x.Metadata.PluginDescriptor.SystemName == "SmartStore.OfflinePayment" || x.Value.PaymentMethodType == PaymentMethodType.Redirection; + }) + .ToSelectListItems(_pluginMediator, model.ThirdPartyPaymentMethods.ToArray()); + + return View(model); } [HttpPost, AdminAuthorize, ChildActionOnly, AdminThemed] public ActionResult Configure(PayPalPlusConfigurationModel model, FormCollection form) { - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); - - var oldClientId = settings.ClientId; - var oldSecret = settings.Secret; - var oldProfileId = settings.ExperienceProfileId; - - var validator = new PayPalPlusConfigValidator(T, x => - { - return storeScope == 0 || storeDependingSettingHelper.IsOverrideChecked(settings, x, form); - }); - - validator.Validate(model, ModelState); - - if (!ModelState.IsValid) - { - return Configure(settings, storeScope); - } - - ModelState.Clear(); - model.Copy(settings, false); - - // Credentials changed: reset profile and webhook id to avoid errors. - if (!oldClientId.IsCaseInsensitiveEqual(settings.ClientId) || !oldSecret.IsCaseInsensitiveEqual(settings.Secret)) - { - if (oldProfileId.IsCaseInsensitiveEqual(settings.ExperienceProfileId)) - settings.ExperienceProfileId = null; - - settings.WebhookId = null; - } - - using (Services.Settings.BeginScope()) - { - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); - } - - using (Services.Settings.BeginScope()) - { - // Multistore context not possible, see IPN handling. - Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); - } + if (!SaveConfigurationModel(model, form)) + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); - NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + return Configure(settings, storeScope); + } - return RedirectToConfiguration(PayPalPlusProvider.SystemName, false); - } + return RedirectToConfiguration(PayPalPlusProvider.SystemName, false); + } public ActionResult PaymentInfo() { @@ -296,11 +277,13 @@ public ActionResult PaymentWall() var result = PayPalService.EnsureAccessToken(session, settings); if (result.Success) { - var protocol = (store.SslEnabled ? "https" : "http"); + var protocol = store.SslEnabled ? "https" : "http"; var returnUrl = Url.Action("CheckoutReturn", "PayPalPlus", new { area = Plugin.SystemName }, protocol); var cancelUrl = Url.Action("CheckoutCancel", "PayPalPlus", new { area = Plugin.SystemName }, protocol); + + var paymentData = PayPalService.CreatePaymentData(settings, session, cart, returnUrl, cancelUrl); - result = PayPalService.CreatePayment(settings, session, cart, PayPalPlusProvider.SystemName, returnUrl, cancelUrl); + result = PayPalService.CreatePayment(settings, session, paymentData); if (result == null) { // No payment required. @@ -369,7 +352,7 @@ public ActionResult PatchShipping() var settings = Services.Settings.LoadSetting(store.Id); var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); - var result = PayPalService.PatchShipping(settings, session, cart, PayPalPlusProvider.SystemName); + var result = PayPalService.PatchShipping(settings, session, cart); var errorMessage = result.ErrorMessage; if (!result.Success && result.IsValidationError) @@ -387,7 +370,7 @@ public ActionResult CheckoutCompleted() _genericAttributeService.SaveAttribute(customer, PayPalPlusProvider.SystemName + ".SessionData", (string)null, store.Id); - var instruct = _httpContext.Session[PayPalPlusProvider.CheckoutCompletedKey] as string; + var instruct = _httpContext.Session["PayPalCheckoutCompleted"] as string; if (instruct.HasValue()) { return Content(instruct); @@ -396,7 +379,6 @@ public ActionResult CheckoutCompleted() return new EmptyResult(); } - [ValidateInput(false)] public ActionResult CheckoutReturn(string systemName, string paymentId, string PayerID) { // Request.QueryString: @@ -404,7 +386,6 @@ public ActionResult CheckoutReturn(string systemName, string paymentId, string P var store = Services.StoreContext.CurrentStore; var customer = Services.WorkContext.CurrentCustomer; - var settings = Services.Settings.LoadSetting(store.Id); var session = _httpContext.GetPayPalState(PayPalPlusProvider.SystemName); if (systemName.IsEmpty()) @@ -429,7 +410,6 @@ public ActionResult CheckoutReturn(string systemName, string paymentId, string P return RedirectToAction("Confirm", "Checkout", new { area = "" }); } - [ValidateInput(false)] public ActionResult CheckoutCancel() { // Request.QueryString: diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs index c28bb24fcd..7fd62683de 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs @@ -2,22 +2,22 @@ using System.IO; using System.Net; using System.Web.Mvc; +using SmartStore.ComponentModel; using SmartStore.Core.Configuration; using SmartStore.Core.Logging; +using SmartStore.PayPal.Models; using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Security; +using SmartStore.Web.Framework.Settings; namespace SmartStore.PayPal.Controllers { - public abstract class PayPalRestApiControllerBase : PayPalPaymentControllerBase where TSetting : PayPalApiSettingsBase, ISettings, new() + public abstract class PayPalRestApiControllerBase : PayPalPaymentControllerBase where TSetting : PayPalApiSettingsBase, ISettings, new() { - public PayPalRestApiControllerBase( - string systemName, - IPayPalService payPalService) + public PayPalRestApiControllerBase(IPayPalService payPalService) { - SystemName = systemName; PayPalService = payPalService; } @@ -26,7 +26,6 @@ private string GetControllerName() return GetType().Name.Replace("Controller", ""); } - protected string SystemName { get; private set; } protected IPayPalService PayPalService { get; private set; } [AdminAuthorize] @@ -36,9 +35,9 @@ public ActionResult UpsertExperienceProfile() var settings = Services.Settings.LoadSetting(storeScope); var store = Services.StoreService.GetStoreById(storeScope == 0 ? Services.StoreContext.CurrentStore.Id : storeScope); - var session = new PayPalSessionData(); + var session = new PayPalSessionData { ProviderSystemName = ProviderSystemName }; - var result = PayPalService.EnsureAccessToken(session, settings); + var result = PayPalService.EnsureAccessToken(session, settings); if (result.Success) { result = PayPalService.UpsertCheckoutExperience(settings, session, store); @@ -49,12 +48,16 @@ public ActionResult UpsertExperienceProfile() } } - if (result.Success) - NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); - else - NotifyError(result.ErrorMessage); + if (result.Success) + { + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + } + else + { + NotifyError(result.ErrorMessage); + } - return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = ProviderSystemName }); } [AdminAuthorize] @@ -62,9 +65,9 @@ public ActionResult DeleteExperienceProfile() { var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); var settings = Services.Settings.LoadSetting(storeScope); - var session = new PayPalSessionData(); + var session = new PayPalSessionData { ProviderSystemName = ProviderSystemName }; - var result = PayPalService.EnsureAccessToken(session, settings); + var result = PayPalService.EnsureAccessToken(session, settings); if (result.Success) { result = PayPalService.DeleteCheckoutExperience(settings, session); @@ -75,26 +78,34 @@ public ActionResult DeleteExperienceProfile() } } - if (result.Success) - NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); - else - NotifyError(result.ErrorMessage); + if (result.Success) + { + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + } + else + { + NotifyError(result.ErrorMessage); + } - return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = ProviderSystemName }); } [AdminAuthorize] public ActionResult CreateWebhook() { var settings = Services.Settings.LoadSetting(); - var session = new PayPalSessionData(); + var session = new PayPalSessionData { ProviderSystemName = ProviderSystemName }; - using (Services.Settings.BeginScope()) + using (Services.Settings.BeginScope()) { if (settings.WebhookId.HasValue()) { - var unused = PayPalService.DeleteWebhook(settings, session); - Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false); + var result1 = PayPalService.DeleteWebhook(settings, session); + if (result1.Success) + { + settings.WebhookId = null; + Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false); + } } var url = Url.Action("Webhook", GetControllerName(), new { area = Plugin.SystemName }, "https"); @@ -110,22 +121,26 @@ public ActionResult CreateWebhook() } } - if (result.Success) - NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); - else - NotifyError(result.ErrorMessage); + if (result.Success) + { + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + } + else + { + NotifyError(result.ErrorMessage); + } } - return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = ProviderSystemName }); } [AdminAuthorize] public ActionResult DeleteWebhook() { var settings = Services.Settings.LoadSetting(); - var session = new PayPalSessionData(); + var session = new PayPalSessionData { ProviderSystemName = ProviderSystemName }; - if (settings.WebhookId.HasValue()) + if (settings.WebhookId.HasValue()) { var result = PayPalService.EnsureAccessToken(session, settings); if (result.Success) @@ -138,13 +153,17 @@ public ActionResult DeleteWebhook() } } - if (result.Success) - NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); - else - NotifyError(result.ErrorMessage); + if (result.Success) + { + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + } + else + { + NotifyError(result.ErrorMessage); + } } - return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = ProviderSystemName }); } [ValidateInput(false)] @@ -162,14 +181,106 @@ public ActionResult Webhook() var settings = Services.Settings.LoadSetting(); - result = PayPalService.ProcessWebhook(settings, Request.Headers, json, PayPalPlusProvider.SystemName); + result = PayPalService.ProcessWebhook(settings, Request.Headers, json, ProviderSystemName); } - catch (Exception exception) + catch (Exception ex) { - Logger.Log(LogLevel.Warning, exception, null, null); + Logger.Log(LogLevel.Warning, ex, null, null); } return new HttpStatusCodeResult(result); } - } + + protected bool SaveConfigurationModel( + ApiConfigurationModel model, + FormCollection form, + Action map = null) + { + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + + var oldClientId = settings.ClientId; + var oldSecret = settings.Secret; + var oldProfileId = settings.ExperienceProfileId; + + var validator = new PayPalApiConfigValidator(T, x => + { + return storeScope == 0 || storeDependingSettingHelper.IsOverrideChecked(settings, x, form); + }); + + validator.Validate(model, ModelState); + + // Additional validation. + if (ModelState.IsValid) + { + // PayPal review: check if credentials are valid. + var credentialChanged = model.ClientId.HasValue() && model.Secret.HasValue() && + (!model.ClientId.IsCaseInsensitiveEqual(settings.ClientId) || !model.Secret.IsCaseInsensitiveEqual(settings.Secret)); + + if (credentialChanged) + { + var session = new PayPalSessionData { ProviderSystemName = ProviderSystemName }; + var tmpSettings = new PayPalApiSettingsBase + { + UseSandbox = model.UseSandbox, + ClientId = model.ClientId, + Secret = model.Secret + }; + + var result = PayPalService.EnsureAccessToken(session, tmpSettings); + if (!result.Success) + { + ModelState.AddModelError("", T("Plugins.SmartStore.PayPal.InvalidCredentials")); + ModelState.AddModelError("", result.ErrorMessage); + } + } + } + + if (!ModelState.IsValid) + { + return false; + } + + ModelState.Clear(); + model.TransactMode = TransactMode.AuthorizeAndCapture; + + MiniMapper.Map(model, settings); + settings.ApiAccountName = model.ApiAccountName.TrimSafe(); + settings.ApiAccountPassword = model.ApiAccountPassword.TrimSafe(); + settings.ClientId = model.ClientId.TrimSafe(); + settings.ExperienceProfileId = model.ExperienceProfileId.TrimSafe(); + settings.Secret = model.Secret.TrimSafe(); + settings.Signature = model.Signature.TrimSafe(); + settings.WebhookId = model.WebhookId.TrimSafe(); + + // Additional mapping. + map?.Invoke(settings); + + // Credentials changed: reset profile and webhook id to avoid errors. + if (!oldClientId.IsCaseInsensitiveEqual(settings.ClientId) || !oldSecret.IsCaseInsensitiveEqual(settings.Secret)) + { + if (oldProfileId.IsCaseInsensitiveEqual(settings.ExperienceProfileId)) + { + settings.ExperienceProfileId = null; + } + + settings.WebhookId = null; + } + + using (Services.Settings.BeginScope()) + { + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } + + using (Services.Settings.BeginScope()) + { + // Multistore context not possible, see IPN handling. + Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); + } + + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + return true; + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs index 0d4d6a7605..2605fb973e 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs @@ -21,14 +21,15 @@ public PayPalStandardController( IPaymentService paymentService, IOrderService orderService, IOrderProcessingService orderProcessingService) : base( - PayPalStandardProvider.SystemName, paymentService, orderService, orderProcessingService) { } - [AdminAuthorize, ChildActionOnly, LoadSetting] + protected override string ProviderSystemName => PayPalStandardProvider.SystemName; + + [AdminAuthorize, ChildActionOnly, LoadSetting] public ActionResult Configure(PayPalStandardPaymentSettings settings, int storeScope) { var model = new PayPalStandardConfigurationModel(); @@ -100,8 +101,8 @@ public ActionResult PDTHandler(FormCollection form) var total = decimal.Zero; string response; - var provider = PaymentService.LoadPaymentMethodBySystemName(SystemName, true); - var processor = (provider != null ? provider.Value as PayPalStandardProvider : null); + var provider = PaymentService.LoadPaymentMethodBySystemName(PayPalStandardProvider.SystemName, true); + var processor = provider != null ? provider.Value as PayPalStandardProvider : null; if (processor == null) { Logger.Warn(null, T("Plugins.Payments.PayPal.NoModuleLoading", "PDTHandler")); diff --git a/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs b/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs index 9d75b41f77..cf3cc794ac 100644 --- a/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs @@ -8,7 +8,7 @@ namespace SmartStore.PayPal { - public class DependencyRegistrar : IDependencyRegistrar + public class DependencyRegistrar : IDependencyRegistrar { public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { @@ -27,7 +27,13 @@ public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, b builder.RegisterType() .AsActionFilterFor(x => x.Completed()) .InstancePerRequest(); - } + + builder.RegisterType() + .AsActionFilterFor() + .InstancePerRequest(); + + //builder.RegisterType().AsActionFilterFor(); + } } public int Order diff --git a/src/Plugins/SmartStore.PayPal/Description.txt b/src/Plugins/SmartStore.PayPal/Description.txt index 6306509f56..9cf1944dc0 100644 --- a/src/Plugins/SmartStore.PayPal/Description.txt +++ b/src/Plugins/SmartStore.PayPal/Description.txt @@ -2,8 +2,8 @@ Description: Provides the PayPal payment methods PayPal Standard, PayPal Direct, PayPal Express and PayPal PLUS. SystemName: SmartStore.PayPal Group: Payment -Version: 3.2.1 -MinAppVersion: 3.2.1 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.PayPal.dll ResourceRootKey: Plugins.SmartStore.PayPal diff --git a/src/Plugins/SmartStore.PayPal/Events/InstalmentsMessageZoneEventConsumer.cs b/src/Plugins/SmartStore.PayPal/Events/InstalmentsMessageZoneEventConsumer.cs new file mode 100644 index 0000000000..8dd4c31fc1 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Events/InstalmentsMessageZoneEventConsumer.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Web; +using Newtonsoft.Json; +using SmartStore.ComponentModel; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Events; +using SmartStore.Core.Logging; +using SmartStore.PayPal.Providers; +using SmartStore.PayPal.Services; +using SmartStore.Services; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Directory; +using SmartStore.Services.Localization; +using SmartStore.Services.Messages; +using SmartStore.Services.Orders; +using SmartStore.Templating; +using SmartStore.Utilities; + +namespace SmartStore.PayPal.Events +{ + public class InstalmentsMessageZoneEventConsumer : IConsumer + { + private static string[] _templateNames = new string[] + { + MessageTemplateNames.OrderCancelledCustomer, + MessageTemplateNames.OrderCompletedCustomer, + MessageTemplateNames.OrderPlacedCustomer, + MessageTemplateNames.OrderPlacedStoreOwner + }; + + private readonly ICommonServices _services; + private readonly HttpContextBase _httpContext; + private readonly Lazy _orderService; + private readonly Lazy _genericAttributeService; + private readonly Lazy _currencyService; + private readonly Lazy _languageService; + private readonly Lazy _priceFormatter; + + public InstalmentsMessageZoneEventConsumer( + ICommonServices services, + HttpContextBase httpContext, + Lazy orderService, + Lazy genericAttributeService, + Lazy currencyService, + Lazy languageService, + Lazy priceFormatter) + { + _services = services; + _httpContext = httpContext; + _orderService = orderService; + _genericAttributeService = genericAttributeService; + _currencyService = currencyService; + _languageService = languageService; + _priceFormatter = priceFormatter; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public void HandleEvent(MessageModelCreatedEvent message) + { + if (!_templateNames.Contains(message.MessageContext.MessageTemplate.Name)) + { + return; + } + + try + { + dynamic model = message.Model; + if (model?.Order == null) + { + return; + } + + var paymentMethod = (string)model.Order.PaymentMethodSystemName; + if (!paymentMethod.IsCaseInsensitiveEqual(PayPalInstalmentsProvider.SystemName)) + { + return; + } + + var orderId = (int)model.Order.ID; + var storeId = (int)model.Order.StoreId; + + var order = _orderService.Value.GetOrderById(orderId); + if (order == null) + { + return; + } + + // Note, the order attribute has probably not been created at this time! + decimal? costs = null; + decimal? total = null; + var state = _httpContext.Session.SafeGetValue(CheckoutState.CheckoutStateSessionKey); + + if (state != null && state.CustomProperties.ContainsKey(PayPalInstalmentsProvider.SystemName)) + { + var session = state.CustomProperties.Get(PayPalInstalmentsProvider.SystemName) as PayPalSessionData; + costs = session.FinancingCosts; + total = session.TotalInclFinancingCosts; + } + else + { + var str = _genericAttributeService.Value.GetAttribute(nameof(Order), orderId, PayPalInstalmentsOrderAttribute.Key, storeId); + if (str.HasValue()) + { + var orderAttribute = JsonConvert.DeserializeObject(str); + costs = orderAttribute.FinancingCosts; + total = orderAttribute.TotalInclFinancingCosts; + } + } + + if (costs.HasValue && total.HasValue) + { + var store = _services.StoreService.GetStoreById(storeId); + var language = _languageService.Value.GetLanguageById(message.MessageContext.LanguageId ?? order.CustomerLanguageId); + var targetCurrency = _currencyService.Value.GetCurrencyByCode(order.CustomerCurrencyCode) ?? store.PrimaryStoreCurrency; + + var convertedCosts = _currencyService.Value.ConvertFromPrimaryStoreCurrency(costs.Value, targetCurrency, store); + var convertedTotal = _currencyService.Value.ConvertFromPrimaryStoreCurrency(total.Value, targetCurrency, store); + + var formattedFinancingCosts = _priceFormatter.Value.FormatPrice(convertedCosts, true, targetCurrency, language, false, false); + var formattedTotalInclFinancingCosts = _priceFormatter.Value.FormatPrice(convertedTotal, true, targetCurrency, language, false, false); + + model.PayPalInstalments = new HybridExpando(); + model.PayPalInstalments = new Dictionary + { + ["FormattedFinancingCosts"] = formattedFinancingCosts, + ["FormattedTotalInclFinancingCosts"] = formattedTotalInclFinancingCosts, + }; + } + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + + public void HandleEvent(ZoneRenderingEvent message) + { + if (!message.ZoneName.IsCaseInsensitiveEqual("order_summary_after")) + { + return; + } + + try + { + var ppInstalments = message.Evaluate("PayPalInstalments"); + if (ppInstalments != null) + { + var liquidPath = CommonHelper.MapPath("~/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.liquid"); + var content = File.ReadAllText(liquidPath); + + message.InjectContent(content.EmptyNull().Trim()); + } + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs b/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs index 13e6e80e1a..0f141e5461 100644 --- a/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs +++ b/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs @@ -16,24 +16,24 @@ public static string GetPayPalUrl(this PayPalSettingsBase settings) "https://www.paypal.com/cgi-bin/webscr"; } - public static PayPalSessionData GetPayPalState(this HttpContextBase httpContext, string key) + public static PayPalSessionData GetPayPalState(this HttpContextBase httpContext, string providerSystemName) { - Guard.NotEmpty(key, nameof(key)); + Guard.NotEmpty(providerSystemName, nameof(providerSystemName)); var state = httpContext.GetCheckoutState(); - if (!state.CustomProperties.ContainsKey(key)) + if (!state.CustomProperties.ContainsKey(providerSystemName)) { - state.CustomProperties.Add(key, new PayPalSessionData()); + state.CustomProperties.Add(providerSystemName, new PayPalSessionData { ProviderSystemName = providerSystemName }); } - var session = state.CustomProperties.Get(key) as PayPalSessionData; + var session = state.CustomProperties.Get(providerSystemName) as PayPalSessionData; return session; } public static PayPalSessionData GetPayPalState( this HttpContextBase httpContext, - string key, + string providerSystemName, Customer customer, int storeId, IGenericAttributeService genericAttributeService) @@ -42,13 +42,13 @@ public static PayPalSessionData GetPayPalState( Guard.NotNull(customer, nameof(customer)); Guard.NotNull(genericAttributeService, nameof(genericAttributeService)); - var session = httpContext.GetPayPalState(key); + var session = httpContext.GetPayPalState(providerSystemName); if (session.AccessToken.IsEmpty() || session.PaymentId.IsEmpty()) { try { - var str = customer.GetAttribute(key + ".SessionData", genericAttributeService, storeId); + var str = customer.GetAttribute(providerSystemName + ".SessionData", genericAttributeService, storeId); if (str.HasValue()) { var storedSessionData = JsonConvert.DeserializeObject(str); diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalFilter.cs new file mode 100644 index 0000000000..5d313429e9 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalFilter.cs @@ -0,0 +1,48 @@ +using System; +using System.Web.Mvc; +using SmartStore.PayPal.Settings; +using SmartStore.Web.Framework.UI; + +namespace SmartStore.PayPal.Filters +{ + //public class PayPalFilter : IActionFilter + //{ + // private readonly Lazy _widgetProvider; + // private readonly Lazy _payPalInstalmentsSettings; + + // public PayPalFilter( + // Lazy widgetProvider, + // Lazy payPalInstalmentsSettings) + // { + // _widgetProvider = widgetProvider; + // _payPalInstalmentsSettings = payPalInstalmentsSettings; + // } + + // public void OnActionExecuting(ActionExecutingContext filterContext) + // { + // } + + // public void OnActionExecuted(ActionExecutedContext filterContext) + // { + // if (filterContext.IsChildAction) + // return; + + // if (!filterContext.Result.IsHtmlViewResult()) + // return; + + // if (filterContext.HttpContext.Request.HttpMethod != "GET") + // return; + + // // Promotion for instalments payment. + // if (_payPalInstalmentsSettings.Value.Promote && _payPalInstalmentsSettings.Value.PromotionWidgetZones.HasValue()) + // { + // _widgetProvider.Value.RegisterAction( + // _payPalInstalmentsSettings.Value.PromotionWidgetZones.SplitSafe(","), + // "Promote", + // "PayPalInstalments", + // new { area = Plugin.SystemName }, + // _payPalInstalmentsSettings.Value.PromotionDisplayOrder); + // } + // } + //} +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalInstalmentsCheckoutFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalInstalmentsCheckoutFilter.cs new file mode 100644 index 0000000000..3ea92f439d --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalInstalmentsCheckoutFilter.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Web.Mvc; +using Newtonsoft.Json; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Logging; +using SmartStore.PayPal.Providers; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.Services; +using SmartStore.Services.Common; +using SmartStore.Services.Customers; +using SmartStore.Web.Framework.UI; + +namespace SmartStore.PayPal.Filters +{ + public class PayPalInstalmentsCheckoutFilter : IActionFilter + { + private readonly ICommonServices _services; + private readonly Lazy _payPalService; + private readonly Lazy _genericAttributeService; + private readonly Lazy _widgetProvider; + + public PayPalInstalmentsCheckoutFilter( + ICommonServices services, + Lazy payPalService, + Lazy genericAttributeService, + Lazy widgetProvider) + { + _services = services; + _payPalService = payPalService; + _genericAttributeService = genericAttributeService; + _widgetProvider = widgetProvider; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + if (filterContext?.ActionDescriptor == null || filterContext?.HttpContext?.Request == null) + { + return; + } + + var action = filterContext.ActionDescriptor.ActionName; + if (action.IsCaseInsensitiveEqual("Confirm")) + { + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + var selectedMethod = customer.GetAttribute(SystemCustomerAttributeNames.SelectedPaymentMethod, store.Id); + + if (!selectedMethod.IsCaseInsensitiveEqual(PayPalInstalmentsProvider.SystemName)) + { + return; + } + + var urlHelper = new UrlHelper(filterContext.HttpContext.Request.RequestContext); + var settings = _services.Settings.LoadSetting(store.Id); + var session = filterContext.HttpContext.GetPayPalState(PayPalInstalmentsProvider.SystemName); + + var protocol = store.SslEnabled ? "https" : "http"; + var returnUrl = urlHelper.Action("CheckoutReturn", "PayPalInstalments", new { area = Plugin.SystemName }, protocol); + var cancelUrl = urlHelper.Action("CheckoutCancel", "PayPalInstalments", new { area = Plugin.SystemName }, protocol); + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + + // PayPal review: start payment flow again if address has changed. + var paymentData = _payPalService.Value.CreatePaymentData(settings, session, cart, returnUrl, cancelUrl); + var serializedData = JsonConvert.SerializeObject(paymentData); + var dataHash = serializedData.Hash(Encoding.ASCII, false); + var hasEqualPaymentData = session.PaymentDataHash.HasValue() && session.PaymentDataHash == dataHash; + + if (session.ApprovalUrl.IsEmpty() || !hasEqualPaymentData) + { + // Create payment and redirect to PayPal. + session.PayerId = session.PaymentId = session.ApprovalUrl = session.PaymentDataHash = null; + session.FinancingCosts = session.TotalInclFinancingCosts = decimal.Zero; + + var result = _payPalService.Value.EnsureAccessToken(session, settings); + if (result.Success) + { + result = _payPalService.Value.CreatePayment(settings, session, paymentData); + if (result == null) + { + // No payment required. + } + else if (result.Success && result.Json != null) + { + foreach (var link in result.Json.links) + { + if (((string)link.rel).IsCaseInsensitiveEqual("approval_url")) + { + session.PaymentId = result.Id; + session.ApprovalUrl = link.href; + session.PaymentDataHash = dataHash; + break; + } + } + } + else + { + _services.Notifier.Error(result.ErrorMessage); + } + } + + if (session.ApprovalUrl.HasValue()) + { + filterContext.Result = new RedirectResult(session.ApprovalUrl, false); + } + else + { + _genericAttributeService.Value.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, null, store.Id); + + _services.Notifier.Error(_services.Localization.GetResource("Plugins.SmartStore.PayPal.PaymentImpossible")); + + filterContext.Result = new RedirectResult(urlHelper.Action("PaymentMethod", "Checkout", new { area = "" }), false); + } + } + else + { + // Show instalments total and fees. + _widgetProvider.Value.RegisterAction("order_summary_totals_after", "OrderSummaryTotals", "PayPalInstalments", new { area = Plugin.SystemName }); + } + } + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml b/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml index 4c9d5a1eeb..6cdf329dfe 100644 --- a/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml @@ -1,383 +1,401 @@  + + PayPal Zahlungsarten + + + Stellt die PayPal-Zahlungsarten zur Verfügung. + + + PayPal + - - PayPal Zahlungsarten - - - Stellt die PayPal-Zahlungsarten PayPal Standard, PayPal Direct, PayPal Express und PayPal PLUS zur Verfügung. - + + Inklusive Finanzierungsbeispiel + + + Nur Text + - - API Zugang;Datenaustausch;Sonstiges - - - {0}]]> - - - Mitteilung;Ereignis;Ereignis-ID;Status;Betrag;Zahlungs-ID - - - Sonstiges - - - {0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden. - - - Bitte überweisen Sie den fälligen Betrag auf folgendes Konto. - - - Referenz;Bankleitzahl;Bank;BIC;IBAN;Kontoinhaber;Kontonummer;Betrag;Zahlung fällig am;Details - - - Mitteilung wurde ignoriert, da {0} Aufträge mit der Payment-ID {1} gefunden wurden. - - - Währung {0} entspricht nicht der Leitwährung des Shops {1}. - - - Keine Weiterleitungs-URL von PayPal erhalten. - - - Fehlender API Access-Token. Methode {0} für Pfad {1}. - - - Die PayPal Sitzung ist abgelaufen. Bitte versuchen Sie es erneut. - - - PayPal konnte die übermittelten Daten nicht verifizieren. - + + Zugangsdaten + + + {0}]]> + + + Mitteilung;Ereignis;Ereignis-ID;Status;Betrag;Zahlungs-ID + + + Sonstiges + + + {0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden. + + + Bitte überweisen Sie den fälligen Betrag auf folgendes Konto. + + + Referenz;Bankleitzahl;Bank;BIC;IBAN;Kontoinhaber;Kontonummer;Betrag;Zahlung fällig am;Details + + + Mitteilung wurde ignoriert, da {0} Aufträge mit der Payment-ID {1} gefunden wurden. + + + Währung {0} entspricht nicht der Leitwährung des Shops {1}. + + + Keine Weiterleitungs-URL von PayPal erhalten. + + + Fehlender API Access-Token. Methode {0} für Pfad {1}. + + + Die PayPal Sitzung ist abgelaufen. Bitte versuchen Sie es erneut. + + + PayPal konnte die übermittelten Daten nicht verifizieren. + + + Promotion + + + Die von Ihnen gewählte Zahlungsart kann Ihnen leider nicht angeboten werden. Bitte wählen Sie eine andere Zahlungsart aus. + + + Die Zugangsdaten sind ungültig. + - - Client-ID - - - Legt die API Client-ID fest. - - - Secret - - - Legt die API Secret fest. - - - Bitte geben Sie Ihre API Zugangsdaten ein. - - - Experience Profil-ID - - - Legt die Experience Profil-ID fest. Das Profil beinhaltet globale Daten wie z.B. Shop-Name und -Logo. Ein Profil braucht nur einmalig angelegt werden. Über den Button können Sie ein neues Profil erstellen oder ein vorhandenes aktualisieren. - - - Webhook-ID - - - Legt die Webhook-ID fest. Über einen Webhook sendet PayPal Statusänderungen von Zahlungen an Ihren Shop, wodurch der Zahlungsstatus Ihrer Aufträge automatisch aktualisert werden kann. - - - - - - Sandbox benutzen - - - Aktiviert die Nutzung der PayPal Sandbox (Testumgebung). - - - Zahlungsstatus über IPN ändern - - - Legt fest, ob durch eingehende IPNs der Zahlunsgstatus eines Auftrags angepasst werden soll. - - - Transaktionsmodus - - - Bestimmen Sie den Transaktionsmodus. - - - API Benutzername - - - Geben Sie Ihren API Benutzername ein. - - - API Passwort - - - Geben Sie Ihr API Passwort ein. - - - Signatur - - - Geben Sie Ihre Signatur ein. - - - PayPal Adresse anzeigen - - - Legt fest, ob die bei PayPal hinterlegte Versandadresse angezeigt werden soll (address_override). - - - Versandadresse erforderlich - - - Legt fest, ob eine Versandadresse erforderlich ist (no_shipping). - - - Die Zahlart ist nicht aktiv ({0}). - - - PayPal IPN. Bestellung konnte nicht gefunden werden. - - - Ungültige PayPal IPN. Der Wert von Feld '{0}' fehlt. - - - PayPal IPN. Benachrichtigung konnte nicht abgerufen werden. - - - Teilerstattung von {0} - - - Zahlartgebühren - - - Rabatt - - - Angewendeter Geschenkgutschein - - - + + Client-ID + + + Legt die API Client-ID fest. + + + Secret + + + Legt die API Secret fest. + + + Bitte geben Sie Ihre API Zugangsdaten ein. + + + Experience Profil-ID + + + Legt die Experience Profil-ID fest. Das Profil beinhaltet globale Daten wie z.B. Shop-Name und -Logo. Ein Profil braucht nur einmalig angelegt werden. Über den Button können Sie ein neues Profil erstellen oder ein vorhandenes aktualisieren. + + + Webhook-ID + + + Legt die Webhook-ID fest. Über einen Webhook sendet PayPal Statusänderungen von Zahlungen an Ihren Shop, wodurch der Zahlungsstatus Ihrer Aufträge automatisch aktualisert werden kann. + - - PayPal Express - - - - - - Stellen Sie bitte sicher, dass PayPal die Primärwährung Ihres Shops unterstützt, falls Sie dieses Gateway benutzen!

+ + + + Sandbox benutzen + + + Aktiviert die Nutzung der PayPal Sandbox (Testumgebung). + + + Zahlungsstatus über IPN ändern + + + Legt fest, ob durch eingehende IPNs der Zahlunsgstatus eines Auftrags angepasst werden soll. + + + Transaktionsmodus + + + Bestimmen Sie den Transaktionsmodus. + + + API Benutzername + + + Geben Sie Ihren API Benutzername ein. + + + API Passwort + + + Geben Sie Ihr API Passwort ein. + + + Signatur + + + Geben Sie Ihre Signatur ein. + + + PayPal Adresse anzeigen + + + Legt fest, ob die bei PayPal hinterlegte Versandadresse angezeigt werden soll (address_override). + + + Versandadresse erforderlich + + + Legt fest, ob eine Versandadresse erforderlich ist (no_shipping). + + + Die Zahlart ist nicht aktiv ({0}). + + + PayPal IPN. Bestellung konnte nicht gefunden werden. + + + Ungültige PayPal IPN. Der Wert von Feld '{0}' fehlt. + + + PayPal IPN. Benachrichtigung konnte nicht abgerufen werden. + + + Teilerstattung von {0} + + + Zahlartgebühren + + + Rabatt + + + Angewendeter Geschenkgutschein + + + + + + PayPal Express + + + + + + Stellen Sie bitte sicher, dass PayPal die Primärwährung Ihres Shops unterstützt, falls Sie dieses Gateway benutzen!

  1. Loggen Sie sich in Ihr PayPal-Konto ein.
  2. -
  3. Klicken Sie auf den Register Mein Profil.
  4. -
  5. Klicken Sie unter Verkäufer/Händler auf API-Zugriff.
  6. -
  7. Klicken Sie auf API-Berechtigung anfordern.
  8. -
  9. Wählen Sie die Option Fordern Sie eine API-Signature an.
  10. -
  11. Klicken Sie auf Zustimmen und senden.
  12. +
  13. Klicken Sie auf den Register Mein Profil.
  14. +
  15. Klicken Sie unter Verkäufer/Händler auf API-Zugriff.
  16. +
  17. Klicken Sie auf API-Berechtigung anfordern.
  18. +
  19. Wählen Sie die Option Fordern Sie eine API-Signature an.
  20. +
  21. Klicken Sie auf Zustimmen und senden.

Aktivieren Sie optional den PayPal-Service zum Empfang von IPN-Nachrichten (Instant Payment Notification):

  1. In Ihr Premier- oder Business-Konto einloggen.
  2. -
  3. Auf den Register Mein Profil klicken.
  4. -
  5. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
  6. -
  7. Sofortige Zahlungsbestätigung klicken.
  8. -
  9. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
  10. -
  11. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalExpress/IPNHandler) eingeben.
  12. -
  13. Speichern klicken. Danach sollten Sie eine Nachricht über die erfolgreiche Aktivierung von IPN erhalten.
  14. +
  15. Auf den Register Mein Profil klicken.
  16. +
  17. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
  18. +
  19. Sofortige Zahlungsbestätigung klicken.
  20. +
  21. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
  22. +
  23. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalExpress/IPNHandler) eingeben.
  24. +
  25. Speichern klicken. Danach sollten Sie eine Nachricht über die erfolgreiche Aktivierung von IPN erhalten.
]]> -
-
- - Wiederkehrende Zahlung - - - Nicht unterstützte Zyklus-Zeit. - - - Sofort abbuchen - - - Autorisierung sofort, Abbuchung später - - - Button im Miniwarenkorb anzeigen - - - Legt fest, ob der Checkout-Button im Miniwarenkorb angezeigt werden soll. - - - Bestätigte E-Mail-Adresse - - - Legt fest, ob es sich bei der verwendeten E-Mail-Adresse, um eine von PayPal bestätigte Adresse handeln muss. - - - Anzeige der Lieferadresse unterdrücken - - - Legt fest, ob auf der PayPal Seite die Lieferadresse angezeigt werden soll. - - - Zeitlimit für API-Callback - - - Legt das Zeitlimit für API-Callbacks fest. - - - Versandkosten (Fallback) - - - Legt die Versandkosten fest, falls keine anderen ermittelt werden können. - -
-
- - - - PayPal Direct - - - - - - Stellen Sie bitte sicher, dass PayPal die Primärwährung Ihres Shops unterstützt, falls Sie dieses Gateway benutzen!

+
+
+ + Wiederkehrende Zahlung + + + Nicht unterstützte Zyklus-Zeit. + + + Sofort abbuchen + + + Autorisierung sofort, Abbuchung später + + + Button im Miniwarenkorb anzeigen + + + Legt fest, ob der Checkout-Button im Miniwarenkorb angezeigt werden soll. + + + Bestätigte E-Mail-Adresse + + + Legt fest, ob es sich bei der verwendeten E-Mail-Adresse, um eine von PayPal bestätigte Adresse handeln muss. + + + Anzeige der Lieferadresse unterdrücken + + + Legt fest, ob auf der PayPal Seite die Lieferadresse angezeigt werden soll. + + + Zeitlimit für API-Callback + + + Legt das Zeitlimit für API-Callbacks fest. + + + Versandkosten (Fallback) + + + Legt die Versandkosten fest, falls keine anderen ermittelt werden können. + +
+
+ + + + PayPal Direct + + + + + + Stellen Sie bitte sicher, dass PayPal die Primärwährung Ihres Shops unterstützt, falls Sie dieses Gateway benutzen!

  1. Loggen Sie sich in Ihr PayPal-Konto ein.
  2. -
  3. Klicken Sie auf den Register Mein Profil.
  4. -
  5. Klicken Sie unter Verkäufer/Händler auf API-Zugriff.
  6. -
  7. Klicken Sie auf API-Berechtigung anfordern.
  8. -
  9. Wählen Sie die Option Fordern Sie eine API-Signature an.
  10. -
  11. Klicken Sie auf Zustimmen und senden.
  12. +
  13. Klicken Sie auf den Register Mein Profil.
  14. +
  15. Klicken Sie unter Verkäufer/Händler auf API-Zugriff.
  16. +
  17. Klicken Sie auf API-Berechtigung anfordern.
  18. +
  19. Wählen Sie die Option Fordern Sie eine API-Signature an.
  20. +
  21. Klicken Sie auf Zustimmen und senden.

Aktivieren Sie optional den PayPal-Service zum Empfang von IPN-Nachrichten (Instant Payment Notification):

  1. In Ihr Premier- oder Business-Konto einloggen.
  2. -
  3. Auf den Register Mein Profil klicken.
  4. -
  5. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
  6. -
  7. Sofortige Zahlungsbestätigung klicken.
  8. -
  9. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
  10. -
  11. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalDirect/IPNHandler) eingeben.
  12. -
  13. Speichern klicken. Danach sollten Sie eine Nachricht über die erfolgreiche Aktivierung von IPN erhalten.
  14. +
  15. Auf den Register Mein Profil klicken.
  16. +
  17. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
  18. +
  19. Sofortige Zahlungsbestätigung klicken.
  20. +
  21. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
  22. +
  23. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalDirect/IPNHandler) eingeben.
  24. +
  25. Speichern klicken. Danach sollten Sie eine Nachricht über die erfolgreiche Aktivierung von IPN erhalten.
]]> -
-
- - Wiederkehrende Zahlung - - - Nicht unterstützte Zyklus-Zeit. - - - Sofort abbuchen - - - Autorisierung sofort, Abbuchung später - -
-
+
+
+ + Wiederkehrende Zahlung + + + Nicht unterstützte Zyklus-Zeit. + + + Sofort abbuchen + + + Autorisierung sofort, Abbuchung später + +
+
- - PayPal Standard - - - - - - Stellen Sie bitte sicher, dass PayPal die Primärwährung Ihres Shops unterstützt, falls Sie dieses Gateway benutzen.

+ + PayPal Standard + + + + + + Stellen Sie bitte sicher, dass PayPal die Primärwährung Ihres Shops unterstützt, falls Sie dieses Gateway benutzen.

Sie müssen PDT (Payment Data Transfer) und die automatische Rückleitung in Ihrem PayPal-Profil aktivieren, um PDT nutzen zu können. Ferner benötigen Sie einen PDT-Identitäts-Token, der für jede PDT-Kommunikation mit PayPal erforderlich ist. Folgen Sie den Schritten, um Ihr Konto für PDT zu konfigurieren:

  1. Loggen Sie sich in Ihr PayPal-Konto ein.
  2. -
  3. Klicken Sie auf den Register Mein Profil.
  4. -
  5. Klicken Sie unter Verkäufer/Händler auf Website-Einstellungen.
  6. -
  7. Aktivieren Sie Automatische Rückleitung.
  8. -
  9. Als Rückleitungs-URL geben Sie bitte die Seite Ihres Shops ein, die die Transaktions-ID von PayPal nach einer Kundenzahlung empfängt (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/PDTHandler).
  10. -
  11. Aktivieren Sie Übertragung der Zahlungsdaten.
  12. -
  13. Klicken Sie auf Speichern.
  14. -
  15. Klicken Sie unter Verkäufer/Händler auf Website-Einstellungen.
  16. -
  17. Scrollen Sie runter zu Übertragung der Zahlungsdaten, um den PDT-Identitäts-Token einzusehen.
  18. +
  19. Klicken Sie auf den Register Mein Profil.
  20. +
  21. Klicken Sie unter Verkäufer/Händler auf Website-Einstellungen.
  22. +
  23. Aktivieren Sie Automatische Rückleitung.
  24. +
  25. Als Rückleitungs-URL geben Sie bitte die Seite Ihres Shops ein, die die Transaktions-ID von PayPal nach einer Kundenzahlung empfängt (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/PDTHandler).
  26. +
  27. Aktivieren Sie Übertragung der Zahlungsdaten.
  28. +
  29. Klicken Sie auf Speichern.
  30. +
  31. Klicken Sie unter Verkäufer/Händler auf Website-Einstellungen.
  32. +
  33. Scrollen Sie runter zu Übertragung der Zahlungsdaten, um den PDT-Identitäts-Token einzusehen.

Es gibt zwei Wege, um IPN-Nachrichten (Instant Payment Notification) zu empfangen (optional):

    -
  1. Unten IPN aktivieren abhaken. Die URL zu Ihrem IPN-Handler wird dann über die Anfrage übermittelt.
  2. +
  3. Unten IPN aktivieren abhaken. Die URL zu Ihrem IPN-Handler wird dann über die Anfrage übermittelt.
  4. Ihr PayPal-Konto für diesen Service konfigurieren. Schritte hierzu:
    1. In Ihr Premier- oder Business-Konto einloggen.
    2. -
    3. Auf den Register Mein Profil klicken.
    4. -
    5. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
    6. -
    7. Zurück zu Mein Profil und auf Sofortige Zahlungsbestätigung klicken.
    8. -
    9. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
    10. -
    11. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/IPNHandler) eingeben.
    12. -
    13. Speichern klicken. Danach sollten Sie eine Nachricht über die erfolgreiche Aktivierung von IPN erhalten.
    14. +
    15. Auf den Register Mein Profil klicken.
    16. +
    17. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
    18. +
    19. Zurück zu Mein Profil und auf Sofortige Zahlungsbestätigung klicken.
    20. +
    21. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
    22. +
    23. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/IPNHandler) eingeben.
    24. +
    25. Speichern klicken. Danach sollten Sie eine Nachricht über die erfolgreiche Aktivierung von IPN erhalten.
]]> -
-
- - Nach Bestätigung der Bestellung werden Sie direkt zu PayPal weitergeleitet. Halten Sie bitte Ihre Daten für das Online-Banking bereit. - - - Business E-Mail - - - PayPal Business E-Mail festlegen. - - - PDT-Identitäts-Token - - - PDT-Identitäts-Token festlegen. - - - Gesamtbetrag überprüfen - - - Aktivieren Sie diese Option, falls der durch PayPal übermittelte Gesamtbetrag überprüft werden soll. - - - Nur warnen - - - Legt fest, ob im Falle einer Nichtübereinstimmung des Gesamtbetrags lediglich eine Auftragsnotiz angelegt werden soll. Ansonsten wird ein Fehler erzeugt und der Zahlungsstatus wird nicht aktualisiert. - - - Produktbezeichnungen und Einzelpreise übermitteln - - - Aktivieren Sie diese Option, falls Produktbezeichnungen und Einzelpreise an PayPal übermittelt werden sollen. - - - IPN aktivieren - - - Aktivieren Sie diese Option, falls IPN (Instant Payment Notification) aktiviert werden soll. - - - Bitte leer lassen, falls die URL des Standard-IPN-Handlers verwendet werden soll. - - - IPN-Handler (URL) - - - URL zum IPN-Handler festlegen. - - - PayPal Standard PDT. Fehler beim Abruf von mc_gross. - - - PayPal Standard PDT. Der übermittelte Gesamtbetrag {0} entspricht nicht dem des Shops {1} (ungerundet {2}). Die Differenz beträgt {3}. - - - PayPal Standard PDT ist fehlgeschlagen. - - - Unvollständige Zugangsdaten für die PayPal API. Bitte geben Sie die erforderlichen Zugangsdaten im Konfigurationsbereich ein. - - - Versandgebühren - - - Umsatzsteuer - - - - + + + Nach Bestätigung der Bestellung werden Sie direkt zu PayPal weitergeleitet. Halten Sie bitte Ihre Daten für das Online-Banking bereit. + + + Business E-Mail + + + PayPal Business E-Mail festlegen. + + + PDT-Identitäts-Token + + + PDT-Identitäts-Token festlegen. + + + Gesamtbetrag überprüfen + + + Aktivieren Sie diese Option, falls der durch PayPal übermittelte Gesamtbetrag überprüft werden soll. + + + Nur warnen + + + Legt fest, ob im Falle einer Nichtübereinstimmung des Gesamtbetrags lediglich eine Auftragsnotiz angelegt werden soll. Ansonsten wird ein Fehler erzeugt und der Zahlungsstatus wird nicht aktualisiert. + + + Produktbezeichnungen und Einzelpreise übermitteln + + + Aktivieren Sie diese Option, falls Produktbezeichnungen und Einzelpreise an PayPal übermittelt werden sollen. + + + IPN aktivieren + + + Aktivieren Sie diese Option, falls IPN (Instant Payment Notification) aktiviert werden soll. + + + Bitte leer lassen, falls die URL des Standard-IPN-Handlers verwendet werden soll. + + + IPN-Handler (URL) + + + URL zum IPN-Handler festlegen. + + + PayPal Standard PDT. Fehler beim Abruf von mc_gross. + + + PayPal Standard PDT. Der übermittelte Gesamtbetrag {0} entspricht nicht dem des Shops {1} (ungerundet {2}). Die Differenz beträgt {3}. + + + PayPal Standard PDT ist fehlgeschlagen. + + + Unvollständige Zugangsdaten für die PayPal API. Bitte geben Sie die erforderlichen Zugangsdaten im Konfigurationsbereich ein. + + + Versandgebühren + + + Umsatzsteuer + + + + - - -
-
+
+
+
+
- - PayPal PLUS - - - - - - PayPal PLUS bietet die Möglichkeit per PayPal, Kreditkarte, Lastschriftverfahren und per Rechnung zu bezahlen. + + PayPal PLUS + + + + + + PayPal PLUS bietet die Möglichkeit per PayPal, Kreditkarte, Lastschriftverfahren und per Rechnung zu bezahlen. Die Bezahlung auf Rechnung steht möglicherweise nicht allen Händlern zur Verfügung. PayPal PLUS unterstützt nur den Transaktionstyp der sofortigen Buchung und ist derzeit nur in Deutschland verfügbar.

PayPal PLUS ersetzt die Zahlartenliste im Kassenbereich durch die PayPal Payment Wall. Sie können der Payment Wall jedoch bis zu 10 weitere Offline- bzw. Weiterleitungszahlarten hinzufügen.

So richten Sie PayPal PLUS ein:

    -
  1. Legen Sie unter My Apps & Credentials eine neue REST API Anwendung an. -Unter Manage your applications finden Sie weitere Informationen zum Thema PayPal Anwendungen (Englisch).
  2. -
  3. Klicken Sie auf den Namen der Anwendung und tragen Sie Client-ID und Secret weiter unten auf dieser Seite ein. Speichern.
  4. -
  5. Optional: Klicken Sie unten bei Experience Profil-ID auf Hinzufügen, um ein Profil zu erstellen. Ihre Shop-Daten (Name, Logo etc.) werden Käufern dadurch beim Bezahlen auf den PayPal-Seiten angezeigt.
  6. -
  7. Optional: Klicken Sie unten bei Webhook-ID auf Hinzufügen, um einen Webhook zu erstellen. Über einen Webhook empfängt der Shop PayPal-Mitteilungen und aktualisiert u.U. den Zahlungsstatus Ihrer Aufträge.
  8. -

Um Bilder zu weiteren Zahlarten festzulegen sind im Content-Ordner des betreffenden Plugins Dateien mit folgendem Namensmuster zu hinterlegen: branding.<Plugin-Systemname|Provider-Systemname>.<jpg|jpeg|png|gif>
Für Vorkasse lautet der Pfad z.B. \Plugins\SmartStore.OfflinePayment\Content\branding.Payments.Prepayment.jpg. Wichtig: Der Shop muss hierfür SSL unterstützen.

]]> -
-
- - Wir können Ihnen momentan leider keine PayPal PLUS Zahlungsart anbieten. Bitte versuchen Sie es später erneut. - - - Entschuldigung, das hat gerade nicht funktioniert. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode. - - - .]]> - - - Weitere Zahlarten - - - Sie können bis zu 5 weitere Zahlarten festlegen. Diese werden zusammen mit den PayPal PLUS Zahlarten im Kassenbereich angeboten. - - - Logo der Zahlarten anzeigen - - - Legt fest, ob für die weiteren Zahlarten deren Logo angezeigt werden soll. SSL erforderlich. - - - Beschreibung der Zahlarten anzeigen - - - Legt fest, ob für die weiteren Zahlarten deren Beschreibung angezeigt werden soll. - -
-
+
  • Legen Sie unter My Apps & Credentials eine neue REST API Anwendung an. +Unter Manage your applications finden Sie weitere Informationen zum Thema PayPal Anwendungen (Englisch).
  • +
  • Klicken Sie auf den Namen der Anwendung und tragen Sie Client-ID und Secret weiter unten auf dieser Seite ein. Speichern.
  • +
  • Optional: Klicken Sie unten bei Experience Profil-ID auf Hinzufügen, um ein Profil zu erstellen. Ihre Shop-Daten (Name, Logo etc.) werden Käufern dadurch beim Bezahlen auf den PayPal-Seiten angezeigt.
  • +
  • Optional: Klicken Sie unten bei Webhook-ID auf Hinzufügen, um einen Webhook zu erstellen. Über einen Webhook empfängt der Shop PayPal-Mitteilungen und aktualisiert u.U. den Zahlungsstatus Ihrer Aufträge.
  • +

    Um Bilder zu weiteren Zahlarten festzulegen sind im Content-Ordner des betreffenden Plugins Dateien mit folgendem Namensmuster zu hinterlegen: branding.<Plugin-Systemname|Provider-Systemname>.<jpg|jpeg|png|gif>
    Für Vorkasse lautet der Pfad z.B. \Plugins\SmartStore.OfflinePayment\Content\branding.Payments.Prepayment.jpg. Wichtig: Der Shop muss hierfür SSL unterstützen.

    ]]> +
    +
    + + Wir können Ihnen momentan leider keine PayPal PLUS Zahlungsart anbieten. Bitte versuchen Sie es später erneut. + + + Entschuldigung, das hat gerade nicht funktioniert. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode. + + + .]]> + + + Weitere Zahlarten + + + Sie können bis zu 5 weitere Zahlarten festlegen. Diese werden zusammen mit den PayPal PLUS Zahlarten im Kassenbereich angeboten. + + + Logo der Zahlarten anzeigen + + + Legt fest, ob für die weiteren Zahlarten deren Logo angezeigt werden soll. SSL erforderlich. + + + Beschreibung der Zahlarten anzeigen + + + Legt fest, ob für die weiteren Zahlarten deren Beschreibung angezeigt werden soll. + +
    +
    + + Ratenzahlung Powered by PayPal + + + + + Ratenzahlung + + + + So richten Sie Ratenzahlung Powered by PayPal ein:

    +
      +
    1. Legen Sie unter My Apps & Credentials eine neue REST API Anwendung an. +Unter Manage your applications finden Sie weitere Informationen zum Thema PayPal Anwendungen (Englisch).
    2. +
    3. Klicken Sie auf den Namen der Anwendung und tragen Sie Client-ID und Secret weiter unten auf dieser Seite ein. Speichern.
    4. +
    5. Optional: Klicken Sie unten bei Experience Profil-ID auf Hinzufügen, um ein Profil zu erstellen. Ihre Shop-Daten (Name, Logo etc.) werden Käufern dadurch beim Bezahlen auf den PayPal-Seiten angezeigt.
    6. +
    7. Optional: Klicken Sie unten bei Webhook-ID auf Hinzufügen, um einen Webhook zu erstellen. Über einen Webhook empfängt der Shop PayPal-Mitteilungen und aktualisiert u.U. den Zahlungsstatus Ihrer Aufträge.
    8. +

    Nach Auftragserteilung dürfen die Auftragssumme und die Auftragspositionen nicht mehr geändert werden!

    ]]> +
    +
    + + Wir ermöglichen Ihnen die Finanzierung Ihres Einkaufs mithilfe der Ratenzahlung Powered by PayPal. In Sekundenschnelle, vollständig online, vorbehaltlich Bonitätsprüfung. + + + Finanzierungskosten + + + Gesamt inkl. Finanzierungskosten + + + Informationen zu möglichen Raten. + + + Finanzierung gewünscht? + + + Finanzierung ab {0} im Monat. + + + Finanzierung ab {0} in {1} monatlichen Raten. + + + Repräsentatives Beispiel gem. § 6a PAngV + + + Plan {0} + + + Nettodarlehensbetrag + + + Fester Sollzinssatz + + + Effektiver Jahreszins + + + Zu zahlender Gesamtbetrag + + + {0} monatliche Raten in Höhe von je + + + letzte Rate zu + + + Zinsbetrag + + + Es liegen keine Finanzierungsdetails vor. + + + Zahlen Sie bequem und einfach in monatlichen Raten. + + + Ihre Ratenzahlung und den passenden Finanzierungsplan können Sie im Rahmen des Bestellprozesses auswählen. Ihr Antrag erfolgt komplett online über Ratenzahlung Powered by PayPal und wird in wenigen Schritten hier im Shop abgeschlossen. + + + Minimaler Finanzierungsbetrag + + + Legt den minimalen Betrag fest, ab dem eine Ratenzahlung angeboten werden soll. + + + Maximaler Finanzierungsbetrag + + + Legt den maximalen Betrag fest, bis zu dem eine Ratenzahlung angeboten werden soll. + + + Auf Produktseiten + + + Legt fest, ob und wie die Ratenzahlung auf Produktseiten beworben werden soll. + + + Auf der Warenkorbseite + + + Legt fest, ob und wie die Ratenzahlung auf der Warenkorbseite beworben werden soll. + + + Bei der Zahlartenauswahl + + + Legt fest, ob und wie die Ratenzahlung bei der Zahlartenauswahl beworben werden soll. + + + Darlehensgeber + + + Firmenname und -anschrift + + +
    +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml b/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml index bc6404c3f9..5e14838769 100644 --- a/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml @@ -1,389 +1,407 @@  + + PayPal Payment Methods + + + Provides the PayPal payment methods. + + + PayPal + - - PayPal Payment Methods - - - Provides the PayPal payment methods PayPal Standard, PayPal Direct, PayPal Express and PayPal PLUS. - + + Including financing example + + + Only text + - - API access;Data exchange;Miscellaneous - - - {0}]]> - - - Message;Event;Event-ID;State;Amount;Payment-ID - - - Miscellaneous - - - The execution of the payment has been declined by PayPal. - - - {0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden. - - - Please transfer the due amount to the following bank account. - - - Reference;Bank routing number;Bank;BIC;IBAN;Account holder;Account number;Amount;Payment due date;Details - - - Message has been ignored because {0} orders with payment ID {1} found. - - - Currency {0} does not equal primary store currency {1}. - - - No redirect URL received from PayPal. - - - Missing API access token. Method {0} for path {1}. - - - The PayPal session expired. Please try again. - - - PayPal was unable to verify the transmitted data. - + + Access data + + + {0}]]> + + + Message;Event;Event-ID;State;Amount;Payment-ID + + + Miscellaneous + + + The execution of the payment has been declined by PayPal. + + + {0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden. + + + Please transfer the due amount to the following bank account. + + + Reference;Bank routing number;Bank;BIC;IBAN;Account holder;Account number;Amount;Payment due date;Details + + + Message has been ignored because {0} orders with payment ID {1} found. + + + Currency {0} does not equal primary store currency {1}. + + + No redirect URL received from PayPal. + + + Missing API access token. Method {0} for path {1}. + + + The PayPal session expired. Please try again. + + + PayPal was unable to verify the transmitted data. + + + Promotion + + + Unfortunately, the payment method you have chosen cannot be offered to you. Please select another payment method. + + + The credentials are invalid. + - - Client ID - - - Specifies the API client ID. - - - Secret - - - Specifies the API secret. - - - Please enter your API credentials. - - - Experience profile ID - - - Specifies the experience profile ID. The profile contains global data like shop name and logo. A profile needs to be created only once. Using the button, you can create a new profile or update an existing one. - - - Webhook ID - - - Specifies the webhook ID. PayPal sends payment status changes via a webhook to your shop, whereby the payment status of your orders can be automatically synced. - + + Client ID + + + Specifies the API client ID. + + + Secret + + + Specifies the API secret. + + + Please enter your API credentials. + + + Experience profile ID + + + Specifies the experience profile ID. The profile contains global data like shop name and logo. A profile needs to be created only once. Using the button, you can create a new profile or update an existing one. + + + Webhook ID + + + Specifies the webhook ID. PayPal sends payment status changes via a webhook to your shop, whereby the payment status of your orders can be automatically synced. + - - - - Use Sandbox - - - Check the box to enable Sandbox (testing environment). - - - IPN may change payment status - - - Specifies whether received IPNs should change the payment status of an order. - - - Transaction mode - - - Specify the payment transaction mode. - - - API Account Name - - - Specify the API account name. - - - API Account Password - - - Specify the API account password. - - - Signature - - - Enter the signature. - - - Show PayPal address - - - Specifies whether to show the shipping address stored at PayPal (address_override). - - - Shipping address required - - - Specifies whether a shipping address is required (no_shipping). - - - The payment method is not active ({0}). - - - PayPal IPN. Recurring payment. - - - PayPal IPN. Order cannot be found. - - - Irregular PayPal IPN. The value of field '{0}' is missing. - - - PayPal IPN. Failed to retrieve notification. - - - Partial refund of {0} - - - Payment method fee - - - Discount - - - Giftcard Applied - - - - - - PayPal Express - - - - - - + + + Use Sandbox + + + Check the box to enable Sandbox (testing environment). + + + IPN may change payment status + + + Specifies whether received IPNs should change the payment status of an order. + + + Transaction mode + + + Specify the payment transaction mode. + + + API Account Name + + + Specify the API account name. + + + API Account Password + + + Specify the API account password. + + + Signature + + + Enter the signature. + + + Show PayPal address + + + Specifies whether to show the shipping address stored at PayPal (address_override). + + + Shipping address required + + + Specifies whether a shipping address is required (no_shipping). + + + The payment method is not active ({0}). + + + PayPal IPN. Recurring payment. + + + PayPal IPN. Order cannot be found. + + + Irregular PayPal IPN. The value of field '{0}' is missing. + + + PayPal IPN. Failed to retrieve notification. + + + Partial refund of {0} + + + Payment method fee + + + Discount + + + Giftcard Applied + + + + + + PayPal Express + + + + + + If you're using this gateway ensure that your primary store currency is supported by Paypal!

    1. Log into your PayPal account.
    2. -
    3. Click on Profile from the Home Menu.
    4. -
    5. Under Account Information choose API Access.
    6. -
    7. Choose Request API credentials.
    8. -
    9. Choose API Signature as the credential type.
    10. -
    11. Enable the checkbox and agree to the license Terms-I agree to the API Certificate Terms of Use and terms incorporated therein and press the Submit button.
    12. +
    13. Click on Profile from the Home Menu.
    14. +
    15. Under Account Information choose API Access.
    16. +
    17. Choose Request API credentials.
    18. +
    19. Choose API Signature as the credential type.
    20. +
    21. Enable the checkbox and agree to the license Terms-I agree to the API Certificate Terms of Use and terms incorporated therein and press the Submit button.

    Before being able to receive IPN (Instant Payment Notification) messages (optional), you'll need to activate this service; follow these steps:

    1. Log in to your Premier or Business account.
    2. Click the Profile subtab.
    3. -
    4. Click Language Encoding and More options and select UTF-8.
    5. -
    6. Click Instant Payment Notification in the Selling Preferences column.
    7. -
    8. Click the Edit IPN Settings button to update your settings.
    9. -
    10. Select Receive IPN messages (enabled) and enter the URL of your IPN handler (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalExpress/IPNHandler).
    11. -
    12. Click Save, and you should get a message that you have successfully activated IPN.
    13. +
    14. Click Language Encoding and More options and select UTF-8.
    15. +
    16. Click Instant Payment Notification in the Selling Preferences column.
    17. +
    18. Click the Edit IPN Settings button to update your settings.
    19. +
    20. Select Receive IPN messages (enabled) and enter the URL of your IPN handler (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalExpress/IPNHandler).
    21. +
    22. Click Save, and you should get a message that you have successfully activated IPN.
    ]]> -
    -
    - - recurring payment - - - Not supported cycle period. - - - Immediately debit - - - Authorize immediately, debit later - - - Show button in mini shopping cart - - - Soecifies to show the checkout button in the mini shopping cart. - - - Confirmed email address - - - Choose whether the used email address has to be confirmed by PayPal. - - - Disable display of the shipping address. - - - Disables display of the shipping address. - - - Timeout for API callback - - - Determines the timeout for API callbacks. - - - Shipping costs (fallback) - - - Determines shipping costs if no other could be obtained. - -
    -
    + +
    + + recurring payment + + + Not supported cycle period. + + + Immediately debit + + + Authorize immediately, debit later + + + Show button in mini shopping cart + + + Soecifies to show the checkout button in the mini shopping cart. + + + Confirmed email address + + + Choose whether the used email address has to be confirmed by PayPal. + + + Disable display of the shipping address. + + + Disables display of the shipping address. + + + Timeout for API callback + + + Determines the timeout for API callbacks. + + + Shipping costs (fallback) + + + Determines shipping costs if no other could be obtained. + + + - - PayPal Direct - - - - - - If you're using this gateway ensure that your primary store currency is supported by Paypal!

    + + PayPal Direct + + + + + + If you're using this gateway ensure that your primary store currency is supported by Paypal!

    1. Log into your PayPal account.
    2. -
    3. Click on Profile from the Home Menu.
    4. -
    5. Under Account Information choose API Access.
    6. -
    7. Choose Request API credentials.
    8. -
    9. Choose API Signature as the credential type.
    10. -
    11. Enable the checkbox and agree to the license Terms-I agree to the API Certificate Terms of Use and terms incorporated therein and press the Submit button.
    12. +
    13. Click on Profile from the Home Menu.
    14. +
    15. Under Account Information choose API Access.
    16. +
    17. Choose Request API credentials.
    18. +
    19. Choose API Signature as the credential type.
    20. +
    21. Enable the checkbox and agree to the license Terms-I agree to the API Certificate Terms of Use and terms incorporated therein and press the Submit button.

    Before being able to receive IPN (Instant Payment Notification) messages (optional), you'll need to activate this service; follow these steps:

    1. Log in to your Premier or Business account.
    2. Click the Profile subtab.
    3. -
    4. Click Language Encoding and More options and select UTF-8.
    5. -
    6. Click Instant Payment Notification in the Selling Preferences column.
    7. -
    8. Click the Edit IPN Settings button to update your settings.
    9. -
    10. Select Receive IPN messages (enabled) and enter the URL of your IPN handler (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalDirect/IPNHandler).
    11. -
    12. Click Save, and you should get a message that you have successfully activated IPN.
    13. +
    14. Click Language Encoding and More options and select UTF-8.
    15. +
    16. Click Instant Payment Notification in the Selling Preferences column.
    17. +
    18. Click the Edit IPN Settings button to update your settings.
    19. +
    20. Select Receive IPN messages (enabled) and enter the URL of your IPN handler (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalDirect/IPNHandler).
    21. +
    22. Click Save, and you should get a message that you have successfully activated IPN.
    ]]> -
    -
    - - recurring payment - - - Not supported cycle period. - - - Immediately debit - - - Authorize immediately, debit later - -
    -
    +
    +
    + + recurring payment + + + Not supported cycle period. + + + Immediately debit + + + Authorize immediately, debit later + +
    +
    - - PayPal Standard - - - - - - If you're using this gateway ensure that your primary store currency is supported by Paypal.

    + + PayPal Standard + + + + + + If you're using this gateway ensure that your primary store currency is supported by Paypal.

    To use PDT (Payment Data Transfer), you must activate PDT and Auto Return in your PayPal account profile. You must also acquire a PDT identity token, which is used in all PDT communication you send to PayPal. Follow these steps to configure your account for PDT:

    1. Log in to your PayPal account.
    2. -
    3. Click the Profile subtab.
    4. -
    5. Click Website Payment Preferences in the Seller Preferences column.
    6. -
    7. Under Auto Return for Website Payments, click the On radio button.
    8. -
    9. For the Return URL, enter the URL on your site that will receive the transaction ID posted by PayPal after a customer payment (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/PDTHandler).
    10. -
    11. Under Payment Data Transfer, click the On radio button.
    12. -
    13. Click Save.
    14. -
    15. Click Website Payment Preferences in the Seller Preferences column.
    16. -
    17. Scroll down to Payment Data Transfer of the page to view your PDT identity token.
    18. +
    19. Click the Profile subtab.
    20. +
    21. Click Website Payment Preferences in the Seller Preferences column.
    22. +
    23. Under Auto Return for Website Payments, click the On radio button.
    24. +
    25. For the Return URL, enter the URL on your site that will receive the transaction ID posted by PayPal after a customer payment (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/PDTHandler).
    26. +
    27. Under Payment Data Transfer, click the On radio button.
    28. +
    29. Click Save.
    30. +
    31. Click Website Payment Preferences in the Seller Preferences column.
    32. +
    33. Scroll down to Payment Data Transfer of the page to view your PDT identity token.

    Two ways to be able to receive IPN (Instant Payment Notification) messages (optional):

      -
    1. Check Enable IPN below. It will include in the request the url of you IPN handler.
    2. +
    3. Check Enable IPN below. It will include in the request the url of you IPN handler.
    4. The second way is to configure your paypal account to activate this service; follow these steps:
      1. Log in to your Premier or Business account.
      2. -
      3. Click the Profile subtab.
      4. -
      5. Click Language Encoding and More options and select UTF-8.
      6. -
      7. Back to Profile click Instant Payment Notification in the Selling Preferences column.
      8. -
      9. Click the Edit IPN Settings button to update your settings.
      10. -
      11. Select Receive IPN messages (Enabled) and enter the URL of your IPN handler (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/IPNHandler).
      12. -
      13. Click Save, and you should get a message that you have successfully activated IPN.
      14. +
      15. Click the Profile subtab.
      16. +
      17. Click Language Encoding and More options and select UTF-8.
      18. +
      19. Back to Profile click Instant Payment Notification in the Selling Preferences column.
      20. +
      21. Click the Edit IPN Settings button to update your settings.
      22. +
      23. Select Receive IPN messages (Enabled) and enter the URL of your IPN handler (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/IPNHandler).
      24. +
      25. Click Save, and you should get a message that you have successfully activated IPN.
    ]]> -
    -
    - - After confirmation of the order, you will be redirected straight to PayPal. Please provide your data for online banking. - - - Business Email - - - Specify your PayPal business email. - - - PDT Identity Token - - - Specify PDT identity token - - - Validate order total - - - Check the box if the PDT handler should validate PayPal's order totals. - - - Only warn - - - Specifies whether to add an order note if the order total is unequal. Otherwise an error will be logged and the payment status is not updated. - - - Transmit product names and unit prices - - - Check the box if product names and unit prices should be transmitted to PayPal. - - - Enable IPN - - - Check the box if IPN (Instant Payment Notification) is enabled. - - - Please leave blank to use the default IPN handler URL. - - - IPN Handler - - - Specify IPN Handler. - - - PayPal Standard PDT. Error getting mc_gross. - - - PayPal Standard PDT. Returned order total {0} doesn't equal order total {1} (not rounded {2}). The difference is {3}. - - - PayPal Standard PDT failed. - - - Incomplete credentials for the PayPal API. Please enter the required data in the configuration area. - - - Shipping fee - - - Sales tax - - - - + + + After confirmation of the order, you will be redirected straight to PayPal. Please provide your data for online banking. + + + Business Email + + + Specify your PayPal business email. + + + PDT Identity Token + + + Specify PDT identity token + + + Validate order total + + + Check the box if the PDT handler should validate PayPal's order totals. + + + Only warn + + + Specifies whether to add an order note if the order total is unequal. Otherwise an error will be logged and the payment status is not updated. + + + Transmit product names and unit prices + + + Check the box if product names and unit prices should be transmitted to PayPal. + + + Enable IPN + + + Check the box if IPN (Instant Payment Notification) is enabled. + + + Please leave blank to use the default IPN handler URL. + + + IPN Handler + + + Specify IPN Handler. + + + PayPal Standard PDT. Error getting mc_gross. + + + PayPal Standard PDT. Returned order total {0} doesn't equal order total {1} (not rounded {2}). The difference is {3}. + + + PayPal Standard PDT failed. + + + Incomplete credentials for the PayPal API. Please enter the required data in the configuration area. + + + Shipping fee + + + Sales tax + + + + - - -
    -
    +
    +
    +
    +
    - - PayPal PLUS - - - - - - PayPal PLUS is a solution where PayPal offers PayPal, Credit Card, Direct Debit (ELV) and pay upon invoice as individual payment options on the payment selection page. + + PayPal PLUS + + + + + + PayPal PLUS is a solution where PayPal offers PayPal, Credit Card, Direct Debit (ELV) and pay upon invoice as individual payment options on the payment selection page. Pay upon invoice may not be available for all merchants. PayPal PLUS only supports the transaction type of instant settlement and is only available in Germany at the moment.

    PayPal PLUS replaces the list of payment methods in checkout by the PayPal payment wall. You can add up to 10 offline or redirection payment methods to the payment wall.

    How to set up PayPal PLUS:

      -
    1. Create a new REST API application under My Apps & Credentials. -Go to Manage your applications to get more information about PayPal applications.
    2. -
    3. Click the name of the application and enter Client-ID and Secret below on this page. Save.
    4. -
    5. Optional: Click Add next to Experience profile ID to create a new profile. Your shop data (name, logo etc.) can now be displayed to customers when paying on PayPal pages.
    6. -
    7. Optional: Click Add next to Webhook-ID to create a webhook. Through a webhook the shop receives PayPal messages und possibly updates the payment state of your orders.
    8. -

    To define images for other payment methods, you have to store files with the following name pattern in the content folder of the plugin: branding.<Plugin-Systemname|Provider-Systemname>.<jpg|jpeg|png|gif>
    For prepayment the pfad is for example \Plugins\SmartStore.OfflinePayment\Content\branding.Payments.Prepayment.jpg. Important: The shop must support SSL for this.

    ]]> -
    -
    - - Unfortunately we cannot offer you a PayPal PLUS payment method. Please try again later. - - - I'm sorry, that just didn't work. Please try again or select another payment method. - - - .]]> - - - More payment methods - - - You can set up to 5 more payment methods. These are offered along with the PayPal PLUS payment methods during the checkout process. - - - Display logo of other payment methods - - - Specifies whether to display the logo for other payment methods. SSL required. - - - Display description of other payment methods - - - Specifies whether to display the description for other payment methods. - -
    -
    +
  • Create a new REST API application under My Apps & Credentials. +Go to Manage your applications to get more information about PayPal applications.
  • +
  • Click the name of the application and enter Client-ID and Secret below on this page. Save.
  • +
  • Optional: Click Add next to Experience profile ID to create a new profile. Your shop data (name, logo etc.) can now be displayed to customers when paying on PayPal pages.
  • +
  • Optional: Click Add next to Webhook-ID to create a webhook. Through a webhook the shop receives PayPal messages und possibly updates the payment state of your orders.
  • +

    To define images for other payment methods, you have to store files with the following name pattern in the content folder of the plugin: branding.<Plugin-Systemname|Provider-Systemname>.<jpg|jpeg|png|gif>
    For prepayment the pfad is for example \Plugins\SmartStore.OfflinePayment\Content\branding.Payments.Prepayment.jpg. Important: The shop must support SSL for this.

    ]]> +
    +
    + + Unfortunately we cannot offer you a PayPal PLUS payment method. Please try again later. + + + I'm sorry, that just didn't work. Please try again or select another payment method. + + + .]]> + + + More payment methods + + + You can set up to 5 more payment methods. These are offered along with the PayPal PLUS payment methods during the checkout process. + + + Display logo of other payment methods + + + Specifies whether to display the logo for other payment methods. SSL required. + + + Display description of other payment methods + + + Specifies whether to display the description for other payment methods. + +
    +
    + + + Ratenzahlung Powered by PayPal + + + + Installment + + + + + How to set up Ratenzahlung Powered by PayPal:

    +
      +
    1. Create a new REST API application under My Apps & Credentials. +Go to Manage your applications to get more information about PayPal applications.
    2. +
    3. Click the name of the application and enter Client-ID and Secret below on this page. Save.
    4. +
    5. Optional: Click Add next to Experience profile ID to create a new profile. Your shop data (name, logo etc.) can now be displayed to customers when paying on PayPal pages.
    6. +
    7. Optional: Click Add next to Webhook-ID to create a webhook. Through a webhook the shop receives PayPal messages und possibly updates the payment state of your orders.
    8. +

    Once an order has been placed, the order total and order items may no longer be changed!

    ]]> +
    +
    + + We enable you to finance your purchase with the payment in installments powered by PayPal. In seconds, fully online, subject to credit check. + + + Financing costs + + + Total incl. financing costs + + + Information on possible rates. + + + Financing wanted? + + + Financing from {0} per month. + + + Financing from {0} in {1} monthly installments. + + + Representative example according to § 6a PAngV + + + Plan {0} + + + Net loan amount + + + Fixed nominal rate + + + Annual percentage rate + + + Total payable amount + + + {0} monthly installments in the amount of + + + last installment of + + + Interest amount + + + No financing details are available. + + + Pay conveniently and easily in monthly installments. + + + You can select your installment payment and the appropriate financing plan during the ordering process. Your application is processed online via Ratenzahlung Powered by PayPal and can be completed here in the shop in just a few steps. + + + Minimum financing amount + + + Specifies the minimum anount from which to offer a payment in installments. + + + Maximum financing amount + + + Specifies the maximum amount up to which to offer a payment in installments. + + + On product pages + + + Specifies whether and how to promote payment in installments on product pages. + + + On cart page + + + Specifies whether and how to promote payment in installments on the cart page. + + + When selecting payment methods + + + Specifies whether and how to promote when selecting payment methods. + + + Lender + + + Company name and address. + + +
    +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs index 8544216277..65d8c5b6af 100644 --- a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs +++ b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs @@ -1,19 +1,13 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using SmartStore.ComponentModel; using SmartStore.PayPal.Settings; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; -using SmartStore.Web.Framework.Validators; -using SmartStore.Core.Localization; -using System; -using FluentValidation; namespace SmartStore.PayPal.Models { - public abstract class ApiConfigurationModel : ModelBase + public abstract class ApiConfigurationModel : ModelBase { - public string[] ConfigGroups { get; set; } public string PrimaryStoreCurrencyCode { get; set; } [SmartResourceDisplayName("Plugins.Payments.PayPal.UseSandbox")] @@ -107,61 +101,5 @@ public void Copy(PayPalExpressPaymentSettings settings, bool fromSettings) settings.Signature = Signature.TrimSafe(); } } - } - - public class PayPalPlusConfigurationModel : ApiConfigurationModel - { - public PayPalPlusConfigurationModel() - { - TransactMode = TransactMode.AuthorizeAndCapture; - } - - [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.ThirdPartyPaymentMethods")] - public List ThirdPartyPaymentMethods { get; set; } - public IList AvailableThirdPartyPaymentMethods { get; set; } - - [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.DisplayPaymentMethodLogo")] - public bool DisplayPaymentMethodLogo { get; set; } - - [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.DisplayPaymentMethodDescription")] - public bool DisplayPaymentMethodDescription { get; set; } - - - public void Copy(PayPalPlusPaymentSettings settings, bool fromSettings) - { - if (fromSettings) - { - MiniMapper.Map(settings, this); - } - else - { - TransactMode = TransactMode.AuthorizeAndCapture; - - MiniMapper.Map(this, settings); - settings.ApiAccountName = ApiAccountName.TrimSafe(); - settings.ApiAccountPassword = ApiAccountPassword.TrimSafe(); - settings.ClientId = ClientId.TrimSafe(); - settings.ExperienceProfileId = ExperienceProfileId.TrimSafe(); - settings.Secret = Secret.TrimSafe(); - settings.Signature = Signature.TrimSafe(); - settings.WebhookId = WebhookId.TrimSafe(); - } - } - } - - public class PayPalPlusConfigValidator : SmartValidatorBase - { - public PayPalPlusConfigValidator(Localizer T, Func addRule) - { - if (addRule("ClientId")) - { - RuleFor(x => x.ClientId).NotEmpty(); - } - - if (addRule("Secret")) - { - RuleFor(x => x.Secret).NotEmpty(); - } - } - } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Models/Congfiguration/PayPalInstalmentsConfigModel.cs b/src/Plugins/SmartStore.PayPal/Models/Congfiguration/PayPalInstalmentsConfigModel.cs new file mode 100644 index 0000000000..aa10dac96c --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Models/Congfiguration/PayPalInstalmentsConfigModel.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.Web.Framework; + +namespace SmartStore.PayPal.Models +{ + public class PayPalInstalmentsConfigModel : ApiConfigurationModel + { + public PayPalInstalmentsConfigModel() + { + TransactMode = TransactMode.AuthorizeAndCapture; + } + + [SmartResourceDisplayName("Plugins.Payments.PayPalInstalments.FinancingMin")] + public decimal FinancingMin { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalInstalments.FinancingMax")] + public decimal FinancingMax { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalInstalments.ProductPagePromotion")] + public PayPalPromotion? ProductPagePromotion { get; set; } + public IList ProductPagePromotions { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalInstalments.CartPagePromotion")] + public PayPalPromotion? CartPagePromotion { get; set; } + public IList CartPagePromotions { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalInstalments.PaymentListPromotion")] + public PayPalPromotion? PaymentListPromotion { get; set; } + public IList PaymentListPromotions { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalInstalments.Lender")] + public string Lender { get; set; } + + //[SmartResourceDisplayName("Plugins.Payments.PayPalInstalments.Promote")] + //public bool Promote { get; set; } + + //[SmartResourceDisplayName("Plugins.Payments.PayPalInstalments.PromotionWidgetZones")] + //[UIHint("WidgetZone")] + //public string[] PromotionWidgetZones { get; set; } + + //[SmartResourceDisplayName("Plugins.Payments.PayPalInstalments.PromotionDisplayOrder")] + //public int PromotionDisplayOrder { get; set; } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Models/Congfiguration/PayPalPlusConfigurationModel.cs b/src/Plugins/SmartStore.PayPal/Models/Congfiguration/PayPalPlusConfigurationModel.cs new file mode 100644 index 0000000000..b104dfdd0a --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Models/Congfiguration/PayPalPlusConfigurationModel.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using FluentValidation; +using SmartStore.Core.Localization; +using SmartStore.PayPal.Settings; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Validators; + +namespace SmartStore.PayPal.Models +{ + public class PayPalPlusConfigurationModel : ApiConfigurationModel + { + public PayPalPlusConfigurationModel() + { + TransactMode = TransactMode.AuthorizeAndCapture; + } + + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.ThirdPartyPaymentMethods")] + public List ThirdPartyPaymentMethods { get; set; } + public IList AvailableThirdPartyPaymentMethods { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.DisplayPaymentMethodLogo")] + public bool DisplayPaymentMethodLogo { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.DisplayPaymentMethodDescription")] + public bool DisplayPaymentMethodDescription { get; set; } + } + + + public class PayPalApiConfigValidator : SmartValidatorBase + { + public PayPalApiConfigValidator(Localizer T, Func addRule) + { + if (addRule("ClientId")) + { + RuleFor(x => x.ClientId).NotEmpty(); + } + + if (addRule("Secret")) + { + RuleFor(x => x.Secret).NotEmpty(); + } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Plugin.cs b/src/Plugins/SmartStore.PayPal/Plugin.cs index 054332ca29..e34585a10b 100644 --- a/src/Plugins/SmartStore.PayPal/Plugin.cs +++ b/src/Plugins/SmartStore.PayPal/Plugin.cs @@ -1,80 +1,165 @@ using System; +using System.Collections.Generic; +using System.Web.Routing; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; +using SmartStore.PayPal.Providers; using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; -using SmartStore.Services.Configuration; -using SmartStore.Services.Localization; +using SmartStore.Services; +using SmartStore.Services.Cms; +using SmartStore.Services.Directory; +using SmartStore.Web.Models.Catalog; +using SmartStore.Web.Models.Order; +using SmartStore.Web.Models.ShoppingCart; namespace SmartStore.PayPal { - public class Plugin : BasePlugin - { - private readonly ISettingService _settingService; - private readonly ILocalizationService _localizationService; + [SystemName("Widgets.PayPal")] + [FriendlyName("PayPal")] + public class Plugin : BasePlugin, IWidget + { + private readonly ICommonServices _services; private readonly Lazy _payPalService; + private readonly Lazy _currencyService; - public Plugin( - ISettingService settingService, - ILocalizationService localizationService, - Lazy payPalService) + public Plugin( + ICommonServices services, + Lazy payPalService, + Lazy currencyService) { - _settingService = settingService; - _localizationService = localizationService; + _services = services; _payPalService = payPalService; + _currencyService = currencyService; Logger = NullLogger.Instance; } public ILogger Logger { get; set; } - public static string SystemName - { - get { return "SmartStore.PayPal"; } - } + public static string SystemName => "SmartStore.PayPal"; public override void Install() { - _settingService.SaveSetting(new PayPalExpressPaymentSettings()); - _settingService.SaveSetting(new PayPalDirectPaymentSettings()); - _settingService.SaveSetting(new PayPalStandardPaymentSettings()); - _settingService.SaveSetting(new PayPalPlusPaymentSettings()); + _services.Settings.SaveSetting(new PayPalExpressPaymentSettings()); + _services.Settings.SaveSetting(new PayPalDirectPaymentSettings()); + _services.Settings.SaveSetting(new PayPalStandardPaymentSettings()); + _services.Settings.SaveSetting(new PayPalPlusPaymentSettings()); + _services.Settings.SaveSetting(new PayPalInstalmentsSettings()); - _localizationService.ImportPluginResourcesFromXml(this.PluginDescriptor); + _services.Localization.ImportPluginResourcesFromXml(this.PluginDescriptor); base.Install(); } public override void Uninstall() { - try - { - var settings = _settingService.LoadSetting(); - if (settings.WebhookId.HasValue()) - { - var session = new PayPalSessionData(); - var result = _payPalService.Value.EnsureAccessToken(session, settings); - - if (result.Success) - result = _payPalService.Value.DeleteWebhook(settings, session); - - if (!result.Success) - Logger.Log(LogLevel.Error, null, result.ErrorMessage, null); - } - } - catch (Exception exception) - { - Logger.Log(LogLevel.Error, exception, null, null); - } - - _settingService.DeleteSetting(); - _settingService.DeleteSetting(); - _settingService.DeleteSetting(); - _settingService.DeleteSetting(); - - _localizationService.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); + DeleteWebhook(_services.Settings.LoadSetting(), PayPalPlusProvider.SystemName); + DeleteWebhook(_services.Settings.LoadSetting(), PayPalInstalmentsProvider.SystemName); + + _services.Settings.DeleteSetting(); + _services.Settings.DeleteSetting(); + _services.Settings.DeleteSetting(); + _services.Settings.DeleteSetting(); + _services.Settings.DeleteSetting(); + + _services.Localization.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); base.Uninstall(); } + + public IList GetWidgetZones() + { + return new List + { + "productdetails_add_info", + "order_summary_totals_after", + "orderdetails_page_aftertotal", + "invoice_aftertotal" + }; + } + + public void GetDisplayWidgetRoute(string widgetZone, object model, int storeId, out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = controllerName = null; + routeValues = new RouteValueDictionary + { + { "Namespaces", "SmartStore.PayPal.Controllers" }, + { "area", SystemName } + }; + + if (widgetZone == "productdetails_add_info") + { + var viewModel = model as ProductDetailsModel; + if (viewModel != null) + { + var price = viewModel.ProductPrice.PriceWithDiscountValue > decimal.Zero + ? viewModel.ProductPrice.PriceWithDiscountValue + : viewModel.ProductPrice.PriceValue; + + if (price > decimal.Zero) + { + actionName = "Promotion"; + controllerName = "PayPalInstalments"; + + // Convert price because it is in working currency. + price = _currencyService.Value.ConvertToPrimaryStoreCurrency(price, _services.WorkContext.WorkingCurrency); + + routeValues.Add("origin", "productpage"); + routeValues.Add("amount", price); + } + } + } + else if (widgetZone == "order_summary_totals_after") + { + var viewModel = model as ShoppingCartModel; + if (viewModel != null && viewModel.IsEditable) + { + actionName = "Promotion"; + controllerName = "PayPalInstalments"; + + routeValues.Add("origin", "cart"); + routeValues.Add("amount", decimal.Zero); + } + } + else if (widgetZone == "orderdetails_page_aftertotal" || widgetZone == "invoice_aftertotal") + { + var viewModel = model as OrderDetailsModel; + if (viewModel != null) + { + actionName = "OrderDetails"; + controllerName = "PayPalInstalments"; + + routeValues.Add("orderId", viewModel.Id); + routeValues.Add("print", widgetZone.IsCaseInsensitiveEqual("invoice_aftertotal")); + } + } + } + + private void DeleteWebhook(PayPalApiSettingsBase settings, string providerSystemName) + { + try + { + if (settings?.WebhookId.HasValue() ?? false) + { + var session = new PayPalSessionData { ProviderSystemName = providerSystemName }; + var result = _payPalService.Value.EnsureAccessToken(session, settings); + + if (result.Success) + { + result = _payPalService.Value.DeleteWebhook(settings, session); + } + + if (!result.Success) + { + Logger.Log(LogLevel.Error, null, result.ErrorMessage, null); + } + } + } + catch (Exception ex) + { + Logger.Log(LogLevel.Error, ex, null, null); + } + } } } diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalInstalmentsProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalInstalmentsProvider.cs new file mode 100644 index 0000000000..bf4d1e2da6 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalInstalmentsProvider.cs @@ -0,0 +1,57 @@ +using System; +using Newtonsoft.Json; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Controllers; +using SmartStore.PayPal.Settings; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal.Providers +{ + [DisplayOrder(1)] + [SystemName("Payments.PayPalInstalments")] + [FriendlyName("Ratenzahlung Powered by PayPal")] + [DependentWidgets("Widgets.PayPal")] + public class PayPalInstalmentsProvider : PayPalRestApiProviderBase + { + public PayPalInstalmentsProvider() + : base(SystemName) + { + } + + public static string SystemName => "Payments.PayPalInstalments"; + + public override PaymentMethodType PaymentMethodType => PaymentMethodType.StandardAndRedirection; + + public override Type GetControllerType() + { + return typeof(PayPalInstalmentsController); + } + + public override void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) + { + var order = postProcessPaymentRequest.Order; + var session = HttpContext.GetPayPalState(SystemName); + + var orderAttribute = new PayPalInstalmentsOrderAttribute + { + FinancingCosts = session.FinancingCosts, + TotalInclFinancingCosts = session.TotalInclFinancingCosts + }; + + GenericAttributeService.SaveAttribute(order, PayPalInstalmentsOrderAttribute.Key, JsonConvert.SerializeObject(orderAttribute), order.StoreId); + + base.PostProcessPayment(postProcessPaymentRequest); + } + } + + + [Serializable] + public class PayPalInstalmentsOrderAttribute + { + [JsonIgnore] + public static string Key => string.Concat(PayPalInstalmentsProvider.SystemName, ".OrderAttribute"); + + public decimal FinancingCosts { get; set; } + public decimal TotalInclFinancingCosts { get; set; } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs index 7b83e6d349..64a4db72a4 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs @@ -43,11 +43,6 @@ protected string GetControllerName() return GetControllerType().Name.EmptyNull().Replace("Controller", ""); } - public static string CheckoutCompletedKey - { - get { return "PayPalCheckoutCompleted"; } - } - public override bool SupportCapture { get { return true; } @@ -90,7 +85,7 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces NewPaymentStatus = PaymentStatus.Pending }; - HttpContext.Session.SafeRemove(CheckoutCompletedKey); + HttpContext.Session.SafeRemove("PayPalCheckoutCompleted"); var storeId = processPaymentRequest.StoreId; var customer = Services.WorkContext.CurrentCustomer; @@ -102,7 +97,7 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces session.SessionExpired = true; result.AddError(T("Plugins.SmartStore.PayPal.SessionExpired")); - // Redirect to payment wall and create new payment (we need the payment id). + // Redirect to payment page and create new payment (we need the payment id). var urlHelper = new UrlHelper(HttpContext.Request.RequestContext); HttpContext.Response.Redirect(urlHelper.Action("PaymentMethod", "Checkout", new { area = "" })); @@ -194,14 +189,14 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces public override void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) { - var storeId = postProcessPaymentRequest.Order.StoreId; - var customer = Services.WorkContext.CurrentCustomer; - var session = HttpContext.GetPayPalState(_providerSystemName, customer, storeId, GenericAttributeService); + var order = postProcessPaymentRequest.Order; + var customer = order.Customer ?? Services.WorkContext.CurrentCustomer; + var session = HttpContext.GetPayPalState(_providerSystemName, customer, order.StoreId, GenericAttributeService); var instruction = PayPalService.CreatePaymentInstruction(session.PaymentInstruction); if (instruction.HasValue()) { - HttpContext.Session[CheckoutCompletedKey] = instruction; + HttpContext.Session["PayPalCheckoutCompleted"] = instruction; OrderService.AddOrderNote(postProcessPaymentRequest.Order, instruction, true); } @@ -215,19 +210,23 @@ public override CapturePaymentResult Capture(CapturePaymentRequest capturePaymen }; var settings = Services.Settings.LoadSetting(capturePaymentRequest.Order.StoreId); - var session = new PayPalSessionData(); + var session = new PayPalSessionData { ProviderSystemName = _providerSystemName }; - var apiResult = PayPalService.EnsureAccessToken(session, settings); + var apiResult = PayPalService.EnsureAccessToken(session, settings); if (apiResult.Success) { apiResult = PayPalService.Capture(settings, session, capturePaymentRequest); - if (apiResult.Success) - result.NewPaymentStatus = PaymentStatus.Paid; + if (apiResult.Success) + { + result.NewPaymentStatus = PaymentStatus.Paid; + } } - if (!apiResult.Success) - result.Errors.Add(apiResult.ErrorMessage); + if (!apiResult.Success) + { + result.Errors.Add(apiResult.ErrorMessage); + } return result; } @@ -240,23 +239,24 @@ public override RefundPaymentResult Refund(RefundPaymentRequest refundPaymentReq }; var settings = Services.Settings.LoadSetting(refundPaymentRequest.Order.StoreId); - var session = new PayPalSessionData(); + var session = new PayPalSessionData { ProviderSystemName = _providerSystemName }; - var apiResult = PayPalService.EnsureAccessToken(session, settings); + var apiResult = PayPalService.EnsureAccessToken(session, settings); if (apiResult.Success) { apiResult = PayPalService.Refund(settings, session, refundPaymentRequest); if (apiResult.Success) { - if (refundPaymentRequest.IsPartialRefund) - result.NewPaymentStatus = PaymentStatus.PartiallyRefunded; - else - result.NewPaymentStatus = PaymentStatus.Refunded; + result.NewPaymentStatus = refundPaymentRequest.IsPartialRefund + ? PaymentStatus.PartiallyRefunded + : PaymentStatus.Refunded; } } - if (!apiResult.Success) - result.Errors.Add(apiResult.ErrorMessage); + if (!apiResult.Success) + { + result.Errors.Add(apiResult.ErrorMessage); + } return result; } @@ -269,9 +269,9 @@ public override VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) }; var settings = Services.Settings.LoadSetting(voidPaymentRequest.Order.StoreId); - var session = new PayPalSessionData(); + var session = new PayPalSessionData { ProviderSystemName = _providerSystemName }; - var apiResult = PayPalService.EnsureAccessToken(session, settings); + var apiResult = PayPalService.EnsureAccessToken(session, settings); if (apiResult.Success) { apiResult = PayPalService.Void(settings, session, voidPaymentRequest); @@ -281,8 +281,10 @@ public override VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) } } - if (!apiResult.Success) - result.Errors.Add(apiResult.ErrorMessage); + if (!apiResult.Success) + { + result.Errors.Add(apiResult.ErrorMessage); + } return result; } @@ -291,14 +293,14 @@ public override void GetConfigurationRoute(out string actionName, out string con { actionName = "Configure"; controllerName = GetControllerName(); - routeValues = new RouteValueDictionary { { "area", "SmartStore.PayPal" } }; + routeValues = new RouteValueDictionary { { "area", Plugin.SystemName } }; } public override void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) { actionName = "PaymentInfo"; controllerName = GetControllerName(); - routeValues = new RouteValueDictionary { { "area", "SmartStore.PayPal" } }; + routeValues = new RouteValueDictionary { { "area", Plugin.SystemName } }; } } } diff --git a/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs b/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs index c4a911fa4f..e30b5d554c 100644 --- a/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs +++ b/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs @@ -9,7 +9,7 @@ namespace SmartStore.PayPal.Services { - public interface IPayPalService + public interface IPayPalService { void AddOrderNote(PayPalSettingsBase settings, Order order, string anyString, bool isIpn = false); @@ -19,25 +19,33 @@ public interface IPayPalService PaymentStatus GetPaymentStatus(string state, string reasonCode, PaymentStatus defaultStatus); - PayPalResponse CallApi(string method, string path, string accessToken, PayPalApiSettingsBase settings, string data); + PayPalResponse CallApi( + string method, + string path, + PayPalApiSettingsBase settings, + PayPalSessionData session, + string data); PayPalResponse EnsureAccessToken(PayPalSessionData session, PayPalApiSettingsBase settings); PayPalResponse GetPayment(PayPalApiSettingsBase settings, PayPalSessionData session); - PayPalResponse CreatePayment( - PayPalApiSettingsBase settings, - PayPalSessionData session, - List cart, - string providerSystemName, - string returnUrl, - string cancelUrl); + Dictionary CreatePaymentData( + PayPalApiSettingsBase settings, + PayPalSessionData session, + List cart, + string returnUrl, + string cancelUrl); + + PayPalResponse CreatePayment( + PayPalApiSettingsBase settings, + PayPalSessionData session, + Dictionary data); PayPalResponse PatchShipping( PayPalApiSettingsBase settings, PayPalSessionData session, - List cart, - string providerSystemName); + List cart); PayPalResponse ExecutePayment(PayPalApiSettingsBase settings, PayPalSessionData session); @@ -60,5 +68,16 @@ HttpStatusCode ProcessWebhook( NameValueCollection headers, string rawJson, string providerSystemName); - } + + #region Credit + + FinancingOptions GetFinancingOptions( + PayPalInstalmentsSettings settings, + PayPalSessionData session, + string origin, + decimal amount, + PayPalPromotion? promotion = null); + + #endregion + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs index d5bba1b72c..5211bf9cc0 100644 --- a/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs +++ b/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs @@ -1,5 +1,4 @@ - -namespace SmartStore.PayPal.Services +namespace SmartStore.PayPal.Services { public enum PayPalPaymentInstructionItem { @@ -24,4 +23,10 @@ public enum PayPalMessage Amount, PaymentId } + + public enum PayPalPromotion + { + FinancingExample = 0, + TextOnly + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalPaymentFilter.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalPaymentFilter.cs new file mode 100644 index 0000000000..73d8ff91de --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Services/PayPalPaymentFilter.cs @@ -0,0 +1,83 @@ +using System; +using SmartStore.PayPal.Providers; +using SmartStore.PayPal.Settings; +using SmartStore.Services; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal.Services +{ + public partial class PayPalPaymentFilter : IPaymentMethodFilter + { + protected readonly ICommonServices _services; + protected readonly Lazy _orderTotalCalculationService; + + public PayPalPaymentFilter( + ICommonServices services, + Lazy orderTotalCalculationService) + { + _services = services; + _orderTotalCalculationService = orderTotalCalculationService; + } + + public string GetConfigurationUrl(string systemName) => null; + + public bool IsExcluded(PaymentFilterRequest request) + { + if (request.Customer == null) + { + return false; + } + + var method = request.PaymentMethod?.Metadata?.SystemName; + + if (method.IsCaseInsensitiveEqual(PayPalInstalmentsProvider.SystemName)) + { + return IsPaymentExcluded(request); + } + + return false; + } + + protected bool CanPayOrderAmount(PayPalInstalmentsSettings settings, PaymentFilterRequest request) + { + var cartTotal = ((decimal?)_orderTotalCalculationService.Value.GetShoppingCartTotal(request.Cart)) ?? decimal.Zero; + + if (cartTotal == decimal.Zero) + { + return false; + } + + return cartTotal >= settings.FinancingMin && cartTotal <= settings.FinancingMax; + } + + protected bool IsPaymentExcluded(PaymentFilterRequest request) + { + var ba = request.Customer.BillingAddress; + var sa = request.Customer.ShippingAddress; + + if (ba?.Country != null && !ba.Country.TwoLetterIsoCode.IsCaseInsensitiveEqual("DE")) + { + return true; + } + if (sa != null && sa.Country != null && !sa.Country.TwoLetterIsoCode.IsCaseInsensitiveEqual("DE")) + { + return true; + } + + var settings = _services.Settings.LoadSetting(request.StoreId); + + if (settings.ClientId.IsEmpty() || settings.Secret.IsEmpty()) + { + return true; + } + + if (!CanPayOrderAmount(settings, request)) + { + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalService.Credit.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalService.Credit.cs new file mode 100644 index 0000000000..e9a2e1d54a --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Services/PayPalService.Credit.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SmartStore.Core.Domain.Orders; +using SmartStore.PayPal.Settings; +using SmartStore.Services.Customers; + +namespace SmartStore.PayPal.Services +{ + public partial class PayPalService + { + public FinancingOptions GetFinancingOptions( + PayPalInstalmentsSettings settings, + PayPalSessionData session, + string origin, + decimal amount, + PayPalPromotion? promotion = null) + { + Guard.NotNull(settings, nameof(settings)); + Guard.NotNull(session, nameof(session)); + Guard.NotEmpty(origin, nameof(origin)); + + var store = _services.StoreContext.CurrentStore; + + // Get promotion type. + if (!promotion.HasValue) + { + switch (origin) + { + case "productpage": + promotion = settings.ProductPagePromotion; + break; + case "cart": + promotion = settings.CartPagePromotion; + break; + case "paymentinfo": + promotion = settings.PaymentListPromotion; + break; + } + } + + if (!promotion.HasValue || settings.ClientId.IsEmpty() || settings.Secret.IsEmpty()) + { + return null; + } + + // Get financing amount. + if (origin == "cart" || origin == "paymentinfo") + { + var cart = _services.WorkContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + decimal? cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart, usePaymentMethodAdditionalFee: false); + if (!cartTotal.HasValue) + { + _orderTotalCalculationService.GetShoppingCartSubTotal(cart, out _, out _, out _, out amount); + } + else + { + amount = cartTotal.Value; + } + } + + if (!settings.IsAmountFinanceable(amount)) + { + return null; + } + + // Get financing options from API. + var result = new FinancingOptions(origin) + { + Promotion = promotion, + Lender = settings.Lender + }; + + var sourceCurrency = store.PrimaryStoreCurrency; + var targetCurrency = _services.WorkContext.WorkingCurrency; + + result.NetLoanAmount = new Money(_currencyService.ConvertCurrency(amount, sourceCurrency, targetCurrency, store), targetCurrency); + + if (promotion == PayPalPromotion.FinancingExample) + { + var response = EnsureAccessToken(session, settings); + if (response.Success) + { + var index = 0; + var dc = decimal.Zero; + var data = new Dictionary(); + var transactionAmount = new Dictionary(); + transactionAmount.Add("value", amount.FormatInvariant()); + transactionAmount.Add("currency_code", store.PrimaryStoreCurrency.CurrencyCode); + + var merchantCountry = _countryService.Value.GetCountryById(_companyInfoSettings.Value.CountryId) ?? _countryService.Value.GetAllCountries().FirstOrDefault(); + data.Add("financing_country_code", merchantCountry.TwoLetterIsoCode); + data.Add("transaction_amount", transactionAmount); + + response = CallApi("POST", "/v1/credit/calculated-financing-options", settings, session, JsonConvert.SerializeObject(data)); + + if (response.Success && response.Json.financing_options != null) + { + foreach (var fo in response.Json.financing_options[0].qualifying_financing_options) + { + var option = new FinancingOptions.Option(); + + option.MonthlyPayment = Parse((string)fo.monthly_payment.value, sourceCurrency, targetCurrency, store); + + if (option.MonthlyPayment.Amount > decimal.Zero) + { + if (decimal.TryParse(((string)fo.credit_financing.apr).EmptyNull(), NumberStyles.Number, CultureInfo.InvariantCulture, out dc)) + { + option.AnnualPercentageRate = dc; + } + if (decimal.TryParse(((string)fo.credit_financing.nominal_rate).EmptyNull(), NumberStyles.Number, CultureInfo.InvariantCulture, out dc)) + { + option.NominalRate = dc; + } + + option.Term = ((string)fo.credit_financing.term).ToInt(); + option.MinAmount = Parse((string)fo.min_amount.value, sourceCurrency, targetCurrency, store); + option.TotalInterest = Parse((string)fo.total_interest.value, sourceCurrency, targetCurrency, store); + option.TotalCost = Parse((string)fo.total_cost.value, sourceCurrency, targetCurrency, store); + + // PayPal review: do not display last instalment. + //var instalments = fo.estimated_installments as JArray; + //var lastInstalment = instalments?.LastOrDefault()?.SelectToken("total_payment.value")?.ToString(); + //option.LastInstalment = Parse(lastInstalment, sourceCurrency, targetCurrency, store); + + //if (option.LastInstalment.Amount == decimal.Zero) + //{ + // option.LastInstalment = new Money(option.MonthlyPayment.Amount, targetCurrency); + //} + + result.Qualified.Add(option); + } + } + + result.Qualified = result.Qualified + .OrderBy(x => x.Term) + .ThenBy(x => x.MonthlyPayment.Amount) + .ToList(); + + result.Qualified.Each(x => x.Index = ++index); + } + } + } + + return result; + } + } + + + public class FinancingOptions + { + public FinancingOptions(string origin) + { + Origin = origin; + Qualified = new List