From 953ecda97ada055b4f57d072acdfba92455452e8 Mon Sep 17 00:00:00 2001 From: Carbon Build Service Account Date: Thu, 8 Dec 2016 23:58:30 +0000 Subject: [PATCH] PayPal express checkout support with PayPal accounts, PCI review & compliance. --- .gitattributes | 63 ++ CustomerPortal.sln | 14 +- .../CustomerPortal.Deployment.deployproj | 1 + .../Templates/WebSite.json | 248 ++--- .../Templates/WebSite.parameters.json | 70 +- Source/CustomerPortal.Deployment/app.config | 10 +- .../App_Start/Startup.Auth.cs | 2 +- .../BusinessLogic/ApplicationDomain.cs | 6 +- .../BusinessLogic/Branding/PortalBranding.cs | 10 +- .../Commerce/CommerceOperations.cs | 179 ++-- .../Commerce/ICommerceOperations.cs | 13 +- .../BusinessLogic/Commerce/IPaymentGateway.cs | 7 +- .../BusinessLogic/Commerce/OrderNormalizer.cs | 255 +++++ .../PaymentConfigurationRepository.cs | 3 +- .../Commerce/PaymentGateways/PayPalGateway.cs | 347 +++++-- .../Commerce/Transactions/AuthorizePayment.cs | 20 +- .../Transactions/RenewSubscription.cs | 4 +- .../BusinessLogic/CustomerPortalPrincipal.cs | 2 +- .../PartnerCenterCustomersRepository.cs | 4 +- .../BusinessLogic/Exceptions/ErrorCode.cs | 15 - .../Offers/MicrosoftOfferLogoIndexer.cs | 4 +- .../Offers/PartnerOfferNormalizer.cs | 6 +- .../Offers/PartnerOffersRepository.cs | 8 +- .../Configuration/ApplicationConfiguration.cs | 4 +- .../Configuration/WebPortalConfiguration.json | 127 ++- .../Styles/Plugins/CreditCardInput.css | 15 - .../Content/Styles/Plugins/OfferTile.css | 2 +- .../Content/Styles/Plugins/ProcessOrder.css | 17 + .../Controllers/AdminConsoleController.cs | 19 +- .../Controllers/CustomerAccountController.cs | 4 +- ...iptionController.cs => OrderController.cs} | 285 +++--- .../Controllers/PartnerOfferController.cs | 16 +- .../Controllers/TemplateController.cs | 11 + .../Extensions.cs | 8 +- .../Filters/WebApi/ErrorHandler.cs | 3 - .../Models/OrderSubscriptionItemViewModel.cs | 23 +- .../Models/OrderViewModel.cs | 12 +- .../Models/PaymentCard.cs | 63 -- .../Models/PaymentConfiguration.cs | 5 + .../Models/TransactionResult.cs | 13 +- .../PartnerCenter.CustomerPortal.csproj | 62 +- .../Resources.Designer.cs | 874 +++++++----------- .../Resources.resx | 344 +++---- .../Plugins/AddSubscriptionsPresenter.js | 59 +- .../Scripts/Plugins/CommerceOperationType.js | 21 + .../Plugins/CustomerRegistrationPresenter.js | 153 +-- .../Scripts/Plugins/ErrorCode.js | 17 +- .../Scripts/Plugins/OfferListPresenter.js | 76 +- .../Scripts/Plugins/ProcessOrderPresenter.js | 147 +++ .../RegistrationConfirmationPresenter.js | 74 +- .../Scripts/Plugins/SubscriptionsPresenter.js | 2 +- .../Plugins/UpdateSubscriptionsPresenter.js | 110 +-- .../Plugins/Views/CreditCardInputView.js | 112 --- .../Scripts/WebPortal/Core/SessionManager.js | 1 + .../Scripts/WebPortal/Core/UrlManager.js | 19 +- .../Scripts/_references.js | Bin 9154 -> 9264 bytes .../Views/Controls/CreditCardInput.cshtml | 45 - .../Views/Controls/NewCustomerProfile.cshtml | 2 +- .../Views/Framework/Resources.cshtml | 36 +- .../Views/Shared/AddOrUpdateOffer.cshtml | 4 +- .../Views/Shared/AddSubscriptions.cshtml | 6 +- .../Views/Shared/BrandingSetup.cshtml | 8 +- .../Views/Shared/CustomerRegistration.cshtml | 6 +- .../Views/Shared/Error.cshtml | 6 +- .../Views/Shared/OfferList.cshtml | 2 +- .../Views/Shared/PaymentSetup.cshtml | 5 +- .../Views/Shared/ProcessOrder.cshtml | 71 ++ .../Shared/RegistrationConfirmation.cshtml | 61 +- .../Views/Shared/StandardSplashScreen.cshtml | 2 +- .../Views/Shared/Subscriptions.cshtml | 2 +- .../Views/Shared/UpdateSubscriptions.cshtml | 10 +- .../PartnerCenter.CustomerPortal/Web.config | 30 +- .../packages.config | 14 +- azuredeploy.json | 406 ++++---- azuredeploy.param.json | 102 +- 75 files changed, 2417 insertions(+), 2390 deletions(-) create mode 100644 .gitattributes create mode 100644 Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/OrderNormalizer.cs delete mode 100644 Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/CreditCardInput.css create mode 100644 Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/ProcessOrder.css rename Source/PartnerCenter.CustomerPortal/Controllers/{SubscriptionController.cs => OrderController.cs} (56%) delete mode 100644 Source/PartnerCenter.CustomerPortal/Models/PaymentCard.cs create mode 100644 Source/PartnerCenter.CustomerPortal/Scripts/Plugins/CommerceOperationType.js create mode 100644 Source/PartnerCenter.CustomerPortal/Scripts/Plugins/ProcessOrderPresenter.js delete mode 100644 Source/PartnerCenter.CustomerPortal/Scripts/Plugins/Views/CreditCardInputView.js delete mode 100644 Source/PartnerCenter.CustomerPortal/Views/Controls/CreditCardInput.cshtml create mode 100644 Source/PartnerCenter.CustomerPortal/Views/Shared/ProcessOrder.cshtml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/CustomerPortal.sln b/CustomerPortal.sln index e9fc474..6d7ff57 100644 --- a/CustomerPortal.sln +++ b/CustomerPortal.sln @@ -1,12 +1,24 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PartnerCenter.CustomerPortal", "Source\PartnerCenter.CustomerPortal\PartnerCenter.CustomerPortal.csproj", "{07C22DE5-B22A-474D-B593-9F5DA84C6DD8}" EndProject Project("{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}") = "CustomerPortal.Deployment", "Source\CustomerPortal.Deployment\CustomerPortal.Deployment.deployproj", "{D0359F86-1AA6-4646-87A0-4BA66A5D35FD}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Azure Click To Deploy", "Azure Click To Deploy", "{EA762CF6-7773-4566-9B93-6DF622B028B8}" + ProjectSection(SolutionItems) = preProject + azuredeploy.json = azuredeploy.json + azuredeploy.param.json = azuredeploy.param.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Docs", "GitHub Docs", "{967550ED-94D1-40B1-8CA5-831075025128}" + ProjectSection(SolutionItems) = preProject + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Source/CustomerPortal.Deployment/CustomerPortal.Deployment.deployproj b/Source/CustomerPortal.Deployment/CustomerPortal.Deployment.deployproj index d6f23d8..5f08d3d 100644 --- a/Source/CustomerPortal.Deployment/CustomerPortal.Deployment.deployproj +++ b/Source/CustomerPortal.Deployment/CustomerPortal.Deployment.deployproj @@ -14,6 +14,7 @@ d0359f86-1aa6-4646-87a0-4ba66a5d35fd + true Deployment 1.0 diff --git a/Source/CustomerPortal.Deployment/Templates/WebSite.json b/Source/CustomerPortal.Deployment/Templates/WebSite.json index f8e379d..f71b9c3 100644 --- a/Source/CustomerPortal.Deployment/Templates/WebSite.json +++ b/Source/CustomerPortal.Deployment/Templates/WebSite.json @@ -1,116 +1,116 @@ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", - "parameters": { - "webSiteName": { - "type": "string", - "minLength": 1 - }, - "webPortalClientId": { - "type": "string", - "minLength": 1 - }, - "webPortalClientSecret": { - "type": "string", - "minLength": 1 - }, - "webPortalAadTenantId": { - "type": "string", - "minLength": 1 - }, - "partnerCenterApplicationId": { - "type": "string", - "minLength": 1 - }, - "partnerCenterApplicationSecret": { - "type": "string", - "minLength": 1 - }, - "partnerCenterAadTenantId": { - "type": "string", - "minLength": 1 - }, - "aadAuthorityEndpoint": { - "type": "string", - "defaultValue": "https://login.microsoftonline.com/" - }, - "graphEndpoint": { - "type": "string", - "defaultValue": "https://graph.windows.net" - }, - "azureStorageConnectionEndpointSuffix": { - "type": "string", - "defaultValue": "core.windows.net" - }, - "partnercenterApiEndpoint": { - "type": "string", - "defaultValue": "https://api.partnercenter.microsoft.com" - }, - "hostingPlanName": { - "type": "string", - "minLength": 1 - }, - "skuName": { - "type": "string", - "defaultValue": "F1", - "allowedValues": [ - "F1", - "D1", - "B1", - "B2", - "B3", - "S1", - "S2", - "S3", - "P1", - "P2", - "P3", - "P4" - ], - "metadata": { - "description": "Describes plan's pricing tier and instance size. Check details at https://azure.microsoft.com/en-us/pricing/details/app-service/" - } - }, - "skuCapacity": { - "type": "int", - "defaultValue": 1, - "minValue": 1, - "metadata": { - "description": "Describes plan's instance count" - } - }, - "_artifactsLocation": { - "type": "string" - }, - "_artifactsLocationSasToken": { - "type": "securestring" - }, - "CustomerPortalPackageFolder": { - "type": "string", - "minLength": 1, - "metadata": { - "description": "WebDeploy package location. This path is relative to the _artifactsLocation parameter" - } - }, - "CustomerPortalPackageFileName": { - "type": "string", - "minLength": 1, - "metadata": { - "description": "Name of the webdeploy package" - } - }, - "CustomerPortalStorageAccountType": { - "type": "string", - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "Standard_ZRS", - "Standard_GRS", - "Standard_RAGRS", - "Premium_LRS" - ] - } + "parameters": { + "webSiteName": { + "type": "string", + "minLength": 1 + }, + "webPortalClientId": { + "type": "string", + "minLength": 1 + }, + "webPortalClientSecret": { + "type": "string", + "minLength": 1 + }, + "webPortalAadTenantId": { + "type": "string", + "minLength": 1 + }, + "partnerCenterApplicationId": { + "type": "string", + "minLength": 1 + }, + "partnerCenterApplicationSecret": { + "type": "string", + "minLength": 1 }, + "partnerCenterAadTenantId": { + "type": "string", + "minLength": 1 + }, + "aadAuthorityEndpoint": { + "type": "string", + "defaultValue": "https://login.microsoftonline.com/" + }, + "graphEndpoint": { + "type": "string", + "defaultValue": "https://graph.windows.net" + }, + "azureStorageConnectionEndpointSuffix": { + "type": "string", + "defaultValue": "core.windows.net" + }, + "partnercenterApiEndpoint": { + "type": "string", + "defaultValue": "https://api.partnercenter.microsoft.com" + }, + "hostingPlanName": { + "type": "string", + "minLength": 1 + }, + "skuName": { + "type": "string", + "defaultValue": "F1", + "allowedValues": [ + "F1", + "D1", + "B1", + "B2", + "B3", + "S1", + "S2", + "S3", + "P1", + "P2", + "P3", + "P4" + ], + "metadata": { + "description": "Describes plan's pricing tier and instance size. Check details at https://azure.microsoft.com/en-us/pricing/details/app-service/" + } + }, + "skuCapacity": { + "type": "int", + "defaultValue": 1, + "minValue": 1, + "metadata": { + "description": "Describes plan's instance count" + } + }, + "_artifactsLocation": { + "type": "string" + }, + "_artifactsLocationSasToken": { + "type": "securestring" + }, + "CustomerPortalPackageFolder": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "WebDeploy package location. This path is relative to the _artifactsLocation parameter" + } + }, + "CustomerPortalPackageFileName": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "Name of the webdeploy package" + } + }, + "CustomerPortalStorageAccountType": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_ZRS", + "Standard_GRS", + "Standard_RAGRS", + "Premium_LRS" + ] + } + }, "variables": { "CustomerPortalStorageAccountName": "[concat('storage', uniqueString(resourceGroup().id))]" }, @@ -178,20 +178,20 @@ "tags": { "displayName": "ApplicationSettings" }, - "properties": { - "webPortal.clientId": "[parameters('webPortalClientId')]", - "webPortal.clientSecret": "[parameters('webPortalClientSecret')]", - "webPortal.AadTenantId": "[parameters('webPortalAadTenantId')]", - "aadEndpoint": "[parameters('aadAuthorityEndpoint')]", - "aadGraphEndpoint": "[parameters('graphEndpoint')]", - "partnerCenter.apiEndPoint": "[parameters('partnercenterApiEndpoint')]", - "partnerCenter.applicationId": "[parameters('partnerCenterApplicationId')]", - "partnerCenter.applicationSecret": "[parameters('partnerCenterApplicationSecret')]", - "partnerCenter.AadTenantId": "[parameters('partnerCenterAadTenantId')]", - "webPortal.azureStorageConnectionString": "[Concat('DefaultEndpointsProtocol=https;AccountName=',variables('CustomerPortalStorageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('CustomerPortalStorageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value)]", - "webPortal.azureStorageConnectionEndpointSuffix": "[parameters('azureStorageConnectionEndpointSuffix')]", - "webPortal.cacheConnectionString": "" - } + "properties": { + "webPortal.clientId": "[parameters('webPortalClientId')]", + "webPortal.clientSecret": "[parameters('webPortalClientSecret')]", + "webPortal.AadTenantId": "[parameters('webPortalAadTenantId')]", + "aadEndpoint": "[parameters('aadAuthorityEndpoint')]", + "aadGraphEndpoint": "[parameters('graphEndpoint')]", + "partnerCenter.apiEndPoint": "[parameters('partnercenterApiEndpoint')]", + "partnerCenter.applicationId": "[parameters('partnerCenterApplicationId')]", + "partnerCenter.applicationSecret": "[parameters('partnerCenterApplicationSecret')]", + "partnerCenter.AadTenantId": "[parameters('partnerCenterAadTenantId')]", + "webPortal.azureStorageConnectionString": "[Concat('DefaultEndpointsProtocol=https;AccountName=',variables('CustomerPortalStorageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('CustomerPortalStorageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value)]", + "webPortal.azureStorageConnectionEndpointSuffix": "[parameters('azureStorageConnectionEndpointSuffix')]", + "webPortal.cacheConnectionString": "" + } } ] }, @@ -215,4 +215,4 @@ "value": "[concat('https://',reference(resourceId('Microsoft.Web/sites', parameters('webSiteName'))).hostNames[0])]" } } -} +} \ No newline at end of file diff --git a/Source/CustomerPortal.Deployment/Templates/WebSite.parameters.json b/Source/CustomerPortal.Deployment/Templates/WebSite.parameters.json index da0230e..276747b 100644 --- a/Source/CustomerPortal.Deployment/Templates/WebSite.parameters.json +++ b/Source/CustomerPortal.Deployment/Templates/WebSite.parameters.json @@ -1,39 +1,39 @@ { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", - "parameters": { - "azureStorageConnectionEndpointSuffix": { - "value": "core.windows.net" - }, - "hostingPlanName": { - "value": "Free" - }, - "CustomerPortalPackageFolder": { - "value": "PartnerCenter.CustomerPortal" - }, - "CustomerPortalPackageFileName": { - "value": "package.zip" - }, - "partnerCenterApplicationId": { - "value": "" - }, - "partnerCenterApplicationSecret": { - "value": "" - }, - "partnerCenterAadTenantId": { - "value": "" - }, - "webPortalClientId": { - "value": "" - }, - "webPortalClientSecret": { - "value": "" - }, - "webPortalAadTenantId": { - "value": "" - }, - "webSiteName": { - "value": "" - } + "parameters": { + "azureStorageConnectionEndpointSuffix": { + "value": "core.windows.net" + }, + "hostingPlanName": { + "value": "Free" + }, + "CustomerPortalPackageFolder": { + "value": "PartnerCenter.CustomerPortal" + }, + "CustomerPortalPackageFileName": { + "value": "package.zip" + }, + "partnerCenterApplicationId": { + "value": "" + }, + "partnerCenterApplicationSecret": { + "value": "" + }, + "partnerCenterAadTenantId": { + "value": "" + }, + "webPortalClientId": { + "value": "" + }, + "webPortalClientSecret": { + "value": "" + }, + "webPortalAadTenantId": { + "value": "" + }, + "webSiteName": { + "value": "" } -} + } +} \ No newline at end of file diff --git a/Source/CustomerPortal.Deployment/app.config b/Source/CustomerPortal.Deployment/app.config index d873122..8519362 100644 --- a/Source/CustomerPortal.Deployment/app.config +++ b/Source/CustomerPortal.Deployment/app.config @@ -4,7 +4,7 @@ - + @@ -34,6 +34,14 @@ + + + + + + + + \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/App_Start/Startup.Auth.cs b/Source/PartnerCenter.CustomerPortal/App_Start/Startup.Auth.cs index a7fc613..3f1ce2e 100644 --- a/Source/PartnerCenter.CustomerPortal/App_Start/Startup.Auth.cs +++ b/Source/PartnerCenter.CustomerPortal/App_Start/Startup.Auth.cs @@ -139,4 +139,4 @@ public void ConfigureAuth(IAppBuilder app) }); } } -} +} \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/ApplicationDomain.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/ApplicationDomain.cs index 10b5433..a1fe014 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/ApplicationDomain.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/ApplicationDomain.cs @@ -119,11 +119,11 @@ public static async Task InitializeAsync() private static async Task AcquirePartnerCenterAccessAsync() { PartnerService.Instance.ApiRootUrl = ConfigurationManager.AppSettings["partnerCenter.apiEndPoint"]; - PartnerService.Instance.ApplicationName = "Web Store Front V1.0"; + PartnerService.Instance.ApplicationName = "Web Store Front V1.1"; var credentials = await PartnerCredentials.Instance.GenerateByApplicationCredentialsAsync( ConfigurationManager.AppSettings["partnercenter.applicationId"], - ConfigurationManager.AppSettings["partnercenter.applicationSecret"], + ConfigurationManager.AppSettings["partnercenter.applicationSecret"], ConfigurationManager.AppSettings["partnercenter.AadTenantId"], ConfigurationManager.AppSettings["aadEndpoint"], ConfigurationManager.AppSettings["aadGraphEndpoint"]); @@ -131,4 +131,4 @@ private static async Task AcquirePartnerCenterAccessAsync() return PartnerService.Instance.CreatePartnerOperations(credentials); } } -} +} \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Branding/PortalBranding.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Branding/PortalBranding.cs index f881d73..d73c177 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Branding/PortalBranding.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Branding/PortalBranding.cs @@ -115,7 +115,7 @@ private async Task NormalizeAsync(BrandingConfiguration brandingConfiguration) if (brandingConfiguration.ContactUs == null) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "ContactUs section not found in portal branding configuration").AddDetail("Field", "ContactUs"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.ContactUsSectionNotFound).AddDetail("Field", "ContactUs"); } brandingConfiguration.ContactUs.Email.AssertNotEmpty("Email"); @@ -126,7 +126,7 @@ private async Task NormalizeAsync(BrandingConfiguration brandingConfiguration) } catch (FormatException) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "Invalid contact us email address").AddDetail("Field", "ContactUs.Email"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidContactUsEmailAddress).AddDetail("Field", "ContactUs.Email"); } try @@ -135,7 +135,7 @@ private async Task NormalizeAsync(BrandingConfiguration brandingConfiguration) } catch (ArgumentException) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "Invalid contact us phone").AddDetail("Field", "ContactUs.Phone"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidContactUsPhoneExceptionMessage).AddDetail("Field", "ContactUs.Phone"); } if (brandingConfiguration.ContactSales == null) @@ -161,7 +161,7 @@ private async Task NormalizeAsync(BrandingConfiguration brandingConfiguration) } catch (FormatException) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "Invalid contact sales email address").AddDetail("Field", "ContactSales.Email"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidContactSalesEmailAddress).AddDetail("Field", "ContactSales.Email"); } } @@ -177,7 +177,7 @@ private async Task NormalizeAsync(BrandingConfiguration brandingConfiguration) } catch (ArgumentException) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "Invalid contact sales phone").AddDetail("Field", "ContactSales.Phone"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidContactSalesPhoneExceptionMessage).AddDetail("Field", "ContactSales.Phone"); } } } diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/CommerceOperations.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/CommerceOperations.cs index 24cef1d..09fdde2 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/CommerceOperations.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/CommerceOperations.cs @@ -8,10 +8,8 @@ namespace Microsoft.Store.PartnerCenter.CustomerPortal.BusinessLogic.Commerce { using System; using System.Collections.Generic; - using System.Diagnostics; using System.Linq; using System.Threading.Tasks; - using Exceptions; using Infrastructure; using Models; using PartnerCenter.Models.Orders; @@ -54,13 +52,12 @@ public CommerceOperations(ApplicationDomain applicationDomain, string customerId /// The subscription's yearly price per seat. /// The prorated amount to charge for the new extra seat. public static decimal CalculateProratedSeatCharge(DateTime expiryDate, decimal yearlyRatePerSeat) - { + { DateTime rightNow = DateTime.UtcNow; expiryDate = expiryDate.ToUniversalTime(); decimal dailyChargePerSeat = yearlyRatePerSeat / 365m; - - // TODO :: Review these conversions. All date based calculations need to be done using datime.date. + // round up the remaining days in case there was a fraction and ensure it does not exceed 365 days decimal remainingDaysTillExpiry = Math.Ceiling(Convert.ToDecimal((expiryDate - rightNow).TotalDays)); remainingDaysTillExpiry = Math.Min(remainingDaysTillExpiry, 365); @@ -71,24 +68,30 @@ public static decimal CalculateProratedSeatCharge(DateTime expiryDate, decimal y /// /// Purchases one or more partner offers. /// - /// A collection of purchase lines items to buy. - /// A transaction result which summarizes its outcomes. - public async Task PurchaseAsync(IEnumerable purchaseLineItems) + /// The order to execute. + /// A transaction result which summarizes its outcome. + public async Task PurchaseAsync(OrderViewModel order) { - purchaseLineItems.AssertNotNull(nameof(purchaseLineItems)); + // use the normalizer to validate the order. + OrderNormalizer orderNormalizer = new OrderNormalizer(this.ApplicationDomain, order); + order = await orderNormalizer.NormalizePurchaseSubscriptionOrderAsync(); - if (purchaseLineItems.Count() <= 0) + // build the purchase line items. + List purchaseLineItems = new List(); + foreach (var orderItem in order.Subscriptions) { - throw new ArgumentException("PurchaseLineItems should have at least one entry", nameof(purchaseLineItems)); - } + string offerId = orderItem.OfferId; + int quantity = orderItem.Quantity; + purchaseLineItems.Add(new PurchaseLineItem(offerId, quantity)); + } + + // associate line items in order to partner offers. var lineItemsWithOffers = await this.AssociateWithPartnerOffersAsync(purchaseLineItems); - ICollection subTransactions = new List(); - - decimal orderTotalPrice = Math.Round(this.CalculateOrderTotalPrice(lineItemsWithOffers), 2); + ICollection subTransactions = new List(); - // we have the order total, prepare payment authorization - var paymentAuthorization = new AuthorizePayment(this.PaymentGateway, orderTotalPrice); + // prepare payment authorization + var paymentAuthorization = new AuthorizePayment(this.PaymentGateway); subTransactions.Add(paymentAuthorization); // build the Partner Center order and pass it to the place order transaction @@ -115,49 +118,40 @@ public async Task PurchaseAsync(IEnumerable // build an aggregated transaction from the previous steps and execute it as a whole await CommerceOperations.RunAggregatedTransaction(subTransactions); - return new TransactionResult(orderTotalPrice, persistSubscriptionsAndPurchases.Result, DateTime.UtcNow); + return new TransactionResult(persistSubscriptionsAndPurchases.Result, DateTime.UtcNow); } /// /// Purchases additional seats for an existing subscription the customer has already bought. /// - /// The ID of the subscription for which to increase its quantity. - /// The number of new seats to purchase on top of the existing ones. + /// The order to execute. /// A transaction result which summarizes its outcome. - public async Task PurchaseAdditionalSeatsAsync(string subscriptionId, int seatsToPurchase) + public async Task PurchaseAdditionalSeatsAsync(OrderViewModel order) { - // validate inputs - subscriptionId.AssertNotEmpty("subscriptionId"); - seatsToPurchase.AssertPositive("seatsToPurchase"); + // use the normalizer to validate the order. + OrderNormalizer orderNormalizer = new OrderNormalizer(this.ApplicationDomain, order); + order = await orderNormalizer.NormalizePurchaseAdditionalSeatsOrderAsync(); + + List orderSubscriptions = order.Subscriptions.ToList(); + string subscriptionId = orderSubscriptions.First().SubscriptionId; + int seatsToPurchase = orderSubscriptions.First().Quantity; + decimal proratedSeatCharge = orderSubscriptions.First().SeatPrice; + string partnerOfferId = orderSubscriptions.First().PartnerOfferId; // we will add up the transactions here ICollection subTransactions = new List(); - // determine the prorated seat charge - var subscriptionToAugment = await this.GetSubscriptionAsync(subscriptionId); - var partnerOffer = await this.ApplicationDomain.OffersRepository.RetrieveAsync(subscriptionToAugment.PartnerOfferId); - - // if subscription expiry date.Date is less than today's UTC date then subcription has expired. - if (subscriptionToAugment.ExpiryDate.Date < DateTime.UtcNow.Date) - { - // this subscription has already expired, don't permit adding seats until the subscription is renewed - throw new PartnerDomainException(ErrorCode.SubscriptionExpired); - } - - decimal proratedSeatCharge = Math.Round(CommerceOperations.CalculateProratedSeatCharge(subscriptionToAugment.ExpiryDate, partnerOffer.Price), 2); - decimal totalCharge = Math.Round(proratedSeatCharge * seatsToPurchase, 2); - // configure a transaction to charge the payment gateway with the prorated rate - var paymentAuthorization = new AuthorizePayment(this.PaymentGateway, totalCharge); + var paymentAuthorization = new AuthorizePayment(this.PaymentGateway); subTransactions.Add(paymentAuthorization); - + // configure a purchase additional seats transaction with the requested seats to purchase subTransactions.Add(new PurchaseExtraSeats( this.ApplicationDomain.PartnerCenterClient.Customers.ById(this.CustomerId).Subscriptions.ById(subscriptionId), seatsToPurchase)); DateTime rightNow = DateTime.UtcNow; - + // record the purchase in our purchase store subTransactions.Add(new RecordPurchase( this.ApplicationDomain.CustomerPurchasesRepository, @@ -168,16 +162,15 @@ public async Task PurchaseAdditionalSeatsAsync(string subscri // build an aggregated transaction from the previous steps and execute it as a whole await CommerceOperations.RunAggregatedTransaction(subTransactions); - + var additionalSeatsPurchaseResult = new TransactionResultLineItem( - subscriptionId, - subscriptionToAugment.PartnerOfferId, + subscriptionId, + partnerOfferId, seatsToPurchase, proratedSeatCharge, seatsToPurchase * proratedSeatCharge); - return new TransactionResult( - totalCharge, + return new TransactionResult( new TransactionResultLineItem[] { additionalSeatsPurchaseResult }, rightNow); } @@ -185,24 +178,21 @@ public async Task PurchaseAdditionalSeatsAsync(string subscri /// /// Renews an existing subscription for a customer. /// - /// The ID of the subscription to renew. + /// The order to execute. /// A transaction result which summarizes its outcome. - public async Task RenewSubscriptionAsync(string subscriptionId) + public async Task RenewSubscriptionAsync(OrderViewModel order) { - // validate inputs - subscriptionId.AssertNotEmpty("subscriptionId"); - - // grab the customer subscription from our store - var subscriptionToAugment = await this.GetSubscriptionAsync(subscriptionId); - - // retrieve the partner offer this subscription relates to, we need to know the current price - var partnerOffer = await this.ApplicationDomain.OffersRepository.RetrieveAsync(subscriptionToAugment.PartnerOfferId); - - if (partnerOffer.IsInactive) - { - // renewing deleted offers is prohibited - throw new PartnerDomainException(ErrorCode.PurchaseDeletedOfferNotAllowed).AddDetail("Id", partnerOffer.Id); - } + // use the normalizer to validate the order. + OrderNormalizer orderNormalizer = new OrderNormalizer(this.ApplicationDomain, order); + order = await orderNormalizer.NormalizeRenewSubscriptionOrderAsync(); + + List orderSubscriptions = order.Subscriptions.ToList(); + string subscriptionId = orderSubscriptions.First().SubscriptionId; + string partnerOfferId = orderSubscriptions.First().PartnerOfferId; + decimal partnerOfferPrice = orderSubscriptions.First().SeatPrice; + DateTime subscriptionExpiryDate = orderSubscriptions.First().SubscriptionExpiryDate; + int quantity = orderSubscriptions.First().Quantity; + decimal totalCharge = Math.Round(quantity * partnerOfferPrice, 2); // retrieve the subscription from Partner Center var subscriptionOperations = this.ApplicationDomain.PartnerCenterClient.Customers.ById(this.CustomerId).Subscriptions.ById(subscriptionId); @@ -210,10 +200,9 @@ public async Task RenewSubscriptionAsync(string subscriptionI // we will add up the transactions here ICollection subTransactions = new List(); - decimal totalCharge = Math.Round(partnerCenterSubscription.Quantity * partnerOffer.Price, 2); // configure a transaction to charge the payment gateway with the prorated rate - var paymentAuthorization = new AuthorizePayment(this.PaymentGateway, totalCharge); + var paymentAuthorization = new AuthorizePayment(this.PaymentGateway); subTransactions.Add(paymentAuthorization); // add a renew subscription transaction to the pipeline @@ -225,13 +214,13 @@ public async Task RenewSubscriptionAsync(string subscriptionI // record the renewal in our purchase store subTransactions.Add(new RecordPurchase( - this.ApplicationDomain.CustomerPurchasesRepository, - new CustomerPurchaseEntity(CommerceOperationType.Renewal, Guid.NewGuid().ToString(), this.CustomerId, subscriptionId, partnerCenterSubscription.Quantity, partnerOffer.Price, rightNow))); - + this.ApplicationDomain.CustomerPurchasesRepository, + new CustomerPurchaseEntity(CommerceOperationType.Renewal, Guid.NewGuid().ToString(), this.CustomerId, subscriptionId, partnerCenterSubscription.Quantity, partnerOfferPrice, rightNow))); + // extend the expiry date by one year subTransactions.Add(new UpdatePersistedSubscription( this.ApplicationDomain.CustomerSubscriptionsRepository, - new CustomerSubscriptionEntity(this.CustomerId, subscriptionId, partnerOffer.Id, subscriptionToAugment.ExpiryDate.AddYears(1)))); + new CustomerSubscriptionEntity(this.CustomerId, subscriptionId, partnerOfferId, subscriptionExpiryDate.AddYears(1)))); // add a capture payment to the transaction pipeline subTransactions.Add(new CapturePayment(this.PaymentGateway, () => paymentAuthorization.Result)); @@ -241,13 +230,12 @@ public async Task RenewSubscriptionAsync(string subscriptionI var renewSubscriptionResult = new TransactionResultLineItem( subscriptionId, - partnerOffer.Id, + partnerOfferId, partnerCenterSubscription.Quantity, - partnerOffer.Price, + partnerOfferPrice, totalCharge); - return new TransactionResult( - totalCharge, + return new TransactionResult( new TransactionResultLineItem[] { renewSubscriptionResult }, rightNow); } @@ -282,25 +270,6 @@ private static async Task RunAggregatedTransaction(IEnumerable - /// Retrieves a customer subscription from persistence. - /// - /// The subscription ID. - /// The matching subscription. - private async Task GetSubscriptionAsync(string subscriptionId) - { - // grab the customer subscription from our store - var customerSubscriptions = await this.ApplicationDomain.CustomerSubscriptionsRepository.RetrieveAsync(this.CustomerId); - var subscriptionToAugment = customerSubscriptions.Where(subscription => subscription.SubscriptionId == subscriptionId).FirstOrDefault(); - - if (subscriptionToAugment == null) - { - throw new PartnerDomainException(ErrorCode.SubscriptionNotFound); - } - - return subscriptionToAugment; - } - /// /// Binds each purchase line item with the partner offer it is requesting. /// @@ -322,17 +291,6 @@ private async Task> AssociateWithPartnerO PartnerOffer offerToPurchase = allPartnerOffers.Where(offer => offer.Id == lineItem.PartnerOfferId).FirstOrDefault(); - if (offerToPurchase == null) - { - // oops, this offer Id is unknown to us - throw new PartnerDomainException(ErrorCode.PartnerOfferNotFound).AddDetail("Id", lineItem.PartnerOfferId); - } - else if (offerToPurchase.IsInactive) - { - // purchasing deleted offers is prohibited - throw new PartnerDomainException(ErrorCode.PurchaseDeletedOfferNotAllowed).AddDetail("Id", offerToPurchase.Id); - } - // associate the line item with the partner offer lineItemToOfferAssociations.Add(new PurchaseLineItemWithOffer(lineItem, offerToPurchase)); } @@ -340,23 +298,6 @@ private async Task> AssociateWithPartnerO return lineItemToOfferAssociations; } - /// - /// Calculates the total price for a given purchase line items collection. - /// - /// A list of purchase line items. - /// The total price. - private decimal CalculateOrderTotalPrice(IEnumerable purchaseLineItems) - { - decimal orderTotalPrice = 0; - - foreach (var lineItem in purchaseLineItems) - { - orderTotalPrice += lineItem.PurchaseLineItem.Quantity * lineItem.PartnerOffer.Price; - } - - return orderTotalPrice; - } - /// /// Builds a Microsoft Partner Center order from a list of purchase line items. /// diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/ICommerceOperations.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/ICommerceOperations.cs index 24df6b8..212b63e 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/ICommerceOperations.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/ICommerceOperations.cs @@ -28,23 +28,22 @@ public interface ICommerceOperations /// /// Purchases one or more partner offers. /// - /// A collection of purchase lines items to buy. + /// The order to execute. /// A transaction result which summarizes its outcome. - Task PurchaseAsync(IEnumerable purchaseLineItems); + Task PurchaseAsync(OrderViewModel order); /// /// Purchases additional seats for an existing subscription the customer has already bought. /// - /// The ID of the subscription for which to increase its quantity. - /// The number of new seats to purchase on top of the existing ones. + /// The order to execute. /// A transaction result which summarizes its outcome. - Task PurchaseAdditionalSeatsAsync(string subscriptionId, int seatsToPurchase); + Task PurchaseAdditionalSeatsAsync(OrderViewModel order); /// /// Renews an existing subscription for a customer. /// - /// The ID of the subscription to renew. + /// The order to execute. /// A transaction result which summarizes its outcome. - Task RenewSubscriptionAsync(string subscriptionId); + Task RenewSubscriptionAsync(OrderViewModel order); } } \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/IPaymentGateway.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/IPaymentGateway.cs index 1c28e7b..39532b6 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/IPaymentGateway.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/IPaymentGateway.cs @@ -14,11 +14,10 @@ namespace Microsoft.Store.PartnerCenter.CustomerPortal.BusinessLogic.Commerce public interface IPaymentGateway { /// - /// Authorizes a payment. Ensures the payment is valid. + /// Executes a payment. /// - /// The amount to charge. - /// An authorization code. - Task AuthorizeAsync(decimal amount); + /// An Authorization code. + Task ExecutePaymentAsync(); /// /// Finalizes an authorized payment. diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/OrderNormalizer.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/OrderNormalizer.cs new file mode 100644 index 0000000..e816dc4 --- /dev/null +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/OrderNormalizer.cs @@ -0,0 +1,255 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.Store.PartnerCenter.CustomerPortal.BusinessLogic.Commerce +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Exceptions; + using Models; + + /// + /// Implements the normalizers for orders. + /// + public class OrderNormalizer : DomainObject + { + /// + /// Initializes a new instance of the class. + /// + /// An application domain instance. + /// The order which will be normalized. + public OrderNormalizer(ApplicationDomain applicationDomain, OrderViewModel order) : base(applicationDomain) + { + order.AssertNotNull(nameof(order)); + + this.Order = order; + } + + /// + /// Gets the Order instance. + /// + public OrderViewModel Order { get; private set; } + + /// + /// Normalizes an order to renew a subscription. + /// + /// Normalized order. + public async Task NormalizeRenewSubscriptionOrderAsync() + { + OrderViewModel order = this.Order; + order.CustomerId.AssertNotEmpty(nameof(order.CustomerId)); + if (order.OperationType != CommerceOperationType.Renewal) + { + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidOperationForOrderMessage).AddDetail("Field", "OperationType"); + } + + // create result order object prefilling it with operation type & customer id. + OrderViewModel orderResult = new OrderViewModel() + { + CustomerId = order.CustomerId, + OperationType = order.OperationType + }; + + order.Subscriptions.AssertNotNull(nameof(order.Subscriptions)); + List orderSubscriptions = order.Subscriptions.ToList(); + if (!(orderSubscriptions.Count == 1)) + { + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.MoreThanOneSubscriptionUpdateErrorMessage); + } + + string subscriptionId = orderSubscriptions.First().SubscriptionId; + subscriptionId.AssertNotEmpty(nameof(subscriptionId)); // is Required for the commerce operation. + + // grab the customer subscription from our store + var subscriptionToAugment = await this.GetSubscriptionAsync(subscriptionId, order.CustomerId); + + // retrieve the partner offer this subscription relates to, we need to know the current price + var partnerOffer = await ApplicationDomain.Instance.OffersRepository.RetrieveAsync(subscriptionToAugment.PartnerOfferId); + + if (partnerOffer.IsInactive) + { + // renewing deleted offers is prohibited + throw new PartnerDomainException(ErrorCode.PurchaseDeletedOfferNotAllowed).AddDetail("Id", partnerOffer.Id); + } + + // retrieve the subscription from Partner Center + var subscriptionOperations = ApplicationDomain.Instance.PartnerCenterClient.Customers.ById(order.CustomerId).Subscriptions.ById(subscriptionId); + var partnerCenterSubscription = await subscriptionOperations.GetAsync(); + + List resultOrderSubscriptions = new List(); + resultOrderSubscriptions.Add(new OrderSubscriptionItemViewModel() + { + OfferId = subscriptionId, + SubscriptionId = subscriptionId, + PartnerOfferId = subscriptionToAugment.PartnerOfferId, + SubscriptionExpiryDate = subscriptionToAugment.ExpiryDate, + Quantity = partnerCenterSubscription.Quantity, + SeatPrice = partnerOffer.Price, + SubscriptionName = partnerOffer.Title + }); + + orderResult.Subscriptions = resultOrderSubscriptions; + return await Task.FromResult(orderResult); + } + + /// + /// Normalizes an order to purchase net new subscriptions. + /// + /// Normalized order. + public async Task NormalizePurchaseSubscriptionOrderAsync() + { + OrderViewModel order = this.Order; + order.CustomerId.AssertNotEmpty(nameof(order.CustomerId)); + if (order.OperationType != CommerceOperationType.NewPurchase) + { + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidOperationForOrderMessage).AddDetail("Field", "OperationType"); + } + + // create result order object prefilling it with operation type & customer id. + OrderViewModel orderResult = new OrderViewModel() + { + CustomerId = order.CustomerId, + OperationType = order.OperationType + }; + + order.Subscriptions.AssertNotNull(nameof(order.Subscriptions)); + List orderSubscriptions = order.Subscriptions.ToList(); + if (orderSubscriptions.Count < 1) + { + throw new ArgumentException(Resources.NotEnoughItemsInOrderErrorMessage, nameof(order.Subscriptions)); + } + + // retrieve all the partner offers to match against them + IEnumerable allPartnerOffers = await ApplicationDomain.Instance.OffersRepository.RetrieveAsync(); + + List resultOrderSubscriptions = new List(); + foreach (var lineItem in orderSubscriptions) + { + PartnerOffer offerToPurchase = allPartnerOffers.Where(offer => offer.Id == lineItem.SubscriptionId).FirstOrDefault(); + + if (offerToPurchase == null) + { + // oops, this offer Id is unknown to us + throw new PartnerDomainException(ErrorCode.PartnerOfferNotFound).AddDetail("Id", lineItem.SubscriptionId); + } + else if (offerToPurchase.IsInactive) + { + // purchasing deleted offers is prohibited + throw new PartnerDomainException(ErrorCode.PurchaseDeletedOfferNotAllowed).AddDetail("Id", offerToPurchase.Id); + } + + // populate details for each order subscription item to purchase. + resultOrderSubscriptions.Add(new OrderSubscriptionItemViewModel() + { + OfferId = offerToPurchase.Id, + SubscriptionId = offerToPurchase.Id, + Quantity = lineItem.Quantity, + SeatPrice = offerToPurchase.Price, + SubscriptionName = offerToPurchase.Title + }); + } + + orderResult.Subscriptions = resultOrderSubscriptions; + return await Task.FromResult(orderResult); + } + + /// + /// Normalizes an order to add seats to a subscription. + /// + /// Normalized order. + public async Task NormalizePurchaseAdditionalSeatsOrderAsync() + { + OrderViewModel order = this.Order; + order.CustomerId.AssertNotEmpty(nameof(order.CustomerId)); + if (order.OperationType != CommerceOperationType.AdditionalSeatsPurchase) + { + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidOperationForOrderMessage).AddDetail("Field", "OperationType"); + } + + // create result order object prefilling it with operation type & customer id. + OrderViewModel orderResult = new OrderViewModel() + { + CustomerId = order.CustomerId, + OperationType = order.OperationType + }; + + order.Subscriptions.AssertNotNull(nameof(order.Subscriptions)); + List orderSubscriptions = order.Subscriptions.ToList(); + if (!(orderSubscriptions.Count == 1)) + { + throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", Resources.MoreThanOneSubscriptionUpdateErrorMessage); + } + + string subscriptionId = orderSubscriptions.First().SubscriptionId; + int seatsToPurchase = orderSubscriptions.First().Quantity; + subscriptionId.AssertNotEmpty(nameof(subscriptionId)); // is Required for the commerce operation. + seatsToPurchase.AssertPositive("seatsToPurchase"); + + // grab the customer subscription from our store + var subscriptionToAugment = await this.GetSubscriptionAsync(subscriptionId, order.CustomerId); + + // retrieve the partner offer this subscription relates to, we need to know the current price + var partnerOffer = await ApplicationDomain.Instance.OffersRepository.RetrieveAsync(subscriptionToAugment.PartnerOfferId); + + if (partnerOffer.IsInactive) + { + // renewing deleted offers is prohibited + throw new PartnerDomainException(ErrorCode.PurchaseDeletedOfferNotAllowed).AddDetail("Id", partnerOffer.Id); + } + + // retrieve the subscription from Partner Center + var subscriptionOperations = ApplicationDomain.Instance.PartnerCenterClient.Customers.ById(order.CustomerId).Subscriptions.ById(subscriptionId); + var partnerCenterSubscription = await subscriptionOperations.GetAsync(); + + // if subscription expiry date.Date is less than today's UTC date then subcription has expired. + if (subscriptionToAugment.ExpiryDate.Date < DateTime.UtcNow.Date) + { + // this subscription has already expired, don't permit adding seats until the subscription is renewed + throw new PartnerDomainException(ErrorCode.SubscriptionExpired); + } + + decimal proratedSeatCharge = Math.Round(CommerceOperations.CalculateProratedSeatCharge(subscriptionToAugment.ExpiryDate, partnerOffer.Price), 2); + decimal totalCharge = Math.Round(proratedSeatCharge * seatsToPurchase, 2); + + List resultOrderSubscriptions = new List(); + resultOrderSubscriptions.Add(new OrderSubscriptionItemViewModel() + { + OfferId = subscriptionId, + SubscriptionId = subscriptionId, + PartnerOfferId = subscriptionToAugment.PartnerOfferId, + SubscriptionExpiryDate = subscriptionToAugment.ExpiryDate, + Quantity = seatsToPurchase, + SeatPrice = proratedSeatCharge, + SubscriptionName = partnerOffer.Title + }); + + orderResult.Subscriptions = resultOrderSubscriptions; + return await Task.FromResult(orderResult); + } + + /// + /// Retrieves a customer subscription from persistence. + /// + /// The subscription ID. + /// The customer ID. + /// The matching subscription. + private async Task GetSubscriptionAsync(string subscriptionId, string customerId) + { + // grab the customer subscription from our store + var customerSubscriptions = await ApplicationDomain.Instance.CustomerSubscriptionsRepository.RetrieveAsync(customerId); + var subscriptionToAugment = customerSubscriptions.Where(subscription => subscription.SubscriptionId == subscriptionId).FirstOrDefault(); + + if (subscriptionToAugment == null) + { + throw new PartnerDomainException(ErrorCode.SubscriptionNotFound); + } + + return subscriptionToAugment; + } + } +} \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/PaymentConfigurationRepository.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/PaymentConfigurationRepository.cs index dbf8acb..4d1f336 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/PaymentConfigurationRepository.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/PaymentConfigurationRepository.cs @@ -110,13 +110,14 @@ private async Task NormalizeAsync(PaymentConfiguration paymentConfiguration) { paymentConfiguration.AssertNotNull(nameof(paymentConfiguration)); + // Dont validate WebExperienceProfileId since it will break upgrade as existing deployments dont have this configuration. paymentConfiguration.ClientId.AssertNotEmpty("ClientId"); paymentConfiguration.ClientSecret.AssertNotEmpty("ClientSecret"); paymentConfiguration.AccountType.AssertNotEmpty("Mode"); if (!this.supportedPaymentModes.Contains(paymentConfiguration.AccountType)) { - throw new PartnerDomainException("Payment mode is not supported"); + throw new PartnerDomainException(Resources.InvalidPaymentModeErrorMessage); } await Task.FromResult(0); diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/PaymentGateways/PayPalGateway.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/PaymentGateways/PayPalGateway.cs index 308b2a1..7520f0b 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/PaymentGateways/PayPalGateway.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/PaymentGateways/PayPalGateway.cs @@ -15,7 +15,7 @@ namespace Microsoft.Store.PartnerCenter.CustomerPortal.BusinessLogic.Commerce.Pa using Exceptions; using Models; using PayPal; - using PayPal.Api; + using PayPal.Api; /// /// PayPal payment gateway implementation. @@ -23,9 +23,14 @@ namespace Microsoft.Store.PartnerCenter.CustomerPortal.BusinessLogic.Commerce.Pa public class PayPalGateway : DomainObject, IPaymentGateway { /// - /// Maintains the credit card against which the payment is being managed. + /// Maintains the payer id for the payment gateway. /// - private readonly Models.PaymentCard creditCard; + private readonly string payerId; + + /// + /// Maintains the payment id for the payment gateway. + /// + private readonly string paymentId; /// /// Maintains the description for this payment. @@ -35,22 +40,31 @@ public class PayPalGateway : DomainObject, IPaymentGateway /// /// Initializes a new instance of the class. /// - /// The ApplicationDomain - /// The Payment Card used in this commerce operation. + /// The ApplicationDomain /// The description which will be added to the Payment Card authorization call. - public PayPalGateway(ApplicationDomain applicationDomain, Models.PaymentCard paymentCard, string description) : base(applicationDomain) - { - paymentCard.AssertNotNull(nameof(paymentCard)); + public PayPalGateway(ApplicationDomain applicationDomain, string description) : base(applicationDomain) + { description.AssertNotEmpty(nameof(description)); + this.paymentDescription = description; - // clean up credit card number input. - string cardNumber = paymentCard.CreditCardNumber; - cardNumber = cardNumber.Replace(" ", string.Empty); // remove empty spaces in the string. - cardNumber = cardNumber.Replace("-", string.Empty); // remove dashes. - paymentCard.CreditCardNumber = cardNumber; + this.payerId = string.Empty; + this.paymentId = string.Empty; + } - this.creditCard = paymentCard; - this.paymentDescription = description; + /// + /// Initializes a new instance of the class. + /// + /// The ApplicationDomain + /// The Payer Id. + /// The Payment Id. + public PayPalGateway(ApplicationDomain applicationDomain, string payerId, string paymentId) : base(applicationDomain) + { + payerId.AssertNotEmpty(nameof(payerId)); + paymentId.AssertNotEmpty(nameof(paymentId)); + + this.payerId = payerId; + this.paymentId = paymentId; + this.paymentDescription = string.Empty; } /// @@ -102,91 +116,183 @@ public static void ValidateConfiguration(PaymentConfiguration paymentConfig) } /// - /// Authorizes a payment with PayPal. Ensures the payment is valid. + /// Creates Web Experience profile using portal branding and payment configuration. /// - /// The amount to charge. - /// An authorization code. - public async Task AuthorizeAsync(decimal amount) + /// The Payment configuration. + /// The branding configuration. + /// The locale code used by the web experience profile. Example-US. + /// The created web experience profile id. + public static string CreateWebExperienceProfile(PaymentConfiguration paymentConfig, BrandingConfiguration brandConfig, string countryIso2Code) { - amount.AssertPositive(nameof(amount)); - - CreditCard customerCard = new CreditCard(); - customerCard.first_name = this.creditCard.CardHolderFirstName; - customerCard.last_name = this.creditCard.CardHolderLastName; - customerCard.expire_month = this.creditCard.CreditCardExpiryMonth; - customerCard.expire_year = this.creditCard.CreditCardExpiryYear; - customerCard.number = this.creditCard.CreditCardNumber; - customerCard.type = this.creditCard.CreditCardType; - customerCard.cvv2 = this.creditCard.CreditCardCvn; - - //// PayPal expects the following in the amount (string)input during authorization... - //// There are a few currencies where decimals are not supported. For future consideration. - //// Currency amount must be - //// non - negative number, - //// may optionally contain exactly 2 decimal places separated by '.', - //// optional thousands separator ',', - //// limited to 7 digits before the decimal point], - //// Conversion needs to be done since Commerce layer treats it as a decimal while PayPal expects a string. - //// "12.0"(ie - without the additional zero...expected would be "12.00"). https://msdn.microsoft.com/en-us/library/dwhawy9k(v=vs.110).aspx#FFormatString - - string currency = this.ApplicationDomain.PortalLocalization.CurrencyCode; - string orderAmt = amount.ToString("F", CultureInfo.InvariantCulture); - - // Create Payment Object. - var payment = new Payment() + try { - // Intent is to Authroize this Payment. - intent = "authorize", // required - // Payer Object (Required) - // A resource representing a Payer that funds a payment. Use the List of `FundingInstrument` and the Payment Method as 'credit_card' - payer = new Payer() + Dictionary configMap = new Dictionary(); + configMap.Add("clientId", paymentConfig.ClientId); + configMap.Add("clientSecret", paymentConfig.ClientSecret); + configMap.Add("mode", paymentConfig.AccountType); + configMap.Add("connectionTimeout", "120000"); + + string accessToken = new OAuthTokenCredential(configMap).GetAccessToken(); + var apiContext = new APIContext(accessToken); + apiContext.Config = configMap; + + // Pickup logo & brand name from branding configuration. + // create the web experience profile. + var profile = new WebProfile { - payment_method = "credit_card", // required - // funding instruments object []. - // The Payment creation API requires a list of - // FundingInstrument; add the created `FundingInstrument` to a List - funding_instruments = new List() + name = Guid.NewGuid().ToString(), + presentation = new Presentation { - // A resource representing a Payeer's funding instrument. and the `CreditCardDetails` - new FundingInstrument() - { - // A resource representing a credit card that can be used to fund a payment. - credit_card = customerCard - } + brand_name = brandConfig.OrganizationName, + logo_image = brandConfig.HeaderImage.ToString(), + locale_code = countryIso2Code }, - }, - - // transactions object []. (Required) - // The Payment creation API requires a list of transactions; add the created `Transaction` to a List + input_fields = new InputFields() + { + address_override = 1, + allow_note = false, + no_shipping = 1 + }, + flow_config = new FlowConfig() + { + landing_page_type = "billing" + } + }; + + var createdProfile = profile.Create(apiContext); + return createdProfile.id; + } + catch (PayPalException paypalException) + { + if (paypalException is IdentityException) + { + // thrown when API Context couldn't be setup. + IdentityException identityFailure = paypalException as IdentityException; + IdentityError failureDetails = identityFailure.Details; + if (failureDetails != null && failureDetails.error.ToLower() == "invalid_client") + { + throw new PartnerDomainException(ErrorCode.PaymentGatewayIdentityFailureDuringConfiguration).AddDetail("ErrorMessage", Resources.PaymentGatewayIdentityFailureDuringConfiguration); + } + } + + // if this is not an identity exception rather some other issue. + throw new PartnerDomainException(ErrorCode.PaymentGatewayFailure).AddDetail("ErrorMessage", paypalException.Message); + } + } + + /// + /// Creates a payment transaction and returns the PayPal generated payment URL. + /// + /// The redirect url for PayPal callback to web store portal. + /// The order details for which payment needs to be made. + /// Payment URL from PayPal. + public async Task GeneratePaymentUriAsync(string redirectUrl, OrderViewModel order) + { + string paypalRedirectUrl = string.Empty; + + redirectUrl.AssertNotEmpty(nameof(redirectUrl)); + order.AssertNotNull(nameof(order)); + + APIContext apiContext = await this.GetAPIContextAsync(); + decimal paymentTotal = 0; + + // Create itemlist and add item objects to it. + var itemList = new ItemList() { items = new List() }; + foreach (var subscriptionItem in order.Subscriptions) + { + itemList.items.Add(new Item() + { + name = subscriptionItem.SubscriptionName, + description = this.paymentDescription, + sku = subscriptionItem.SubscriptionId, + currency = this.ApplicationDomain.PortalLocalization.CurrencyCode, + price = subscriptionItem.SeatPrice.ToString("F", CultureInfo.InvariantCulture), // TODO :: Loc. Validate for other cultures. + quantity = subscriptionItem.Quantity.ToString() + }); + paymentTotal += Math.Round(subscriptionItem.Quantity * subscriptionItem.SeatPrice, 2); + } + + string webExperienceId = string.Empty; + apiContext.Config.TryGetValue("WebExperienceProfileId", out webExperienceId); + + Payment payment = new Payment() + { + intent = "authorize", + payer = new Payer() { payment_method = "paypal" }, + experience_profile_id = webExperienceId, // if null its ok, PayPal will pick up the default settings based on PayPal client configuration. transactions = new List() - { - // A transaction defines the contract of a payment - what is the payment for and who is fulfilling it. Transaction is created with a `Payee` and `Amount` types + { new Transaction() { - // Let's you specify a payment amount. + description = this.paymentDescription, + custom = string.Format(CultureInfo.InvariantCulture, "{0}#{1}", order.CustomerId, order.OperationType.ToString()), + item_list = itemList, amount = new Amount() - { - currency = currency, // Required - total = orderAmt // Required - }, - description = this.paymentDescription - } - } + { + currency = this.ApplicationDomain.PortalLocalization.CurrencyCode, + total = paymentTotal.ToString("F", CultureInfo.InvariantCulture) // TODO :: Loc. Validate for other cultures. + } + } + }, + redirect_urls = new RedirectUrls() + { + return_url = redirectUrl + "&payment=success", + cancel_url = redirectUrl + "&payment=failure" + } }; - // Create a payment by posting to the APIService using a valid APIContext - // Retrieve an Authorization Id by making a Payment with intent as 'authorize' and parsing through the Payment object - Payment createdPayment = null; try { - createdPayment = payment.Create(await this.GetAPIContext()); + // CreatePayment function gives us the payment approval url + // on which payer is redirected for paypal acccount payment + var createdPayment = payment.Create(apiContext); + + // get links returned from paypal in response to Create function call + var links = createdPayment.links.GetEnumerator(); + while (links.MoveNext()) + { + Links lnk = links.Current; + if (lnk.rel.ToLower().Trim().Equals("approval_url")) + { + paypalRedirectUrl = lnk.href; + } + } + + return await Task.FromResult(paypalRedirectUrl); + } + catch (PayPalException ex) + { + this.ParsePayPalException(ex); + } + + return await Task.FromResult(string.Empty); + } + + /// + /// Executes a PayPal payment. + /// + /// Capture string id. + public async Task ExecutePaymentAsync() + { + APIContext apiContext = await this.GetAPIContextAsync(); + try + { + Payment payment = new Payment() { id = this.paymentId }; + var paymentExecution = new PaymentExecution() { payer_id = this.payerId }; + var paymentResult = payment.Execute(apiContext, paymentExecution); + + if (paymentResult.state.ToLowerInvariant() == "approved") + { + string authorizationCode = paymentResult.transactions[0].related_resources[0].authorization.id; + return await Task.FromResult(authorizationCode); + } } catch (PayPalException ex) { this.ParsePayPalException(ex); - } + } - return await Task.FromResult(createdPayment.transactions[0].related_resources[0].authorization.id); + return await Task.FromResult(string.Empty); } /// @@ -202,7 +308,7 @@ public async Task CaptureAsync(string authorizationCode) authorizationCode.AssertNotEmpty(nameof(authorizationCode)); - APIContext apiContext = await this.GetAPIContext(); + APIContext apiContext = await this.GetAPIContextAsync(); // given the authorizationId. Lookup the authorization to find the amount. try @@ -243,7 +349,7 @@ public async Task VoidAsync(string authorizationCode) // given the authorizationId string... Lookup the authorization to void it. try { - APIContext apiContext = await this.GetAPIContext(); + APIContext apiContext = await this.GetAPIContextAsync(); Authorization cardAuthorization = Authorization.Get(apiContext, authorizationCode); cardAuthorization.Void(apiContext); await Task.FromResult(string.Empty); @@ -254,11 +360,60 @@ public async Task VoidAsync(string authorizationCode) } } + /// + /// Retrieves the Order from a payment transaction. + /// + /// The Order for which payment was made. + public async Task GetOrderDetailsFromPaymentAsync() + { + OrderViewModel orderFromPayment = null; + APIContext apiContext = await this.GetAPIContextAsync(); + + try + { + // the get will retrieve the payment information. iterate the items in the transaction collection to extract details. + Payment paymentDetails = Payment.Get(apiContext, this.paymentId); + orderFromPayment = new OrderViewModel(); + List orderSubscriptions = new List(); + + if (paymentDetails.transactions.Count > 0) + { + string customData = paymentDetails.transactions[0].custom; + + // parse out the customer Id & operation type from customData. + string[] customDataArray = customData.Split("#".ToCharArray()); + if (customDataArray.Length == 2) + { + orderFromPayment.CustomerId = customDataArray[0]; + orderFromPayment.OperationType = (CommerceOperationType)Enum.Parse(typeof(CommerceOperationType), customDataArray[1], true); + } + + foreach (var paymentTransactionItem in paymentDetails.transactions[0].item_list.items) + { + orderSubscriptions.Add(new OrderSubscriptionItemViewModel() + { + SubscriptionId = paymentTransactionItem.sku, + OfferId = paymentTransactionItem.sku, + Quantity = Convert.ToInt32(paymentTransactionItem.quantity, CultureInfo.InvariantCulture) // TODO :: Loc. Validate for other cultures. + }); + } + } + + orderFromPayment.Subscriptions = orderSubscriptions; + } + catch (PayPalException ex) + { + this.ParsePayPalException(ex); + } + + return await Task.FromResult(orderFromPayment); + } + /// /// Retrieves the API Context for PayPal. /// /// PayPal APIContext - private async Task GetAPIContext() + private async Task GetAPIContextAsync() { //// The GetAccessToken() of the SDK Returns the currently cached access token. //// If no access token was previously cached, or if the current access token is expired, then a new one is generated and returned. @@ -271,6 +426,7 @@ private async Task GetAPIContext() configMap.Add("clientId", paymentConfig.ClientId); configMap.Add("clientSecret", paymentConfig.ClientSecret); configMap.Add("mode", paymentConfig.AccountType); + configMap.Add("WebExperienceProfileId", paymentConfig.WebExperienceProfileId); configMap.Add("connectionTimeout", "120000"); string accessToken = new OAuthTokenCredential(configMap).GetAccessToken(); @@ -292,7 +448,8 @@ private void ParsePayPalException(PayPalException ex) // Get the details of this exception with ex.Details and format the error message in the form of "We are unable to process your payment – {Errormessage} :: [err1, err2, .., errN]". StringBuilder errorString = new StringBuilder(); errorString.Append(Resources.PaymentGatewayErrorPrefix); - + + // build error string for errors returned from financial institutions. if (pe.Details != null) { string errorName = pe.Details.name.ToUpper(); @@ -302,20 +459,6 @@ private void ParsePayPalException(PayPalException ex) errorString.Append(pe.Details.message); throw new PartnerDomainException(ErrorCode.PaymentGatewayFailure).AddDetail("ErrorMessage", errorString.ToString()); } - - // build error string for errors returned from financial institutions. - if (errorName.Contains("CREDIT_CARD_REFUSED")) - { - throw new PartnerDomainException(ErrorCode.CardRefused); - } - else if (errorName.Contains("EXPIRED_CREDIT_CARD")) - { - throw new PartnerDomainException(ErrorCode.CardExpired); - } - else if (errorName.Contains("CREDIT_CARD_CVV_CHECK_FAILED")) - { - throw new PartnerDomainException(ErrorCode.CardCVNCheckFailed); - } else if (errorName.Contains("UNKNOWN_ERROR")) { throw new PartnerDomainException(ErrorCode.PaymentGatewayPaymentError); @@ -339,8 +482,8 @@ private void ParsePayPalException(PayPalException ex) errorString.Replace(',', ']', errorString.Length - 2, 2); // remove the last comma and replace it with ]. } else - { - errorString.Append(Resources.UseAlternateCardMessage); + { + errorString.Append(Resources.PayPalUnableToProcessPayment); } } @@ -365,4 +508,4 @@ private void ParsePayPalException(PayPalException ex) } } } -} +} \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/Transactions/AuthorizePayment.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/Transactions/AuthorizePayment.cs index 49b1804..c510d89 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/Transactions/AuthorizePayment.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/Transactions/AuthorizePayment.cs @@ -19,14 +19,11 @@ public class AuthorizePayment : IBusinessTransactionWithOutput /// /// Initializes a new instance of the class. /// - /// The payment gateway to use for authorization. - /// The amount to charge. - public AuthorizePayment(IPaymentGateway paymentGateway, decimal amountToCharge) - { - paymentGateway.AssertNotNull(nameof(paymentGateway)); - amountToCharge.AssertPositive(nameof(amountToCharge)); - - this.Amount = amountToCharge; + /// The payment gateway to use for authorization. + public AuthorizePayment(IPaymentGateway paymentGateway) + { + paymentGateway.AssertNotNull(nameof(paymentGateway)); + this.PaymentGateway = paymentGateway; } @@ -35,11 +32,6 @@ public AuthorizePayment(IPaymentGateway paymentGateway, decimal amountToCharge) /// public IPaymentGateway PaymentGateway { get; private set; } - /// - /// Gets the amount to charge. - /// - public decimal Amount { get; private set; } - /// /// Gets the authorization code. /// @@ -52,7 +44,7 @@ public AuthorizePayment(IPaymentGateway paymentGateway, decimal amountToCharge) public async Task ExecuteAsync() { // authorize with the payment gateway - this.Result = await this.PaymentGateway.AuthorizeAsync(this.Amount); + this.Result = await this.PaymentGateway.ExecutePaymentAsync(); } /// diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/Transactions/RenewSubscription.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/Transactions/RenewSubscription.cs index dd9d416..c8bb4c1 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/Transactions/RenewSubscription.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Commerce/Transactions/RenewSubscription.cs @@ -67,8 +67,8 @@ public async Task ExecuteAsync() catch (PartnerException subscriptionUpdateProblem) { string exceptionMessage = string.Format( - CultureInfo.InvariantCulture, - "RenewSubscription.ExecuteAsync() Failed: {0}, SubscriptionId: {1}", + CultureInfo.InvariantCulture, + Resources.RenewSubscriptionFailedMessage, subscriptionUpdateProblem, this.existingSubscription.Id); diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/CustomerPortalPrincipal.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/CustomerPortalPrincipal.cs index 6559be4..fd72863 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/CustomerPortalPrincipal.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/CustomerPortalPrincipal.cs @@ -71,4 +71,4 @@ public bool IsPartnerCenterCustomer /// public string PartnerCenterCustomerId { get; private set; } } -} +} \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Customers/PartnerCenterCustomersRepository.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Customers/PartnerCenterCustomersRepository.cs index af51a5d..da7ef16 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Customers/PartnerCenterCustomersRepository.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Customers/PartnerCenterCustomersRepository.cs @@ -44,8 +44,8 @@ public async Task RegisterAsync(string tenantId, string partnerCenterCustomerId) if (!string.IsNullOrWhiteSpace(existingCustomerId)) { throw new InvalidOperationException(string.Format( - CultureInfo.InvariantCulture, - "{0} AD tenant is already registered to Partner Center customer: {1}", + CultureInfo.InvariantCulture, + Resources.ADTenantIsAlreadyRegisteredToCustomer, tenantId, existingCustomerId)); } diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Exceptions/ErrorCode.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Exceptions/ErrorCode.cs index 73cad0a..e956e7a 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Exceptions/ErrorCode.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Exceptions/ErrorCode.cs @@ -101,21 +101,6 @@ public enum ErrorCode /// PaymentGatewayIdentityFailureDuringPayment, - /// - /// Failure in payment gateway due to card refusal by financial institution. - /// - CardRefused, - - /// - /// Failure in payment gateway due to use of an expired card during payment. - /// - CardExpired, - - /// - /// Failure in payment gateway due to invalid CVN. - /// - CardCVNCheckFailed, - /// /// Failure in payment gateway during payment. /// diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/MicrosoftOfferLogoIndexer.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/MicrosoftOfferLogoIndexer.cs index 3c32f30..a621577 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/MicrosoftOfferLogoIndexer.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/MicrosoftOfferLogoIndexer.cs @@ -108,8 +108,10 @@ public async Task GetOfferLogoUriAsync(Offer offer) /// A task. private async Task IndexOffersAsync() { - // retrieve the offers in english + // TODO :: Loc. Need to manage this based on the partner's country locale to retrieve localized offers for the store front. var usaBasedPartnerCenterClient = this.ApplicationDomain.PartnerCenterClient.With(RequestContextFactory.Instance.Create("EN-US")); + + // retrieve the offers in english var englishLocalizedOffers = await usaBasedPartnerCenterClient.Offers.ByCountry(this.ApplicationDomain.PortalLocalization.CountryIso2Code).GetAsync(); foreach (var offer in englishLocalizedOffers.Items) diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/PartnerOfferNormalizer.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/PartnerOfferNormalizer.cs index db496c2..09fffbf 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/PartnerOfferNormalizer.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/PartnerOfferNormalizer.cs @@ -29,19 +29,19 @@ public void Normalize(PartnerOffer partnerOffer) if (!Guid.TryParse(partnerOffer.Id, out offerId)) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "Id must be a valid GUID").AddDetail("Field", "Id"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.IdMustBeAValidGUID).AddDetail("Field", "Id"); } if (string.IsNullOrWhiteSpace(partnerOffer.MicrosoftOfferId)) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "MicrosoftOfferId must be set").AddDetail("Field", "MicrosoftOfferId"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.MicrosoftOfferIdMustBeSet).AddDetail("Field", "MicrosoftOfferId"); } partnerOffer.Title.AssertNotEmpty("Offer title"); if (partnerOffer.Price <= 0) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "Offer price should be more than zero").AddDetail("Field", "Price"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.OfferPriceShouldBeMoreThanZero).AddDetail("Field", "Price"); } partnerOffer.Features = PartnerOfferNormalizer.CleanupEmptyEntries(partnerOffer.Features); diff --git a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/PartnerOffersRepository.cs b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/PartnerOffersRepository.cs index 30fa11c..e0ae2e6 100644 --- a/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/PartnerOffersRepository.cs +++ b/Source/PartnerCenter.CustomerPortal/BusinessLogic/Offers/PartnerOffersRepository.cs @@ -109,7 +109,7 @@ public async Task RetrieveAsync(string partnerOfferId) } else { - throw new PartnerDomainException(ErrorCode.PartnerOfferNotFound, "Offer not found"); + throw new PartnerDomainException(ErrorCode.PartnerOfferNotFound, Resources.OfferNotFound); } } @@ -202,13 +202,13 @@ public async Task UpdateAsync(PartnerOffer partnerOfferUpdate) if (existingPartnerOffer == null) { - throw new PartnerDomainException(ErrorCode.PartnerOfferNotFound, "Offer not found"); + throw new PartnerDomainException(ErrorCode.PartnerOfferNotFound, Resources.OfferNotFound); } if (existingPartnerOffer.MicrosoftOfferId != partnerOfferUpdate.MicrosoftOfferId) { // we do not allow changing the Microsoft offer association since there may be existing purchases that purchased the original Microsoft offer - throw new PartnerDomainException(ErrorCode.MicrosoftOfferImmutable, "Microsoft offer is not allowed to be updated. Create a new offer instead."); + throw new PartnerDomainException(ErrorCode.MicrosoftOfferImmutable, Resources.MicrosoftOfferImmutableErrorMessage); } allPartnerOffers[allPartnerOffers.IndexOf(existingPartnerOffer)] = partnerOfferUpdate; @@ -265,7 +265,7 @@ private async Task> UpdateAsync(ICollection /// The web portal AD tenant ID. - /// + /// private const string WebPortalAadTenantID = "webPortal.AadTenantId"; /// @@ -204,4 +204,4 @@ public static string CacheConnectionString } } } -} +} \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Configuration/WebPortalConfiguration.json b/Source/PartnerCenter.CustomerPortal/Configuration/WebPortalConfiguration.json index 70b554b..ea6a0ce 100644 --- a/Source/PartnerCenter.CustomerPortal/Configuration/WebPortalConfiguration.json +++ b/Source/PartnerCenter.CustomerPortal/Configuration/WebPortalConfiguration.json @@ -287,24 +287,6 @@ } ] }, - { - "Name": "CreditCardInputView", - "DefaultAssetVersion": "1.0", - "Assets": [ - { - "Version": "1.0", - "JavaScript": [ - "~/Scripts/Plugins/Views/CreditCardInputView.js" - ], - "Css": [ - "~/Content/Styles/Plugins/CreditCardInput.css" - ], - "Templates": [ - "~/Views/Controls/CreditCardInput.cshtml" - ] - } - ] - }, { "Name": "OfferTile", "DefaultAssetVersion": "1.0", @@ -356,9 +338,10 @@ "Assets": [ { "Version": "1.0", - "JavaScript": [ - "~/Scripts/Plugins/ErrorCode.js" - ], + "JavaScript": [ + "~/Scripts/Plugins/ErrorCode.js", + "~/Scripts/Plugins/CommerceOperationType.js" + ], "Css": [ "~/Content/Styles/Plugins/Common.css", "~/Content/Styles/Plugins/AddSubscriptions.css" @@ -372,53 +355,69 @@ "DisplayName": "Home", "Tile": "/Content/Images/Plugins/Tiles/home-tile.png", "DefaultFeature": "Home", - "Features": [ + "Features": [ + { + "Name": "Home", + "DefaultAssetVersion": "1.0", + "Assets": [ { - "Name": "Home", - "DefaultAssetVersion": "1.0", - "Assets": [ - { - "Version": "1.0", - "JavaScript": [ - "~/Scripts/Plugins/HomePagePresenter.js" - ], - "Css": [ - "~/Content/Styles/Plugins/HomePage.css" - ] - } - ] - }, + "Version": "1.0", + "JavaScript": [ + "~/Scripts/Plugins/HomePagePresenter.js" + ], + "Css": [ + "~/Content/Styles/Plugins/HomePage.css" + ] + } + ] + }, + { + "Name": "CustomerRegistration", + "DefaultAssetVersion": "1.0", + "Assets": [ { - "Name": "CustomerRegistration", - "DefaultAssetVersion": "1.0", - "Assets": [ - { - "Version": "1.0", - "JavaScript": [ - "~/Scripts/Plugins/CustomerRegistrationPresenter.js" - ], - "Css": [ - "~/Content/Styles/Plugins/CustomerRegistration.css" - ] - } - ] - }, + "Version": "1.0", + "JavaScript": [ + "~/Scripts/Plugins/CustomerRegistrationPresenter.js" + ], + "Css": [ + "~/Content/Styles/Plugins/CustomerRegistration.css" + ] + } + ] + }, + { + "Name": "RegistrationConfirmation", + "DefaultAssetVersion": "1.0", + "Assets": [ { - "Name": "RegistrationConfirmation", - "DefaultAssetVersion": "1.0", - "Assets": [ - { - "Version": "1.0", - "JavaScript": [ - "~/Scripts/Plugins/RegistrationConfirmationPresenter.js" - ], - "Css": [ - "~/Content/Styles/Plugins/RegistrationConfirmation.css" - ] - } - ] + "Version": "1.0", + "JavaScript": [ + "~/Scripts/Plugins/RegistrationConfirmationPresenter.js" + ], + "Css": [ + "~/Content/Styles/Plugins/RegistrationConfirmation.css" + ] } - ] + ] + }, + { + "Name": "ProcessOrder", + "DefaultAssetVersion": "1.0", + "Assets": [ + { + "Version": "1.0", + "JavaScript": [ + "~/Scripts/Plugins/ProcessOrderPresenter.js" + ], + "Css": [ + "~/Content/Styles/Plugins/ProcessOrder.css" + ] + } + ] + } + + ] }, { "Name": "AdminConsole", diff --git a/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/CreditCardInput.css b/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/CreditCardInput.css deleted file mode 100644 index 406d8c1..0000000 --- a/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/CreditCardInput.css +++ /dev/null @@ -1,15 +0,0 @@ -#CreditCardInputTable td { - padding-bottom: 10px; -} - -#CreditCardInputTable #expiryMonthContainer { - float: left; - width: calc(50% - 10px); - padding-right: 10px; -} - -#CreditCardInputTable #expiryYearContainer { - float: left; - width: calc(50% - 10px); - padding-left: 10px; -} \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/OfferTile.css b/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/OfferTile.css index b73ce3a..59c3bea 100644 --- a/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/OfferTile.css +++ b/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/OfferTile.css @@ -5,7 +5,7 @@ color: #464443; background-color: rgb(244,244,244); padding: 10px 2px 10px 2px; - background-image: url('file:///C:\Users\raedjarr\Desktop\50x250\50x250\onedrive-logo.png'); + background-image: url('onedrive-logo.png'); } .TileContainer .Title { diff --git a/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/ProcessOrder.css b/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/ProcessOrder.css new file mode 100644 index 0000000..82b06bd --- /dev/null +++ b/Source/PartnerCenter.CustomerPortal/Content/Styles/Plugins/ProcessOrder.css @@ -0,0 +1,17 @@ +#ProcessOrderContainer { + margin: 15px; +} + +.confirmation-done-button { + margin-top: 20px; + display: inline-block; +} + +.confirmation-table { + min-width: 500px; +} + +.confirmation-total { + font-weight: 700; + font-size: 16px; +} diff --git a/Source/PartnerCenter.CustomerPortal/Controllers/AdminConsoleController.cs b/Source/PartnerCenter.CustomerPortal/Controllers/AdminConsoleController.cs index 6cf9c0e..0870da0 100644 --- a/Source/PartnerCenter.CustomerPortal/Controllers/AdminConsoleController.cs +++ b/Source/PartnerCenter.CustomerPortal/Controllers/AdminConsoleController.cs @@ -8,7 +8,6 @@ namespace Microsoft.Store.PartnerCenter.CustomerPortal.Controllers { using System; using System.Collections.Generic; - using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -87,7 +86,7 @@ public async Task UpdateBrandingConfiguration() // there is a new organization logo to be uploaded if (!organizationLogoPostedFile.ContentType.Trim().StartsWith("image/")) { - throw new PartnerDomainException(ErrorCode.InvalidFileType, "Provide an image file type for the organization logo").AddDetail("Field", "OrganizationLogoFile"); + throw new PartnerDomainException(ErrorCode.InvalidFileType, Resources.InvalidOrganizationLogoFileTypeMessage).AddDetail("Field", "OrganizationLogoFile"); } brandingConfiguration.OrganizationLogoContent = organizationLogoPostedFile.InputStream; @@ -101,7 +100,7 @@ public async Task UpdateBrandingConfiguration() } catch (UriFormatException invalidUri) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "Invalid organization logo URI uploaded", invalidUri).AddDetail("Field", "OrganizationLogo"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidOrganizationLogoUriMessage, invalidUri).AddDetail("Field", "OrganizationLogo"); } } @@ -113,7 +112,7 @@ public async Task UpdateBrandingConfiguration() // there is a new header image to be uploaded if (!headerImageUploadPostedFile.ContentType.Trim().StartsWith("image/")) { - throw new PartnerDomainException(ErrorCode.InvalidFileType, "Provide an image file type for the header image").AddDetail("Field", "HeaderImageFile"); + throw new PartnerDomainException(ErrorCode.InvalidFileType, Resources.InvalidHeaderImageMessage).AddDetail("Field", "HeaderImageFile"); } brandingConfiguration.HeaderImageContent = headerImageUploadPostedFile.InputStream; @@ -127,7 +126,7 @@ public async Task UpdateBrandingConfiguration() } catch (UriFormatException invalidUri) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "Invalid header image URI uploaded", invalidUri).AddDetail("Field", "HeaderImage"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidHeaderImageUriMessage, invalidUri).AddDetail("Field", "HeaderImage"); } } @@ -139,7 +138,7 @@ public async Task UpdateBrandingConfiguration() } catch (UriFormatException invalidUri) { - throw new PartnerDomainException(ErrorCode.InvalidInput, "Invalid privacy agreement URI uploaded", invalidUri).AddDetail("Field", "PrivacyAgreement"); + throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidPrivacyUriMessage, invalidUri).AddDetail("Field", "PrivacyAgreement"); } } @@ -225,7 +224,13 @@ public async Task GetPaymentConfiguration() public async Task UpdatePaymentConfiguration(PaymentConfiguration paymentConfiguration) { // validate the payment configuration before saving. - PayPalGateway.ValidateConfiguration(paymentConfiguration); + PayPalGateway.ValidateConfiguration(paymentConfiguration); + + // create a web experience profile using the branding for the web store. + BrandingConfiguration brandConfig = await ApplicationDomain.Instance.PortalBranding.RetrieveAsync(); + paymentConfiguration.WebExperienceProfileId = PayPalGateway.CreateWebExperienceProfile(paymentConfiguration, brandConfig, ApplicationDomain.Instance.PortalLocalization.CountryIso2Code); + + // Save the validated & complete payment configuration to repository. PaymentConfiguration paymentConfig = await ApplicationDomain.Instance.PaymentConfigurationRepository.UpdateAsync(paymentConfiguration); return paymentConfig; diff --git a/Source/PartnerCenter.CustomerPortal/Controllers/CustomerAccountController.cs b/Source/PartnerCenter.CustomerPortal/Controllers/CustomerAccountController.cs index cb40b8b..7a37295 100644 --- a/Source/PartnerCenter.CustomerPortal/Controllers/CustomerAccountController.cs +++ b/Source/PartnerCenter.CustomerPortal/Controllers/CustomerAccountController.cs @@ -80,8 +80,10 @@ from error in item.Errors } Customer newCustomer = null; - string domainName = string.Format(CultureInfo.InvariantCulture, "{0}.onmicrosoft.com", customerViewModel.DomainPrefix); + // TODO :: Loc. may need special handling for national clouds deployments. + string domainName = string.Format(CultureInfo.InvariantCulture, "{0}.onmicrosoft.com", customerViewModel.DomainPrefix); + // check domain available. bool isDomainTaken = await ApplicationDomain.Instance.PartnerCenterClient.Domains.ByDomain(domainName).ExistsAsync(); if (isDomainTaken) diff --git a/Source/PartnerCenter.CustomerPortal/Controllers/SubscriptionController.cs b/Source/PartnerCenter.CustomerPortal/Controllers/OrderController.cs similarity index 56% rename from Source/PartnerCenter.CustomerPortal/Controllers/SubscriptionController.cs rename to Source/PartnerCenter.CustomerPortal/Controllers/OrderController.cs index a091347..f05fc1d 100644 --- a/Source/PartnerCenter.CustomerPortal/Controllers/SubscriptionController.cs +++ b/Source/PartnerCenter.CustomerPortal/Controllers/OrderController.cs @@ -1,5 +1,5 @@ // ----------------------------------------------------------------------- -// +// // Copyright (c) Microsoft Corporation. All rights reserved. // // ----------------------------------------------------------------------- @@ -8,10 +8,8 @@ namespace Microsoft.Store.PartnerCenter.CustomerPortal.Controllers { using System; using System.Collections.Generic; - using System.Globalization; + using System.Globalization; using System.Linq; - using System.Net; - using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using BusinessLogic; @@ -21,184 +19,168 @@ namespace Microsoft.Store.PartnerCenter.CustomerPortal.Controllers using Filters; using Filters.WebApi; using Models; - using Newtonsoft.Json; + using Newtonsoft.Json; /// - /// Manages customer subscriptions. - /// - [RoutePrefix("api/Subscription")] - public class SubscriptionController : BaseController - { - /// - /// Retrieves a summary of all subscriptions and their respective order histories. - /// - /// The Subscription summary used by the client used for rendering purposes. - [HttpGet] - [@Authorize(UserRole = UserRole.Customer)] - [Route("summary")] - public async Task SubscriptionSummary() - { - return await this.GetSubscriptionSummary(this.Principal.PartnerCenterCustomerId); - } - - /// - /// Adds new subscriptions to an existing customer. - /// - /// Order Model which contains Subscription details and Credit Card to charge. - /// An HTTP response indicating the result of the addition. - [@Authorize(UserRole = UserRole.Customer)] - [HttpPost] - [Route("")] - public async Task AddSubscriptions([FromBody] OrderViewModel orderUpdateInformation) - { - if (!ModelState.IsValid) - { - var errorList = (from item in ModelState.Values - from error in item.Errors - select error.ErrorMessage).ToList(); - string errorMessage = JsonConvert.SerializeObject(errorList); - throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", errorMessage); - } - - await this.AddCustomerSubscription(this.Principal.PartnerCenterCustomerId, orderUpdateInformation); - } - + /// Manages customer orders. + /// + [RoutePrefix("api/Order")] + public class OrderController : BaseController + { /// /// Updates a customer's subscriptions. /// - /// A list of subscriptions to update. - /// The updated subscriptions. + /// A list of subscriptions to update. + /// The payment url from PayPal. [@Authorize(UserRole = UserRole.Customer)] [HttpPost] - [Route("AddSeats")] - public async Task UpdateSubscription([FromBody] OrderViewModel orderUpdateInformation) - { + [Route("Prepare")] + public async Task PrepareOrderForAuthenticatedCustomer([FromBody]OrderViewModel orderDetails) + { if (!ModelState.IsValid) { var errorList = (from item in ModelState.Values from error in item.Errors select error.ErrorMessage).ToList(); string errorMessage = JsonConvert.SerializeObject(errorList); - throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", errorMessage); + throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", errorMessage); } - string clientCustomerId = this.Principal.PartnerCenterCustomerId; + orderDetails.CustomerId = this.Principal.PartnerCenterCustomerId; + string operationDescription = string.Empty; - orderUpdateInformation.Subscriptions.AssertNotNull(nameof(orderUpdateInformation.Subscriptions)); - List orderSubscriptions = orderUpdateInformation.Subscriptions.ToList(); - if (!(orderSubscriptions.Count == 1)) + // Validate & Normalize the order information. + OrderNormalizer orderNormalizer = new OrderNormalizer(ApplicationDomain.Instance, orderDetails); + switch (orderDetails.OperationType) { - throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", Resources.MoreThanOneSubscriptionUpdateErrorMessage); + case CommerceOperationType.AdditionalSeatsPurchase: + operationDescription = Resources.AddSeatsOperationCaption; + orderDetails = await orderNormalizer.NormalizePurchaseAdditionalSeatsOrderAsync(); + break; + case CommerceOperationType.NewPurchase: + operationDescription = Resources.NewPurchaseOperationCaption; + orderDetails = await orderNormalizer.NormalizePurchaseSubscriptionOrderAsync(); + break; + case CommerceOperationType.Renewal: + operationDescription = Resources.RenewOperationCaption; + orderDetails = await orderNormalizer.NormalizeRenewSubscriptionOrderAsync(); + break; } + + // prepare the redirect url so that client can redirect to PayPal. + string redirectUrl = string.Format(CultureInfo.InvariantCulture, "{0}/#ProcessOrder?ret=true", Request.RequestUri.GetLeftPart(UriPartial.Authority)); - string subscriptionId = orderSubscriptions.First().SubscriptionId; - int additionalSeats = orderSubscriptions.First().Quantity; - - subscriptionId.AssertNotEmpty(nameof(subscriptionId)); // required for commerce operation. - additionalSeats.AssertPositive(nameof(additionalSeats)); // required & should be greater than 0. - - string operationDescription = string.Format("Customer Id:[{0}] - Added to subscription:{1}.", clientCustomerId, subscriptionId); - PayPalGateway paymentGateway = new PayPalGateway(ApplicationDomain.Instance, orderUpdateInformation.CreditCard, operationDescription); - CommerceOperations commerceOperation = new CommerceOperations(ApplicationDomain.Instance, clientCustomerId, paymentGateway); - - return await commerceOperation.PurchaseAdditionalSeatsAsync(subscriptionId, additionalSeats); + // execute to paypal and get paypal action URI. + PayPalGateway paymentGateway = new PayPalGateway(ApplicationDomain.Instance, operationDescription); + return await paymentGateway.GeneratePaymentUriAsync(redirectUrl, orderDetails); } /// - /// Renew a customer's subscriptions. + /// Processes the order raised for an authenticated customer. /// - /// A list of subscriptions to update. - /// The updated subscriptions. + /// Payment Id. + /// Payer Id. + /// Commerce transaction result. [@Authorize(UserRole = UserRole.Customer)] - [HttpPost] - [Route("Renew")] - public async Task RenewSubscription([FromBody] OrderViewModel renewalOrderInformation) + [HttpGet] + [Route("Process")] + public async Task ProcessOrderForAuthenticatedCustomer(string paymentId, string payerId) { - if (!ModelState.IsValid) - { - var errorList = (from item in ModelState.Values - from error in item.Errors - select error.ErrorMessage).ToList(); - string errorMessage = JsonConvert.SerializeObject(errorList); - throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", errorMessage); - } - + // extract order information and create paypal payload. string clientCustomerId = this.Principal.PartnerCenterCustomerId; - renewalOrderInformation.Subscriptions.AssertNotNull(nameof(renewalOrderInformation.Subscriptions)); - List orderSubscriptions = renewalOrderInformation.Subscriptions.ToList(); - if (!(orderSubscriptions.Count == 1)) - { - throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", Resources.MoreThanOneSubscriptionUpdateErrorMessage); - } - - string subscriptionId = orderSubscriptions.First().SubscriptionId; - subscriptionId.AssertNotEmpty(nameof(subscriptionId)); // is Required for the commerce operation. + paymentId.AssertNotEmpty(nameof(paymentId)); + payerId.AssertNotEmpty(nameof(payerId)); + PayPalGateway paymentGateway = new PayPalGateway(ApplicationDomain.Instance, payerId, paymentId); - string operationDescription = string.Format("Customer Id:[{0}] - Renewed subscription:{1}.", clientCustomerId, subscriptionId); - PayPalGateway paymentGateway = new PayPalGateway(ApplicationDomain.Instance, renewalOrderInformation.CreditCard, operationDescription); + // use payment gateway to extract order information. + OrderViewModel orderToProcess = await paymentGateway.GetOrderDetailsFromPaymentAsync(); CommerceOperations commerceOperation = new CommerceOperations(ApplicationDomain.Instance, clientCustomerId, paymentGateway); + + TransactionResult transactionResult = null; + switch (orderToProcess.OperationType) + { + case CommerceOperationType.Renewal: + transactionResult = await commerceOperation.RenewSubscriptionAsync(orderToProcess); + break; + case CommerceOperationType.AdditionalSeatsPurchase: + transactionResult = await commerceOperation.PurchaseAdditionalSeatsAsync(orderToProcess); + break; + case CommerceOperationType.NewPurchase: + transactionResult = await commerceOperation.PurchaseAsync(orderToProcess); + break; + } - return await commerceOperation.RenewSubscriptionAsync(subscriptionId); + return await Task.FromResult(transactionResult); } /// - /// Adds new subscriptions to a newly registered customer. + /// Prepare an order for an unauthenticated customer. Supports only purchase of new subscriptions. /// - /// The customer's subscriptions, credit card and Customer id. - /// A registration confirmation. - [HttpPost] - [Route("RegistrationOrder")] + /// A list of subscriptions to update. + /// The payment url from PayPal. [@Authorize(UserRole = UserRole.None)] - public async Task RegisterAddSubscriptions([FromBody] OrderViewModel orderRegistrationInformation) - { + [HttpPost] + [Route("NewCustomerPrepareOrder")] + public async Task PrepareOrderForUnAuthenticatedCustomer([FromBody]OrderViewModel orderDetails) + { if (!ModelState.IsValid) { var errorList = (from item in ModelState.Values from error in item.Errors select error.ErrorMessage).ToList(); string errorMessage = JsonConvert.SerializeObject(errorList); - throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", errorMessage); - } - - string clientCustomerId = orderRegistrationInformation.CustomerId; - if (string.IsNullOrWhiteSpace(clientCustomerId)) - { - string errorMessage = Resources.InvalidCustomerIdErrorMessage; - throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", errorMessage); + throw new PartnerDomainException(ErrorCode.InvalidInput).AddDetail("ErrorMessage", errorMessage); } - await this.AddCustomerSubscription(clientCustomerId, orderRegistrationInformation); - - return await this.GetSubscriptionSummary(clientCustomerId); + // Validate & Normalize the order information. + OrderNormalizer orderNormalizer = new OrderNormalizer(ApplicationDomain.Instance, orderDetails); + orderDetails = await orderNormalizer.NormalizePurchaseSubscriptionOrderAsync(); + + // prepare the redirect url so that client can redirect to PayPal. + string redirectUrl = string.Format(CultureInfo.InvariantCulture, "{0}/#ProcessOrder?ret=true&customerId={1}", Request.RequestUri.GetLeftPart(UriPartial.Authority), orderDetails.CustomerId); + + // execute to paypal and get paypal action URI. + PayPalGateway paymentGateway = new PayPalGateway(ApplicationDomain.Instance, Resources.NewPurchaseOperationCaption); + return await paymentGateway.GeneratePaymentUriAsync(redirectUrl, orderDetails); } /// - /// Adds a portal subscription for the Customer. + /// Processes the order raised for an unauthenticated customer. /// - /// The Customer Id. - /// The Order information containing credit card and list of subscriptions to add. - /// Transaction Result from commerce operation. - private async Task AddCustomerSubscription(string clientCustomerId, OrderViewModel orderSubscriptions) - { - string operationDescription = string.Format("Customer Id:[{0}] - Added Subscription(s).", clientCustomerId); - - PayPalGateway paymentGateway = new PayPalGateway(ApplicationDomain.Instance, orderSubscriptions.CreditCard, operationDescription); - CommerceOperations commerceOperation = new CommerceOperations(ApplicationDomain.Instance, clientCustomerId, paymentGateway); + /// Customer Id generated by register customer call. + /// Payment Id. + /// Payer Id. + /// Subscription Summary. + [@Authorize(UserRole = UserRole.None)] + [HttpGet] + [Route("NewCustomerProcessOrder")] + public async Task ProcessOrderForUnAuthenticatedCustomer(string customerId, string paymentId, string payerId) + { + customerId.AssertNotEmpty(nameof(customerId)); - List orderItems = new List(); - foreach (var orderItem in orderSubscriptions.Subscriptions) - { - string offerId = orderItem.OfferId; - int quantity = orderItem.Quantity; + paymentId.AssertNotEmpty(nameof(paymentId)); + payerId.AssertNotEmpty(nameof(payerId)); + PayPalGateway paymentGateway = new PayPalGateway(ApplicationDomain.Instance, payerId, paymentId); - offerId.AssertNotEmpty(nameof(offerId)); // required for commerce operation. - quantity.AssertPositive(nameof(quantity)); // required & should be greater than 0. + // use payment gateway to extract order information. + OrderViewModel orderToProcess = await paymentGateway.GetOrderDetailsFromPaymentAsync(); + CommerceOperations commerceOperation = new CommerceOperations(ApplicationDomain.Instance, customerId, paymentGateway); + await commerceOperation.PurchaseAsync(orderToProcess); - orderItems.Add(new PurchaseLineItem(offerId, quantity)); - } - - return await commerceOperation.PurchaseAsync(orderItems); + return await this.GetSubscriptionSummaryAsync(customerId); + } + + /// + /// Retrieves a summary of all subscriptions and their respective order histories. + /// + /// The Subscription summary used by the client used for rendering purposes. + [HttpGet] + [@Authorize(UserRole = UserRole.Customer)] + [Route("summary")] + public async Task SubscriptionSummary() + { + return await this.GetSubscriptionSummaryAsync(this.Principal.PartnerCenterCustomerId); } /// @@ -206,25 +188,29 @@ private async Task AddCustomerSubscription(string clientCusto /// /// The customer Id. /// Subscription Summary. - private async Task GetSubscriptionSummary(string customerId) - { + private async Task GetSubscriptionSummaryAsync(string customerId) + { var customerSubscriptionsTask = ApplicationDomain.Instance.CustomerSubscriptionsRepository.RetrieveAsync(customerId); var customerSubscriptionsHistoryTask = ApplicationDomain.Instance.CustomerPurchasesRepository.RetrieveAsync(customerId); var allPartnerOffersTask = ApplicationDomain.Instance.OffersRepository.RetrieveAsync(); - await Task.WhenAll(customerSubscriptionsTask, customerSubscriptionsHistoryTask, allPartnerOffersTask); + var currentMicrosoftOffersTask = ApplicationDomain.Instance.OffersRepository.RetrieveMicrosoftOffersAsync(); + await Task.WhenAll(customerSubscriptionsTask, customerSubscriptionsHistoryTask, allPartnerOffersTask, currentMicrosoftOffersTask); - var customerSubscriptionsHistory = customerSubscriptionsHistoryTask.Result; + var customerSubscriptionsHistory = customerSubscriptionsHistoryTask.Result; // retrieve all the partner offers to match against them IEnumerable allPartnerOffers = allPartnerOffersTask.Result; + // retrive all the microsoft offers to match against. + IEnumerable currentMicrosoftOffers = currentMicrosoftOffersTask.Result; + // start building the summary. decimal summaryTotal = 0; // format all responses to client using portal locale. CultureInfo responseCulture = new CultureInfo(ApplicationDomain.Instance.PortalLocalization.Locale); List customerSubscriptionsView = new List(); - + // iterate through and build the list of customer's subscriptions. foreach (var subscription in customerSubscriptionsTask.Result) { @@ -240,7 +226,7 @@ private async Task GetSubscriptionSummary(string customerI // iterate through and build the SubsriptionHistory for this subscription. foreach (var historyItem in subscriptionHistoryList) { - decimal orderTotal = Math.Round(historyItem.SeatPrice * historyItem.SeatsBought, 2); + decimal orderTotal = Math.Round(historyItem.SeatPrice * historyItem.SeatsBought, 2); historyItems.Add(new SubscriptionHistory() { OrderTotal = orderTotal.ToString("C", responseCulture), @@ -265,13 +251,22 @@ private async Task GetSubscriptionSummary(string customerI DateTime subscriptionExpiryDate = subscription.ExpiryDate.ToUniversalTime(); int remainingDays = (subscriptionExpiryDate.Date - DateTime.UtcNow.Date).Days; bool isRenewable = remainingDays <= 30; - bool isEditable = DateTime.UtcNow.Date <= subscriptionExpiryDate.Date; - + bool isEditable = DateTime.UtcNow.Date <= subscriptionExpiryDate.Date; + + // TODO :: Handle Microsoft offer being pulled back due to EOL. + + // Temporarily mark this partnerOffer item as inactive and dont allow store front customer to manage this subscription. + var alignedMicrosoftOffer = currentMicrosoftOffers.Where(offer => offer.Offer.Id == partnerOfferItem.MicrosoftOfferId).FirstOrDefault(); + if (alignedMicrosoftOffer == null) + { + partnerOfferItem.IsInactive = true; + } + if (partnerOfferItem.IsInactive) { // in case the offer is inactive (marked for deletion) then dont allow renewals or editing on this subscription tied to this offer. isRenewable = false; - isEditable = false; + isEditable = false; } // Compute the pro rated price per seat for this subcription & return for client side processing during updates. @@ -279,9 +274,9 @@ private async Task GetSubscriptionSummary(string customerI SubscriptionViewModel subscriptionItem = new SubscriptionViewModel() { - SubscriptionId = subscription.SubscriptionId, + SubscriptionId = subscription.SubscriptionId, FriendlyName = subscriptionTitle, - PortalOfferId = portalOfferId, + PortalOfferId = portalOfferId, PortalOfferPrice = portalOfferPrice.ToString("C", responseCulture), IsRenewable = isRenewable, // IsRenewable is true if subscription is going to expire in 30 days. IsEditable = isEditable, // IsEditable is true if today is lesser or equal to subscription expiry date. @@ -289,7 +284,7 @@ private async Task GetSubscriptionSummary(string customerI SubscriptionTotal = subscriptionTotal.ToString("C", responseCulture), // Currency format. SubscriptionExpiryDate = subscriptionExpiryDate.Date.ToString("d", responseCulture), // Short date format. SubscriptionOrderHistory = historyItems, - SubscriptionProRatedPrice = proratedPerSeatPrice + SubscriptionProRatedPrice = proratedPerSeatPrice }; // add this subcription to the customer's subscription list. @@ -313,7 +308,7 @@ private async Task GetSubscriptionSummary(string customerI /// The Commerce operation type. /// Localized Operation Type string. private string GetOperationType(CommerceOperationType operationType) - { + { switch (operationType) { case CommerceOperationType.AdditionalSeatsPurchase: diff --git a/Source/PartnerCenter.CustomerPortal/Controllers/PartnerOfferController.cs b/Source/PartnerCenter.CustomerPortal/Controllers/PartnerOfferController.cs index 51a9873..af90ae9 100644 --- a/Source/PartnerCenter.CustomerPortal/Controllers/PartnerOfferController.cs +++ b/Source/PartnerCenter.CustomerPortal/Controllers/PartnerOfferController.cs @@ -47,10 +47,22 @@ public async Task GetOffersCatalog() foreach (var offer in partnerOffers) { - offer.Thumbnail = microsoftOffers.Where(msOffer => msOffer.Offer.Id == offer.MicrosoftOfferId).First().ThumbnailUri; + // TODO :: Handle Microsoft offer being pulled back due to EOL. + var microsoftOfferItem = microsoftOffers.Where(msOffer => msOffer.Offer.Id == offer.MicrosoftOfferId).FirstOrDefault(); + + // temporarily remove the partner offer from catalog display if the corresponding Microsoft offer does not exist. + if (microsoftOfferItem != null) + { + offer.Thumbnail = microsoftOfferItem.ThumbnailUri; + } + else + { + // temporary fix - remove the items from the collection by marking it as Inactive. + offer.IsInactive = true; + } } - offerCatalogViewModel.Offers = partnerOffers; + offerCatalogViewModel.Offers = partnerOffers.Where(offer => offer.IsInactive == false); } return offerCatalogViewModel; diff --git a/Source/PartnerCenter.CustomerPortal/Controllers/TemplateController.cs b/Source/PartnerCenter.CustomerPortal/Controllers/TemplateController.cs index 084eb3d..8ea91e8 100644 --- a/Source/PartnerCenter.CustomerPortal/Controllers/TemplateController.cs +++ b/Source/PartnerCenter.CustomerPortal/Controllers/TemplateController.cs @@ -82,6 +82,17 @@ public ActionResult RegistrationConfirmation() return this.PartialView(); } + /// + /// Serves the HTML template for the process order presenter. + /// + /// The HTML template for the process order presenter. + [HttpGet] + [OutputCache(NoStore = true, Duration = 0)] + public ActionResult ProcessOrder() + { + return this.PartialView(); + } + /// /// Serves the HTML template for the customer account presenter. /// diff --git a/Source/PartnerCenter.CustomerPortal/Extensions.cs b/Source/PartnerCenter.CustomerPortal/Extensions.cs index 9ac98ee..3218e69 100644 --- a/Source/PartnerCenter.CustomerPortal/Extensions.cs +++ b/Source/PartnerCenter.CustomerPortal/Extensions.cs @@ -41,7 +41,7 @@ public static void AssertNotEmpty(this string nonEmptyString, string caption) { if (string.IsNullOrWhiteSpace(nonEmptyString)) { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "{0} is not set", caption ?? "string")); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Resources.AssertStringNotEmptyInvalidError, caption ?? Resources.AssertStringNotEmptyInvalidPrefix)); } } @@ -60,7 +60,7 @@ public static void AssertPhoneNumber(this string phoneNumber, string caption) if (!regexPhoneNumber.IsMatch(phoneNumber)) { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "{0} is invalid.", caption ?? "string")); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Resources.AssertPhoneNumberInvalidError, caption ?? Resources.AssertPhoneNumberInvalidPrefix)); } } @@ -73,7 +73,7 @@ public static void AssertPositive(this int number, string caption) { if (number <= 0) { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "{0} must be greater than zero", caption ?? "number")); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Resources.AssertNumberPositiveInvalidError, caption ?? Resources.AssertNumberPositiveInvalidPrefix)); } } @@ -86,7 +86,7 @@ public static void AssertPositive(this decimal number, string caption) { if (number <= 0) { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "{0} must be greater than zero", caption ?? "number")); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Resources.AssertNumberPositiveInvalidError, caption ?? Resources.AssertNumberPositiveInvalidPrefix)); } } diff --git a/Source/PartnerCenter.CustomerPortal/Filters/WebApi/ErrorHandler.cs b/Source/PartnerCenter.CustomerPortal/Filters/WebApi/ErrorHandler.cs index d95f91c..cd1fb74 100644 --- a/Source/PartnerCenter.CustomerPortal/Filters/WebApi/ErrorHandler.cs +++ b/Source/PartnerCenter.CustomerPortal/Filters/WebApi/ErrorHandler.cs @@ -49,9 +49,6 @@ public override void OnException(HttpActionExecutedContext context) case ErrorCode.DomainNotAvailable: case ErrorCode.MaximumRequestSizeExceeded: case ErrorCode.AlreadyExists: - case ErrorCode.CardCVNCheckFailed: - case ErrorCode.CardExpired: - case ErrorCode.CardRefused: case ErrorCode.PaymentGatewayPaymentError: case ErrorCode.PaymentGatewayIdentityFailureDuringConfiguration: // treat this as a retryable bad input. errorResponseCode = HttpStatusCode.BadRequest; diff --git a/Source/PartnerCenter.CustomerPortal/Models/OrderSubscriptionItemViewModel.cs b/Source/PartnerCenter.CustomerPortal/Models/OrderSubscriptionItemViewModel.cs index d3cb684..28152a0 100644 --- a/Source/PartnerCenter.CustomerPortal/Models/OrderSubscriptionItemViewModel.cs +++ b/Source/PartnerCenter.CustomerPortal/Models/OrderSubscriptionItemViewModel.cs @@ -6,6 +6,7 @@ namespace Microsoft.Store.PartnerCenter.CustomerPortal.Models { + using System; using System.ComponentModel.DataAnnotations; /// @@ -19,13 +20,33 @@ public class OrderSubscriptionItemViewModel public string OfferId { get; set; } /// - /// Gets or sets the subscription Id (used primarily during update calls). + /// Gets or sets the subscription Id. /// public string SubscriptionId { get; set; } + /// + /// Gets or sets the partner offer Id. + /// + public string PartnerOfferId { get; set; } + + /// + /// Gets or sets the subscription name of the offer being ordered. + /// + public string SubscriptionName { get; set; } + + /// + /// Gets or sets the subscription expiry date of the offer being ordered. + /// + public DateTime SubscriptionExpiryDate { get; set; } + /// /// Gets or sets the quantity of the offer being ordered. /// public int Quantity { get; set; } + + /// + /// Gets or sets the price of the offer being ordered. + /// + public decimal SeatPrice { get; set; } } } \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Models/OrderViewModel.cs b/Source/PartnerCenter.CustomerPortal/Models/OrderViewModel.cs index 34cf953..5e3ef21 100644 --- a/Source/PartnerCenter.CustomerPortal/Models/OrderViewModel.cs +++ b/Source/PartnerCenter.CustomerPortal/Models/OrderViewModel.cs @@ -20,15 +20,15 @@ public class OrderViewModel [Required] public IEnumerable Subscriptions { get; set; } - /// - /// Gets or sets the Credit Card for this order. - /// - [Required] - public PaymentCard CreditCard { get; set; } - /// /// Gets or sets the Customer Id. /// public string CustomerId { get; set; } + + /// + /// Gets or sets the Operation Type for the order. + /// + [Required] + public CommerceOperationType OperationType { get; set; } } } \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Models/PaymentCard.cs b/Source/PartnerCenter.CustomerPortal/Models/PaymentCard.cs deleted file mode 100644 index 2341e9e..0000000 --- a/Source/PartnerCenter.CustomerPortal/Models/PaymentCard.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// ----------------------------------------------------------------------- - -namespace Microsoft.Store.PartnerCenter.CustomerPortal.Models -{ - using System.ComponentModel.DataAnnotations; - using Validators; - - /// - /// The Credit Card payment instrument view model. - /// - public class PaymentCard - { - /// - /// Gets or sets the customer's credit card type. - /// - [Required] - public string CreditCardType { get; set; } - - /// - /// Gets or sets the customer's credit card holder first name. - /// - [Required] - public string CardHolderFirstName { get; set; } - - /// - /// Gets or sets the customer's credit card holder last name. - /// - [Required] - public string CardHolderLastName { get; set; } - - /// - /// Gets or sets the customer's credit card number. - /// - [Required] - [CreditCard] - public string CreditCardNumber { get; set; } - - /// - /// Gets or sets the customer's credit card expiry month. - /// - [Required] - [Range(1, 12)] - public int CreditCardExpiryMonth { get; set; } - - /// - /// Gets or sets the customer's credit card expiry year. - /// - [ExpiryDateInTenYears] - public int CreditCardExpiryYear { get; set; } - - /// - /// Gets or sets the customer's credit card CVN. - /// Minimum 3 (4 in case of AMEX). - /// - [MinLength(3)] - [MaxLength(4)] - public string CreditCardCvn { get; set; } - } -} \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Models/PaymentConfiguration.cs b/Source/PartnerCenter.CustomerPortal/Models/PaymentConfiguration.cs index 0ce2069..6677df3 100644 --- a/Source/PartnerCenter.CustomerPortal/Models/PaymentConfiguration.cs +++ b/Source/PartnerCenter.CustomerPortal/Models/PaymentConfiguration.cs @@ -25,5 +25,10 @@ public class PaymentConfiguration /// Gets or sets the payment account type. /// public string AccountType { get; set; } + + /// + /// Gets or sets the Web Experience Profile Id. + /// + public string WebExperienceProfileId { get; set; } } } \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Models/TransactionResult.cs b/Source/PartnerCenter.CustomerPortal/Models/TransactionResult.cs index 21f2e5b..0138ed0 100644 --- a/Source/PartnerCenter.CustomerPortal/Models/TransactionResult.cs +++ b/Source/PartnerCenter.CustomerPortal/Models/TransactionResult.cs @@ -17,11 +17,10 @@ public class TransactionResult { /// /// Initializes a new instance of the class. - /// - /// The total amount charged for the transaction. + /// /// A collection of line items bundled in the transaction. /// The time at which the transaction took place. - public TransactionResult(decimal amountCharged, IEnumerable lineItems, DateTime timeStamp) + public TransactionResult(IEnumerable lineItems, DateTime timeStamp) { // we don't validate amount charged since a transaction may result in a negative amount if (lineItems == null || lineItems.Count() <= 0) @@ -33,17 +32,11 @@ public TransactionResult(decimal amountCharged, IEnumerable - /// Gets the total amount charged for the transaction. - /// - public decimal AmountCharged { get; private set; } - /// /// Gets the result line items associated with the transaction. /// diff --git a/Source/PartnerCenter.CustomerPortal/PartnerCenter.CustomerPortal.csproj b/Source/PartnerCenter.CustomerPortal/PartnerCenter.CustomerPortal.csproj index eb298bd..e760267 100644 --- a/Source/PartnerCenter.CustomerPortal/PartnerCenter.CustomerPortal.csproj +++ b/Source/PartnerCenter.CustomerPortal/PartnerCenter.CustomerPortal.csproj @@ -91,28 +91,28 @@ ..\..\packages\Microsoft.Owin.Security.OpenIdConnect.3.0.1\lib\net45\Microsoft.Owin.Security.OpenIdConnect.dll True - - ..\..\packages\Microsoft.Store.PartnerCenter.1.1.0.1\lib\net45\Microsoft.Store.PartnerCenter.dll + + ..\..\packages\Microsoft.Store.PartnerCenter.1.2.0.1\lib\net45\Microsoft.Store.PartnerCenter.dll True - - ..\..\packages\Microsoft.Store.PartnerCenter.1.1.0.1\lib\net45\Microsoft.Store.PartnerCenter.Extensions.dll + + ..\..\packages\Microsoft.Store.PartnerCenter.1.2.0.1\lib\net45\Microsoft.Store.PartnerCenter.Extensions.dll True - - ..\..\packages\Microsoft.Store.PartnerCenter.1.1.0.1\lib\net45\Microsoft.Store.PartnerCenter.Models.dll + + ..\..\packages\Microsoft.Store.PartnerCenter.1.2.0.1\lib\net45\Microsoft.Store.PartnerCenter.Models.dll True - - ..\..\packages\WindowsAzure.Storage.7.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + + ..\..\packages\WindowsAzure.Storage.7.2.1\lib\net40\Microsoft.WindowsAzure.Storage.dll True - - ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll True - - ..\..\packages\PayPal.1.7.3\lib\net451\PayPal.dll + + ..\..\packages\PayPal.1.7.4\lib\net451\PayPal.dll True @@ -128,6 +128,10 @@ ..\..\packages\System.IdentityModel.Tokens.Jwt.4.0.2.206221351\lib\net45\System.IdentityModel.Tokens.Jwt.dll True + + ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll + True + ..\..\packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll @@ -140,6 +144,14 @@ + + ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll + True + + + ..\..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll + True + @@ -164,10 +176,6 @@ ..\..\packages\Microsoft.AspNet.Web.Optimization.1.1.3\lib\net40\System.Web.Optimization.dll - - True - ..\..\packages\Microsoft.AspNet.Razor.3.2.2\lib\net45\System.Web.Razor.dll - True ..\..\packages\Microsoft.AspNet.WebPages.3.2.2\lib\net45\System.Web.WebPages.dll @@ -191,12 +199,6 @@ - - ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll - - - ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll - ..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.2\lib\net45\System.Web.Http.WebHost.dll @@ -246,7 +248,9 @@ + + @@ -312,7 +316,6 @@ - Global.asax @@ -321,7 +324,6 @@ - @@ -404,8 +406,8 @@ - + @@ -439,14 +441,15 @@ + + - @@ -511,14 +514,11 @@ Web.config - Designer Web.config - - Designer - + @@ -560,11 +560,11 @@ - + diff --git a/Source/PartnerCenter.CustomerPortal/Resources.Designer.cs b/Source/PartnerCenter.CustomerPortal/Resources.Designer.cs index 493fc2b..1016931 100644 --- a/Source/PartnerCenter.CustomerPortal/Resources.Designer.cs +++ b/Source/PartnerCenter.CustomerPortal/Resources.Designer.cs @@ -60,15 +60,6 @@ internal Resources() { } } - /// - /// Looks up a localized string similar to About. - /// - public static string About { - get { - return ResourceManager.GetString("About", resourceCulture); - } - } - /// /// Looks up a localized string similar to I accept the terms and conditions. /// @@ -96,33 +87,6 @@ public static string AccessDeniedMessage { } } - /// - /// Looks up a localized string similar to - account. - /// - public static string AccountSuffix { - get { - return ResourceManager.GetString("AccountSuffix", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Active. - /// - public static string ActiveStatus { - get { - return ResourceManager.GetString("ActiveStatus", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add. - /// - public static string AddCapital { - get { - return ResourceManager.GetString("AddCapital", resourceCulture); - } - } - /// /// Looks up a localized string similar to Add. /// @@ -133,20 +97,11 @@ public static string AddCaption { } /// - /// Looks up a localized string similar to Adding subcriptions.... - /// - public static string AddingSubscriptionMessage { - get { - return ResourceManager.GetString("AddingSubscriptionMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to add. + /// Looks up a localized string similar to Add more licenses. /// - public static string AddLink { + public static string AddMoreLicensesCaption { get { - return ResourceManager.GetString("AddLink", resourceCulture); + return ResourceManager.GetString("AddMoreLicensesCaption", resourceCulture); } } @@ -178,20 +133,20 @@ public static string AddOfferFeatureTooltip { } /// - /// Looks up a localized string similar to Add plan. + /// Looks up a localized string similar to Address. /// - public static string AddPlan { + public static string Address { get { - return ResourceManager.GetString("AddPlan", resourceCulture); + return ResourceManager.GetString("Address", resourceCulture); } } /// - /// Looks up a localized string similar to Address. + /// Looks up a localized string similar to Adding seats to subscription.. /// - public static string Address { + public static string AddSeatsOperationCaption { get { - return ResourceManager.GetString("Address", resourceCulture); + return ResourceManager.GetString("AddSeatsOperationCaption", resourceCulture); } } @@ -231,6 +186,15 @@ public static string AdminUserAccount { } } + /// + /// Looks up a localized string similar to {0} AD tenant is already registered to Partner Center customer: {1}. + /// + public static string ADTenantIsAlreadyRegisteredToCustomer { + get { + return ResourceManager.GetString("ADTenantIsAlreadyRegisteredToCustomer", resourceCulture); + } + } + /// /// Looks up a localized string similar to Annual price. /// @@ -249,6 +213,60 @@ public static string AnnualTotalCaption { } } + /// + /// Looks up a localized string similar to {0} must be greater than zero. + /// + public static string AssertNumberPositiveInvalidError { + get { + return ResourceManager.GetString("AssertNumberPositiveInvalidError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to number. + /// + public static string AssertNumberPositiveInvalidPrefix { + get { + return ResourceManager.GetString("AssertNumberPositiveInvalidPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is invalid.. + /// + public static string AssertPhoneNumberInvalidError { + get { + return ResourceManager.GetString("AssertPhoneNumberInvalidError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to phone number. + /// + public static string AssertPhoneNumberInvalidPrefix { + get { + return ResourceManager.GetString("AssertPhoneNumberInvalidPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is not set. + /// + public static string AssertStringNotEmptyInvalidError { + get { + return ResourceManager.GetString("AssertStringNotEmptyInvalidError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to string. + /// + public static string AssertStringNotEmptyInvalidPrefix { + get { + return ResourceManager.GetString("AssertStringNotEmptyInvalidPrefix", resourceCulture); + } + } + /// /// Looks up a localized string similar to Back. /// @@ -384,33 +402,6 @@ public static string CannotAddExistingSubscriptionError { } } - /// - /// Looks up a localized string similar to The card verfiication number check failed. Please enter a valid CVN.. - /// - public static string CardCVNFailedError { - get { - return ResourceManager.GetString("CardCVNFailedError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The card has expired.. - /// - public static string CardExpiredError { - get { - return ResourceManager.GetString("CardExpiredError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The card was refused.. - /// - public static string CardRefusedError { - get { - return ResourceManager.GetString("CardRefusedError", resourceCulture); - } - } - /// /// Looks up a localized string similar to Category. /// @@ -474,24 +465,6 @@ public static string CompanyBrandingConfigurationCaption { } } - /// - /// Looks up a localized string similar to Company information. - /// - public static string CompanyInformation { - get { - return ResourceManager.GetString("CompanyInformation", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Company name. - /// - public static string CompanyName { - get { - return ResourceManager.GetString("CompanyName", resourceCulture); - } - } - /// /// Looks up a localized string similar to Configure your website. /// @@ -511,7 +484,7 @@ public static string Confirmation { } /// - /// Looks up a localized string similar to Thank you! We have received your order. It may take a few minutes to process.. + /// Looks up a localized string similar to Thank you! Customer information has been setup. Please review this page and click submit to place your order.. /// public static string ConfirmationMessage { get { @@ -546,15 +519,6 @@ public static string ContactSalesHeader { } } - /// - /// Looks up a localized string similar to Shown in the Contact us section of the header. - /// - public static string ContactSalesSubText { - get { - return ResourceManager.GetString("ContactSalesSubText", resourceCulture); - } - } - /// /// Looks up a localized string similar to Contact us. /// @@ -592,11 +556,11 @@ public static string ContactUsPhoneCaption { } /// - /// Looks up a localized string similar to Shown in the Contact us section of the header. + /// Looks up a localized string similar to ContactUs section not found in portal branding configuration. /// - public static string ContactUsSubText { + public static string ContactUsSectionNotFound { get { - return ResourceManager.GetString("ContactUsSubText", resourceCulture); + return ResourceManager.GetString("ContactUsSectionNotFound", resourceCulture); } } @@ -654,123 +618,6 @@ public static string CouldNotRetrieveSubscriptionsSummary { } } - /// - /// Looks up a localized string similar to Amex. - /// - public static string CreditCardAmexCaption { - get { - return ResourceManager.GetString("CreditCardAmexCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Verification numer (CVN). - /// - public static string CreditCardCVNCaption { - get { - return ResourceManager.GetString("CreditCardCVNCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Discover. - /// - public static string CreditCardDiscoverCaption { - get { - return ResourceManager.GetString("CreditCardDiscoverCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Expriration date. - /// - public static string CreditCardExpirationDateCaption { - get { - return ResourceManager.GetString("CreditCardExpirationDateCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to First name on card. - /// - public static string CreditCardFirstNameCaption { - get { - return ResourceManager.GetString("CreditCardFirstNameCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Credit Card Information. - /// - public static string CreditCardInfoCaption { - get { - return ResourceManager.GetString("CreditCardInfoCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Last name on card. - /// - public static string CreditCardLastNameCaption { - get { - return ResourceManager.GetString("CreditCardLastNameCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Master Card. - /// - public static string CreditCardMasterCaption { - get { - return ResourceManager.GetString("CreditCardMasterCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not supported. - /// - public static string CreditCardNotSupportedCaption { - get { - return ResourceManager.GetString("CreditCardNotSupportedCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Card number. - /// - public static string CreditCardNumberCaption { - get { - return ResourceManager.GetString("CreditCardNumberCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Credit card type. - /// - public static string CreditCardTypeCaption { - get { - return ResourceManager.GetString("CreditCardTypeCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Visa. - /// - public static string CreditCardVisaCaption { - get { - return ResourceManager.GetString("CreditCardVisaCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Creating order for your registration.... - /// - public static string CustomerOrderRegistrationMessage { - get { - return ResourceManager.GetString("CustomerOrderRegistrationMessage", resourceCulture); - } - } - /// /// Looks up a localized string similar to Organization name. /// @@ -834,15 +681,6 @@ public static string CustomerProfileCompanyInfoSectionCaption { } } - /// - /// Looks up a localized string similar to Company Name. - /// - public static string CustomerProfileCompanyNameCaption { - get { - return ResourceManager.GetString("CustomerProfileCompanyNameCaption", resourceCulture); - } - } - /// /// Looks up a localized string similar to Contact Information. /// @@ -915,15 +753,6 @@ public static string CustomerProfileOffice365EmailCaption { } } - /// - /// Looks up a localized string similar to Office 365. - /// - public static string CustomerProfileOffice365SectionCaption { - get { - return ResourceManager.GetString("CustomerProfileOffice365SectionCaption", resourceCulture); - } - } - /// /// Looks up a localized string similar to Username. /// @@ -933,33 +762,6 @@ public static string CustomerProfileOffice365UserNameCaption { } } - /// - /// Looks up a localized string similar to Password. - /// - public static string CustomerProfilePasswordCaption { - get { - return ResourceManager.GetString("CustomerProfilePasswordCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Phone number. - /// - public static string CustomerProfilePhoneNumberCaption { - get { - return ResourceManager.GetString("CustomerProfilePhoneNumberCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Re-enter password. - /// - public static string CustomerProfileReEnterPasswordCaption { - get { - return ResourceManager.GetString("CustomerProfileReEnterPasswordCaption", resourceCulture); - } - } - /// /// Looks up a localized string similar to State/Province. /// @@ -988,7 +790,7 @@ public static string CustomerRegistrationFailureMessage { } /// - /// Looks up a localized string similar to Registering and Creating order.... + /// Looks up a localized string similar to Registering customer.... /// public static string CustomerRegistrationMessage { get { @@ -1023,15 +825,6 @@ public static string CustomizationOfferListColumnCaption { } } - /// - /// Looks up a localized string similar to Please enter a valid CVN. - /// - public static string CvnValidationMessage { - get { - return ResourceManager.GetString("CvnValidationMessage", resourceCulture); - } - } - /// /// Looks up a localized string similar to -. /// @@ -1149,15 +942,6 @@ public static string Description { } } - /// - /// Looks up a localized string similar to Features:. - /// - public static string DetailedFeaturesCaption { - get { - return ResourceManager.GetString("DetailedFeaturesCaption", resourceCulture); - } - } - /// /// Looks up a localized string similar to The domain is not available. Please enter another domain prefix. /// @@ -1248,6 +1032,24 @@ public static string EmptyOffersErrorMessage { } } + /// + /// Looks up a localized string similar to An error occurred while processing your request.. + /// + public static string ErrorPageDetailsCaption { + get { + return ResourceManager.GetString("ErrorPageDetailsCaption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error. + /// + public static string ErrorPageTitleCaption { + get { + return ResourceManager.GetString("ErrorPageTitleCaption", resourceCulture); + } + } + /// /// Looks up a localized string similar to Could not get portal content. /// @@ -1257,12 +1059,21 @@ public static string FailedToLoadPortal { } } + /// + /// Looks up a localized string similar to Failed to update partner offers persistence. + /// + public static string FailedToUpdatePartnerOffersStore { + get { + return ResourceManager.GetString("FailedToUpdatePartnerOffersStore", resourceCulture); + } + } + /// /// Looks up a localized string similar to Features:. /// - public static string FeaturesListRowCaption { + public static string FeaturesCaption { get { - return ResourceManager.GetString("FeaturesListRowCaption", resourceCulture); + return ResourceManager.GetString("FeaturesCaption", resourceCulture); } } @@ -1321,47 +1132,74 @@ public static string HeaderImageUploadTooltip { } /// - /// Looks up a localized string similar to Help. + /// Looks up a localized string similar to Home page. + /// + public static string HomePage { + get { + return ResourceManager.GetString("HomePage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Id must be a valid GUID. + /// + public static string IdMustBeAValidGUID { + get { + return ResourceManager.GetString("IdMustBeAValidGUID", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The selected images are too large. Please specify smaller images or upload one by one. + /// + public static string ImagesTooLarge { + get { + return ResourceManager.GetString("ImagesTooLarge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please specify a valid address. /// - public static string Help { + public static string InvalidAddress { get { - return ResourceManager.GetString("Help", resourceCulture); + return ResourceManager.GetString("InvalidAddress", resourceCulture); } } /// - /// Looks up a localized string similar to Home page. + /// Looks up a localized string similar to Invalid contact sales email address. /// - public static string HomePage { + public static string InvalidContactSalesEmailAddress { get { - return ResourceManager.GetString("HomePage", resourceCulture); + return ResourceManager.GetString("InvalidContactSalesEmailAddress", resourceCulture); } } /// - /// Looks up a localized string similar to The selected images are too large. Please specify smaller images or upload one by one. + /// Looks up a localized string similar to Please enter a valid contact sales phone. /// - public static string ImagesTooLarge { + public static string InvalidContactSalesPhone { get { - return ResourceManager.GetString("ImagesTooLarge", resourceCulture); + return ResourceManager.GetString("InvalidContactSalesPhone", resourceCulture); } } /// - /// Looks up a localized string similar to Please specify a valid address. + /// Looks up a localized string similar to Invalid contact sales phone. /// - public static string InvalidAddress { + public static string InvalidContactSalesPhoneExceptionMessage { get { - return ResourceManager.GetString("InvalidAddress", resourceCulture); + return ResourceManager.GetString("InvalidContactSalesPhoneExceptionMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Please enter a valid contact sales phone. + /// Looks up a localized string similar to Invalid contact us email address. /// - public static string InvalidContactSalesPhone { + public static string InvalidContactUsEmailAddress { get { - return ResourceManager.GetString("InvalidContactSalesPhone", resourceCulture); + return ResourceManager.GetString("InvalidContactUsEmailAddress", resourceCulture); } } @@ -1375,11 +1213,11 @@ public static string InvalidContactUsPhone { } /// - /// Looks up a localized string similar to Customer Id is invalid.. + /// Looks up a localized string similar to Invalid contact us phone. /// - public static string InvalidCustomerIdErrorMessage { + public static string InvalidContactUsPhoneExceptionMessage { get { - return ResourceManager.GetString("InvalidCustomerIdErrorMessage", resourceCulture); + return ResourceManager.GetString("InvalidContactUsPhoneExceptionMessage", resourceCulture); } } @@ -1392,6 +1230,15 @@ public static string InvalidFileType { } } + /// + /// Looks up a localized string similar to Provide an image file type for the header image. + /// + public static string InvalidHeaderImageMessage { + get { + return ResourceManager.GetString("InvalidHeaderImageMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid header image Uri. Please make sure the Uri is valid.. /// @@ -1401,6 +1248,15 @@ public static string InvalidHeaderImageUri { } } + /// + /// Looks up a localized string similar to Invalid header image URI uploaded. + /// + public static string InvalidHeaderImageUriMessage { + get { + return ResourceManager.GetString("InvalidHeaderImageUriMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to There was a problem in one of the fields you entered: . /// @@ -1429,74 +1285,83 @@ public static string InvalidInputInFormCaption { } /// - /// Looks up a localized string similar to Invalid organization logo Uri. Please make sure the Uri is valid.. + /// Looks up a localized string similar to Invalid operation being performed.. /// - public static string InvalidOrganizationLogoUri { + public static string InvalidOperationForOrderMessage { get { - return ResourceManager.GetString("InvalidOrganizationLogoUri", resourceCulture); + return ResourceManager.GetString("InvalidOperationForOrderMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Portal configuration seems to be invalid. + /// Looks up a localized string similar to Provide an image file type for the organization logo. /// - public static string InvalidPortalConfiguration { + public static string InvalidOrganizationLogoFileTypeMessage { get { - return ResourceManager.GetString("InvalidPortalConfiguration", resourceCulture); + return ResourceManager.GetString("InvalidOrganizationLogoFileTypeMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Invalid privacy Uri. Please make sure the Uri is valid.. + /// Looks up a localized string similar to Invalid organization logo Uri. Please make sure the Uri is valid.. /// - public static string InvalidPrivacyAgreementUri { + public static string InvalidOrganizationLogoUri { get { - return ResourceManager.GetString("InvalidPrivacyAgreementUri", resourceCulture); + return ResourceManager.GetString("InvalidOrganizationLogoUri", resourceCulture); } } /// - /// Looks up a localized string similar to Language. + /// Looks up a localized string similar to Invalid organization logo URI uploaded. /// - public static string Language { + public static string InvalidOrganizationLogoUriMessage { get { - return ResourceManager.GetString("Language", resourceCulture); + return ResourceManager.GetString("InvalidOrganizationLogoUriMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Last. + /// Looks up a localized string similar to Payment mode is not supported. /// - public static string Last { + public static string InvalidPaymentModeErrorMessage { get { - return ResourceManager.GetString("Last", resourceCulture); + return ResourceManager.GetString("InvalidPaymentModeErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Portal configuration seems to be invalid. + /// + public static string InvalidPortalConfiguration { + get { + return ResourceManager.GetString("InvalidPortalConfiguration", resourceCulture); } } /// - /// Looks up a localized string similar to Learn more. + /// Looks up a localized string similar to Invalid privacy Uri. Please make sure the Uri is valid.. /// - public static string LearnMore { + public static string InvalidPrivacyAgreementUri { get { - return ResourceManager.GetString("LearnMore", resourceCulture); + return ResourceManager.GetString("InvalidPrivacyAgreementUri", resourceCulture); } } /// - /// Looks up a localized string similar to License. + /// Looks up a localized string similar to Invalid privacy agreement URI uploaded. /// - public static string LicenseCaption { + public static string InvalidPrivacyUriMessage { get { - return ResourceManager.GetString("LicenseCaption", resourceCulture); + return ResourceManager.GetString("InvalidPrivacyUriMessage", resourceCulture); } } /// - /// Looks up a localized string similar to License:. + /// Looks up a localized string similar to Last. /// - public static string LicenseHeader { + public static string Last { get { - return ResourceManager.GetString("LicenseHeader", resourceCulture); + return ResourceManager.GetString("Last", resourceCulture); } } @@ -1537,47 +1402,38 @@ public static string MicrosoftId { } /// - /// Looks up a localized string similar to Microsoft Offering. - /// - public static string MicrosoftOfferListColumnCaption { - get { - return ResourceManager.GetString("MicrosoftOfferListColumnCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Could not retrieve Microsoft offers. + /// Looks up a localized string similar to MicrosoftOfferId must be set. /// - public static string MicrosoftOfferRetrievalErrorMessage { + public static string MicrosoftOfferIdMustBeSet { get { - return ResourceManager.GetString("MicrosoftOfferRetrievalErrorMessage", resourceCulture); + return ResourceManager.GetString("MicrosoftOfferIdMustBeSet", resourceCulture); } } /// - /// Looks up a localized string similar to Fees per month. + /// Looks up a localized string similar to Microsoft offer is not allowed to be updated. Create a new offer instead.. /// - public static string MonthlyFee { + public static string MicrosoftOfferImmutableErrorMessage { get { - return ResourceManager.GetString("MonthlyFee", resourceCulture); + return ResourceManager.GetString("MicrosoftOfferImmutableErrorMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Price per month. + /// Looks up a localized string similar to Microsoft Offering. /// - public static string MonthlyPrice { + public static string MicrosoftOfferListColumnCaption { get { - return ResourceManager.GetString("MonthlyPrice", resourceCulture); + return ResourceManager.GetString("MicrosoftOfferListColumnCaption", resourceCulture); } } /// - /// Looks up a localized string similar to per user/month. + /// Looks up a localized string similar to Could not retrieve Microsoft offers. /// - public static string MonthPerUser { + public static string MicrosoftOfferRetrievalErrorMessage { get { - return ResourceManager.GetString("MonthPerUser", resourceCulture); + return ResourceManager.GetString("MicrosoftOfferRetrievalErrorMessage", resourceCulture); } } @@ -1600,20 +1456,38 @@ public static string MoreThanOneSubscriptionUpdateErrorMessage { } /// - /// Looks up a localized string similar to MOST POPULAR. + /// Looks up a localized string similar to Go back to: . + /// + public static string NavigateBackTo { + get { + return ResourceManager.GetString("NavigateBackTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your registration order was successfully processed.. /// - public static string MostPopular { + public static string NewCustomerOrderSuccessMessage { get { - return ResourceManager.GetString("MostPopular", resourceCulture); + return ResourceManager.GetString("NewCustomerOrderSuccessMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Go back to: . + /// Looks up a localized string similar to Failure in receiving payment information. Please login and add subscriptions.. /// - public static string NavigateBackTo { + public static string NewCustomerProcessOrderFailureReceivingPaymentMessage { get { - return ResourceManager.GetString("NavigateBackTo", resourceCulture); + return ResourceManager.GetString("NewCustomerProcessOrderFailureReceivingPaymentMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Adding subscriptions.. + /// + public static string NewPurchaseOperationCaption { + get { + return ResourceManager.GetString("NewPurchaseOperationCaption", resourceCulture); } } @@ -1644,6 +1518,15 @@ public static string NonAdminUnauthorizedMessage { } } + /// + /// Looks up a localized string similar to Order should have at least one subscription item.. + /// + public static string NotEnoughItemsInOrderErrorMessage { + get { + return ResourceManager.GetString("NotEnoughItemsInOrderErrorMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Active notifications. /// @@ -1708,7 +1591,7 @@ public static string OfferDeletionFailure { } /// - /// Looks up a localized string similar to Describe a feature this offers gives. + /// Looks up a localized string similar to Describe a feature this offer gives. /// public static string OfferFeaturePlaceHolder { get { @@ -1717,20 +1600,29 @@ public static string OfferFeaturePlaceHolder { } /// - /// Looks up a localized string similar to Example: $100 Per Year. + /// Looks up a localized string similar to Manually renewable. /// - public static string OfferLicensePlaceHolder { + public static string OfferManuallyRenewable { get { - return ResourceManager.GetString("OfferLicensePlaceHolder", resourceCulture); + return ResourceManager.GetString("OfferManuallyRenewable", resourceCulture); } } /// - /// Looks up a localized string similar to Manually renewable. + /// Looks up a localized string similar to Offer not found. /// - public static string OfferManuallyRenewable { + public static string OfferNotFound { get { - return ResourceManager.GetString("OfferManuallyRenewable", resourceCulture); + return ResourceManager.GetString("OfferNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Offer price should be more than zero. + /// + public static string OfferPriceShouldBeMoreThanZero { + get { + return ResourceManager.GetString("OfferPriceShouldBeMoreThanZero", resourceCulture); } } @@ -1824,6 +1716,24 @@ public static string OrderAddFailureMessage { } } + /// + /// Looks up a localized string similar to Error while processing order. Please retry later.. + /// + public static string OrderFailureMessage { + get { + return ResourceManager.GetString("OrderFailureMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failure in processing order. Please click done to try again.. + /// + public static string OrderProcessingFailureNotification { + get { + return ResourceManager.GetString("OrderProcessingFailureNotification", resourceCulture); + } + } + /// /// Looks up a localized string similar to Order Registration Error. /// @@ -1887,24 +1797,6 @@ public static string OrganizationNameFieldSubText { } } - /// - /// Looks up a localized string similar to Other settings. - /// - public static string OtherSettingsConfigurationCaption { - get { - return ResourceManager.GetString("OtherSettingsConfigurationCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Click to configure other settings. - /// - public static string OtherSettingsSectionTooltip { - get { - return ResourceManager.GetString("OtherSettingsSectionTooltip", resourceCulture); - } - } - /// /// Looks up a localized string similar to Password. /// @@ -1977,6 +1869,15 @@ public static string PaymentGatewayIdentityFailureDuringPayment { } } + /// + /// Looks up a localized string similar to Failure in receiving payment information. Please click done to try again.. + /// + public static string PaymentReceiptFailureNotification { + get { + return ResourceManager.GetString("PaymentReceiptFailureNotification", resourceCulture); + } + } + /// /// Looks up a localized string similar to Click to configure your payment options. /// @@ -2041,38 +1942,38 @@ public static string PayPalClientSecretSubText { } /// - /// Looks up a localized string similar to Paypal.com. + /// Looks up a localized string similar to We are unable to process your payment.. /// - public static string PayPalWebsiteCaption { + public static string PayPalUnableToProcessPayment { get { - return ResourceManager.GetString("PayPalWebsiteCaption", resourceCulture); + return ResourceManager.GetString("PayPalUnableToProcessPayment", resourceCulture); } } /// - /// Looks up a localized string similar to Phone number. + /// Looks up a localized string similar to PayPal.com. /// - public static string PhoneHeader { + public static string PayPalWebsiteCaption { get { - return ResourceManager.GetString("PhoneHeader", resourceCulture); + return ResourceManager.GetString("PayPalWebsiteCaption", resourceCulture); } } /// - /// Looks up a localized string similar to Please enter a valid phone number. + /// Looks up a localized string similar to Phone number. /// - public static string PhoneValidationMessage { + public static string PhoneHeaderCaption { get { - return ResourceManager.GetString("PhoneValidationMessage", resourceCulture); + return ResourceManager.GetString("PhoneHeaderCaption", resourceCulture); } } /// - /// Looks up a localized string similar to Pick your Offer. + /// Looks up a localized string similar to Please enter a valid phone number. /// - public static string PickOffer { + public static string PhoneValidationMessage { get { - return ResourceManager.GetString("PickOffer", resourceCulture); + return ResourceManager.GetString("PhoneValidationMessage", resourceCulture); } } @@ -2131,11 +2032,11 @@ public static string PortalStartupFailure { } /// - /// Looks up a localized string similar to Powered by Microsoft WebPortal.Net. + /// Looks up a localized string similar to Preparing order and redirecting to payment gateway.... /// - public static string PoweredBy { + public static string PreparingOrderAndRedirectingMessage { get { - return ResourceManager.GetString("PoweredBy", resourceCulture); + return ResourceManager.GetString("PreparingOrderAndRedirectingMessage", resourceCulture); } } @@ -2185,47 +2086,38 @@ public static string PrivacyAgreementToolTip { } /// - /// Looks up a localized string similar to Sample Corporation. - /// - public static string ProductTitle { - get { - return ResourceManager.GetString("ProductTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Pro-rated. + /// Looks up a localized string similar to Payment information collected, processing order.... /// - public static string ProratedPaymentTotal { + public static string ProcessingOrderMessage { get { - return ResourceManager.GetString("ProratedPaymentTotal", resourceCulture); + return ResourceManager.GetString("ProcessingOrderMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Quantity. + /// Looks up a localized string similar to Successfully received payment information. Processing order.... /// - public static string Quantity { + public static string ProcessingOrderNotification { get { - return ResourceManager.GetString("Quantity", resourceCulture); + return ResourceManager.GetString("ProcessingOrderNotification", resourceCulture); } } /// - /// Looks up a localized string similar to Amount paid on {CurrentDate}. + /// Looks up a localized string similar to Order Processing Page.. /// - public static string RegistrationConfirmAmountPaidOnCaption { + public static string ProcessOrderPageCaption { get { - return ResourceManager.GetString("RegistrationConfirmAmountPaidOnCaption", resourceCulture); + return ResourceManager.GetString("ProcessOrderPageCaption", resourceCulture); } } /// - /// Looks up a localized string similar to Annual commitment expires on {currentDate + 365}. + /// Looks up a localized string similar to Pro-rated. /// - public static string RegistrationConfirmAnnualCommitmentCaption { + public static string ProratedPaymentTotal { get { - return ResourceManager.GetString("RegistrationConfirmAnnualCommitmentCaption", resourceCulture); + return ResourceManager.GetString("ProratedPaymentTotal", resourceCulture); } } @@ -2293,20 +2185,29 @@ public static string RemoveSummaryPointTooltip { } /// - /// Looks up a localized string similar to Renew subscription. + /// Looks up a localized string similar to Renewing subscription.. /// - public static string RenewSubscriptionTitleCaption { + public static string RenewOperationCaption { get { - return ResourceManager.GetString("RenewSubscriptionTitleCaption", resourceCulture); + return ResourceManager.GetString("RenewOperationCaption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RenewSubscription.ExecuteAsync() Failed: {0}, SubscriptionId: {1}. + /// + public static string RenewSubscriptionFailedMessage { + get { + return ResourceManager.GetString("RenewSubscriptionFailedMessage", resourceCulture); } } /// - /// Looks up a localized string similar to retry. + /// Looks up a localized string similar to Renew subscription. /// - public static string Retry { + public static string RenewSubscriptionTitleCaption { get { - return ResourceManager.GetString("Retry", resourceCulture); + return ResourceManager.GetString("RenewSubscriptionTitleCaption", resourceCulture); } } @@ -2410,20 +2311,20 @@ public static string SetupInformation { } /// - /// Looks up a localized string similar to Sign out. + /// Looks up a localized string similar to Shown in the Contact us section of the header. /// - public static string Signout { + public static string ShowInContactUsHeaderCaption { get { - return ResourceManager.GetString("Signout", resourceCulture); + return ResourceManager.GetString("ShowInContactUsHeaderCaption", resourceCulture); } } /// - /// Looks up a localized string similar to Status. + /// Looks up a localized string similar to Sign out. /// - public static string Status { + public static string Signout { get { - return ResourceManager.GetString("Status", resourceCulture); + return ResourceManager.GetString("Signout", resourceCulture); } } @@ -2436,15 +2337,6 @@ public static string Submit { } } - /// - /// Looks up a localized string similar to Subscripiton alias. - /// - public static string SubscripitonAlias { - get { - return ResourceManager.GetString("SubscripitonAlias", resourceCulture); - } - } - /// /// Looks up a localized string similar to Subscription. /// @@ -2490,15 +2382,6 @@ public static string SubscriptionSummaryAnnualCommitmentExpiryCaption { } } - /// - /// Looks up a localized string similar to Annual commitment expires on. - /// - public static string SubscriptionSummaryAnnualCommitmentExpiryPrefixCaption { - get { - return ResourceManager.GetString("SubscriptionSummaryAnnualCommitmentExpiryPrefixCaption", resourceCulture); - } - } - /// /// Looks up a localized string similar to Price Paid. /// @@ -2535,24 +2418,6 @@ public static string SubscriptionSummaryRenewLinkCaption { } } - /// - /// Looks up a localized string similar to Add more licenses. - /// - public static string SubscriptionSummaryUpdateLinkCaption { - get { - return ResourceManager.GetString("SubscriptionSummaryUpdateLinkCaption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Summary:. - /// - public static string SummaryCaption { - get { - return ResourceManager.GetString("SummaryCaption", resourceCulture); - } - } - /// /// Looks up a localized string similar to Summary:. /// @@ -2562,15 +2427,6 @@ public static string SummaryListRowCaption { } } - /// - /// Looks up a localized string similar to Support. - /// - public static string Support { - get { - return ResourceManager.GetString("Support", resourceCulture); - } - } - /// /// Looks up a localized string similar to Support email. /// @@ -2580,15 +2436,6 @@ public static string SupportEmailAddressHeader { } } - /// - /// Looks up a localized string similar to Suspended. - /// - public static string SuspendedStatus { - get { - return ResourceManager.GetString("SuspendedStatus", resourceCulture); - } - } - /// /// Looks up a localized string similar to Could not get the feature content. /// @@ -2670,15 +2517,6 @@ public static string UnpaidSubscriptionSuffix { } } - /// - /// Looks up a localized string similar to Update. - /// - public static string Update { - get { - return ResourceManager.GetString("Update", resourceCulture); - } - } - /// /// Looks up a localized string similar to Could not save payment information. /// @@ -2706,15 +2544,6 @@ public static string UpdatePaymentSuccessMessage { } } - /// - /// Looks up a localized string similar to Add more licenses. - /// - public static string UpdateSubscriptions { - get { - return ResourceManager.GetString("UpdateSubscriptions", resourceCulture); - } - } - /// /// Looks up a localized string similar to Updating offer. /// @@ -2724,24 +2553,6 @@ public static string UpdatingOfferMessage { } } - /// - /// Looks up a localized string similar to Updating subscriptions.... - /// - public static string UpdatingSubscriptionMessage { - get { - return ResourceManager.GetString("UpdatingSubscriptionMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Please use an alternate card.. - /// - public static string UseAlternateCardMessage { - get { - return ResourceManager.GetString("UseAlternateCardMessage", resourceCulture); - } - } - /// /// Looks up a localized string similar to Licenses. /// @@ -2751,15 +2562,6 @@ public static string UserLicencesCaption { } } - /// - /// Looks up a localized string similar to User name. - /// - public static string UserName { - get { - return ResourceManager.GetString("UserName", resourceCulture); - } - } - /// /// Looks up a localized string similar to User name:. /// diff --git a/Source/PartnerCenter.CustomerPortal/Resources.resx b/Source/PartnerCenter.CustomerPortal/Resources.resx index 546e651..da8df30 100644 --- a/Source/PartnerCenter.CustomerPortal/Resources.resx +++ b/Source/PartnerCenter.CustomerPortal/Resources.resx @@ -117,9 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - About - Back @@ -150,18 +147,12 @@ First - - Help - Portal configuration seems to be invalid Last - - Learn more - Loading @@ -171,9 +162,6 @@ More actions... - - MOST POPULAR - Go back to: @@ -198,27 +186,15 @@ Portal configuation could not be found - - Powered by Microsoft WebPortal.Net - - - Sample Corporation - Remember Me - - retry - Retry Sign out - - Support - Could not get the feature content @@ -234,50 +210,26 @@ Yes - - add - - - Add plan - You must select at least one offer to purchase. Total Amount - - Fees per month - - - Quantity - Remove from cart - - - account - - - Add - Address Admin user account - - Company information - - - Company name - Confirmation - Thank you! We have received your order. It may take a few minutes to process. + Thank you! Customer information has been setup. Please review this page and click submit to place your order. Customer information @@ -285,15 +237,9 @@ Done - - Language - Microsoft ID - - Price per month - Details @@ -306,12 +252,6 @@ Total - - Update - - - User name - Add subscriptions @@ -324,36 +264,15 @@ You must accept the terms and conditions - - Active - - - Please enter a valid CVN - Please enter a valid domain prefix - - per user/month - Please enter a valid phone number - - Status - - - Subscripiton alias - Subscription ID: - - Suspended - - - Add more licenses - Update your company information @@ -363,9 +282,6 @@ Choose which offers to sell - - Other settings - Add a payment method @@ -384,9 +300,6 @@ - - - Features: - Summary: @@ -396,18 +309,12 @@ Customization - + Features: - - License - Select a Microsoft offer: - - Summary: - Annual price: @@ -432,14 +339,8 @@ Click to configure your company's branding - - License: - - Describe a feature this offers gives - - - Example: $100 Per Year + Describe a feature this offer gives Click to configure your offers @@ -447,9 +348,6 @@ Write an offer summary - - Click to configure other settings - Click to configure your payment options @@ -594,15 +492,9 @@ Sales contact: - - Shown in the Contact us section of the header - Support contact: - - Shown in the Contact us section of the header - Support email @@ -630,7 +522,7 @@ Your company’s legal name, shown on the header of every page - + Phone number @@ -678,9 +570,6 @@ Enter your PayPal client secret - - Paypal.com - Paypal sandbox account @@ -696,27 +585,6 @@ Payment information updated - - Verification numer (CVN) - - - Expriration date - - - First name on card - - - Credit Card Information - - - Last name on card - - - Card number - - - Credit card type - Address line 1 @@ -732,9 +600,6 @@ Company Information - - Company Name - Contact Information @@ -759,33 +624,15 @@ This will be the user id you'll use with your Office 365 account. - - Office 365 - Username - - Password - - - Phone number - - - Re-enter password - State/Province Zip / Postal code - - Amount paid on {CurrentDate} - - - Annual commitment expires on {currentDate + 365} - -Unpaid @@ -801,9 +648,6 @@ Amount paid on - - Annual commitment expires on - Price Paid @@ -813,9 +657,6 @@ Renew - - Add more licenses - Added seats @@ -825,14 +666,11 @@ Renewal - - Creating order for your registration... - Could not register the customer. Please try later. - Registering and Creating order... + Registering customer... Successfully created customer @@ -840,9 +678,6 @@ Order Registration Error - - Updating subscriptions... - Buy more subscriptions @@ -861,21 +696,6 @@ Order Update Error - - Amex - - - Discover - - - Master Card - - - Visa - - - Not supported - There was an input error encountered. Please check the inputs and try again. @@ -909,9 +729,6 @@ Please add a subscription - - Pick your Offer - Home page @@ -936,14 +753,11 @@ We are unable to process your request at this time. Please try later or contact us over email. Error Details - - - Customer Id is invalid. - Only one subcription can be updated. - - Adding subcriptions... + + Preparing order and redirecting to payment gateway... Error during adding subscriptions. @@ -1023,18 +837,6 @@ One or more offers you are trying to add already exists. You can add more licenses to existing subscriptions. - - The card verfiication number check failed. Please enter a valid CVN. - - - The card has expired. - - - The card was refused. - - - Please use an alternate card. - user/year @@ -1044,4 +846,136 @@ Please enter a valid contact support phone + + Payment information collected, processing order... + + + Adding seats to subscription. + + + Adding subscriptions. + + + Renewing subscription. + + + Error while processing order. Please retry later. + + + Failure in processing order. Please click done to try again. + + + Failure in receiving payment information. Please click done to try again. + + + Successfully received payment information. Processing order... + + + Order Processing Page. + + + Your registration order was successfully processed. + + + Failure in receiving payment information. Please login and add subscriptions. + + + We are unable to process your payment. + + + Shown in the Contact us section of the header + + + {0} AD tenant is already registered to Partner Center customer: {1} + + + phone number + + + {0} is invalid. + + + number + + + {0} must be greater than zero + + + string + + + {0} is not set + + + RenewSubscription.ExecuteAsync() Failed: {0}, SubscriptionId: {1} + + + Provide an image file type for the organization logo + + + Invalid organization logo URI uploaded + + + Provide an image file type for the header image + + + Invalid header image URI uploaded + + + Invalid privacy agreement URI uploaded + + + An error occurred while processing your request. + + + Error + + + Invalid operation being performed. + + + Order should have at least one subscription item. + + + Payment mode is not supported + + + Offer price should be more than zero + + + MicrosoftOfferId must be set + + + Id must be a valid GUID + + + Failed to update partner offers persistence + + + Microsoft offer is not allowed to be updated. Create a new offer instead. + + + Offer not found + + + ContactUs section not found in portal branding configuration + + + Invalid contact us email address + + + Invalid contact us phone + + + Invalid contact sales email address + + + Invalid contact sales phone + + + Add more licenses + + + PayPal.com + \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/AddSubscriptionsPresenter.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/AddSubscriptionsPresenter.js index 19fb9aa..79c959c 100644 --- a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/AddSubscriptionsPresenter.js +++ b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/AddSubscriptionsPresenter.js @@ -8,8 +8,7 @@ Microsoft.WebPortal.AddSubscriptionsPresenter = function (webPortal, feature, co /// The feature for which this presenter is created. this.base.constructor.call(this, webPortal, feature, "Add Subscriptions", "/Template/AddSubscriptions/"); - this.addSubscriptionsView = new Microsoft.WebPortal.Views.AddSubscriptionsView(webPortal, "#AddSubscriptionsViewContainer", context); - this.creditCardInputView = new Microsoft.WebPortal.Views.CreditCardInputView(webPortal, "#CreditCardInputContainer"); + this.addSubscriptionsView = new Microsoft.WebPortal.Views.AddSubscriptionsView(webPortal, "#AddSubscriptionsViewContainer", context); this.onCancelClicked = function () { webPortal.Journey.retract(); @@ -17,6 +16,7 @@ Microsoft.WebPortal.AddSubscriptionsPresenter = function (webPortal, feature, co var self = this; var isPosting = false; + self.OperationType = Microsoft.WebPortal.CommerceOperationType.NewPurchase; //PurchaseSubscriptions. this.getOrders = function () { var orders = []; @@ -24,6 +24,7 @@ Microsoft.WebPortal.AddSubscriptionsPresenter = function (webPortal, feature, co for (var i in this.addSubscriptionsView.subscriptionsList.rows()) { orders.push({ OfferId: this.addSubscriptionsView.subscriptionsList.rows()[i].offer.Id, + SubscriptionId: this.addSubscriptionsView.subscriptionsList.rows()[i].offer.Id, Quantity: this.addSubscriptionsView.subscriptionsList.rows()[i].quantity() }); } @@ -48,18 +49,20 @@ Microsoft.WebPortal.AddSubscriptionsPresenter = function (webPortal, feature, co isPosting = true; - var notification = new Microsoft.WebPortal.Services.Notification(Microsoft.WebPortal.Services.Notification.NotificationType.Progress, self.webPortal.Resources.Strings.Plugins.AddSubscriptionPage.AddingSubscriptionMessage); + var notification = new Microsoft.WebPortal.Services.Notification(Microsoft.WebPortal.Services.Notification.NotificationType.Progress, self.webPortal.Resources.Strings.Plugins.AddSubscriptionPage.PreparingOrderAndRedirectingMessage); self.webPortal.Services.Notifications.add(notification); - new Microsoft.WebPortal.Utilities.RetryableServerCall(this.webPortal.Helpers.ajaxCall("api/Subscription", Microsoft.WebPortal.HttpMethod.Post, { + new Microsoft.WebPortal.Utilities.RetryableServerCall(this.webPortal.Helpers.ajaxCall("api/Order/Prepare", Microsoft.WebPortal.HttpMethod.Post, { Subscriptions: self.getOrders(), - CreditCard: self.getCreditCardInfo(), + OperationType: self.OperationType // CustomerId: registeredCustomer.MicrosoftId // Will be retrieved from loggin in principle. }, Microsoft.WebPortal.ContentType.Json, 120000), "AddSubscriptions", []).execute() - .done(function () { - notification.dismiss(); - self.webPortal.Journey.start(Microsoft.WebPortal.Feature.Subscriptions); + .done(function (result) { + // hand it off to the subscriptions page. + notification.dismiss(); + // we need to now redirect to paypal based on the response from the API. + window.location = result; }) .fail(function (result, status, error) { notification.type(Microsoft.WebPortal.Services.Notification.NotificationType.Error); @@ -76,28 +79,9 @@ Microsoft.WebPortal.AddSubscriptionsPresenter = function (webPortal, feature, co case Microsoft.WebPortal.ErrorCode.InvalidInput: notification.message(self.webPortal.Resources.Strings.Plugins.AddSubscriptionPage.InvalidInputErrorPrefix + errorPayload.Details.ErrorMessage); break; - case Microsoft.WebPortal.ErrorCode.AlreadyExists: - notification.message(self.webPortal.Resources.Strings.Plugins.AddSubscriptionPage.CannotAddExistingSubscriptionError); - break; case Microsoft.WebPortal.ErrorCode.DownstreamServiceError: notification.message(self.webPortal.Resources.Strings.Plugins.AddSubscriptionPage.DownstreamErrorPrefix + errorPayload.Details.ErrorMessage); break; - case Microsoft.WebPortal.ErrorCode.CardCVNCheckFailed: - notification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.CardCVNFailedError); - break; - case Microsoft.WebPortal.ErrorCode.CardExpired: - notification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.CardExpiredError + self.webPortal.Resources.Strings.Plugins.CreditCardView.UseAlternateCardMessage); - break; - case Microsoft.WebPortal.ErrorCode.CardRefused: - notification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.CardRefusedError + self.webPortal.Resources.Strings.Plugins.CreditCardView.UseAlternateCardMessage); - break; - case Microsoft.WebPortal.ErrorCode.PaymentGatewayPaymentError: - notification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.UseAlternateCardMessage); - break; - case Microsoft.WebPortal.ErrorCode.PaymentGatewayIdentityFailureDuringPayment: - case Microsoft.WebPortal.ErrorCode.PaymentGatewayFailure: - notification.message(errorPayload.Details.ErrorMessage); - break; default: notification.message(self.webPortal.Resources.Strings.Plugins.AddSubscriptionPage.OrderAddFailureMessage); break; @@ -113,21 +97,6 @@ Microsoft.WebPortal.AddSubscriptionsPresenter = function (webPortal, feature, co // the form is invalid } } - - this.getCreditCardInfo = function () { - var paymentCard = { - CreditCardType: this.creditCardInputView.viewModel.CardType(), - CardHolderFirstName: this.creditCardInputView.viewModel.CardHolderFirstName(), - CardHolderLastName: this.creditCardInputView.viewModel.CardHolderLastName(), - CreditCardNumber: this.creditCardInputView.viewModel.CardNumber(), - CreditCardExpiryMonth: this.creditCardInputView.viewModel.Month(), - CreditCardExpiryYear: this.creditCardInputView.viewModel.Year(), - CreditCardCvn: this.creditCardInputView.viewModel.CardCvn() - } - - return paymentCard; - } - } // inherit BasePresenter @@ -146,8 +115,7 @@ Microsoft.WebPortal.AddSubscriptionsPresenter.prototype.onRender = function () { ko.applyBindings(this, $("#Form")[0]); - this.addSubscriptionsView.render(); - this.creditCardInputView.render(); + this.addSubscriptionsView.render(); } Microsoft.WebPortal.AddSubscriptionsPresenter.prototype.onShow = function () { @@ -155,8 +123,7 @@ Microsoft.WebPortal.AddSubscriptionsPresenter.prototype.onShow = function () { /// Called when content is shown. /// - this.addSubscriptionsView.show(); - this.creditCardInputView.show(); + this.addSubscriptionsView.show(); // show the offers dialog if there is no row set from parent page. if (this.addSubscriptionsView.subscriptionsList.rows().length <= 0) { diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/CommerceOperationType.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/CommerceOperationType.js new file mode 100644 index 0000000..94a83ad --- /dev/null +++ b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/CommerceOperationType.js @@ -0,0 +1,21 @@ +/// + +Microsoft.WebPortal.CommerceOperationType = { + + /// + /// A brand new purchase. + /// + NewPurchase:0, + + /// + /// Purchase of additional seats for an existing subscription. + /// + AdditionalSeatsPurchase:1, + + /// + /// Existing subscription renewal. + /// + Renewal:2 +} + +//@ sourceURL=CommerceOperationType.js \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/CustomerRegistrationPresenter.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/CustomerRegistrationPresenter.js index 2ece141..5695d5b 100644 --- a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/CustomerRegistrationPresenter.js +++ b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/CustomerRegistrationPresenter.js @@ -9,8 +9,7 @@ Microsoft.WebPortal.CustomerRegistrationPresenter = function (webPortal, feature this.base.constructor.call(this, webPortal, feature, "Home", "/Template/CustomerRegistration/"); this.addSubscriptionsView = new Microsoft.WebPortal.Views.AddSubscriptionsView(webPortal, "#AddSubscriptionsViewContainer", context); - this.customerProfileView = new Microsoft.WebPortal.Views.NewCustomerProfileView(webPortal, "#CustomerProfileContainer"); - this.creditCardInputView = new Microsoft.WebPortal.Views.CreditCardInputView(webPortal, "#CreditCardInputContainer"); + this.customerProfileView = new Microsoft.WebPortal.Views.NewCustomerProfileView(webPortal, "#CustomerProfileContainer"); this.context = context; this.customerRegistrationInfo; @@ -66,7 +65,30 @@ Microsoft.WebPortal.CustomerRegistrationPresenter = function (webPortal, feature ]); // raise the Order passing along the registrationConfirmation data object. - self.raiseOrder(customerNotification, self.customerRegistrationInfo); + // self.raiseOrder(customerNotification, self.customerRegistrationInfo); + var registeredCustomer = registrationConfirmation; + var registrationConfirmationInfo = { + SubscriptionsToOrder: self.getSubscriptions(), + AddressLine1: registeredCustomer.AddressLine1, + AddressLine2: registeredCustomer.AddressLine2, + AdminUserAccount: registeredCustomer.AdminUserAccount, + Password: registeredCustomer.Password, + City: registeredCustomer.City, + CompanyName: registeredCustomer.CompanyName, + Country: registeredCustomer.Country, + Email: registeredCustomer.Email, + FirstName: registeredCustomer.FirstName, + Language: registeredCustomer.Language, + LastName: registeredCustomer.LastName, + MicrosoftId: registeredCustomer.MicrosoftId, + Phone: registeredCustomer.Phone, + State: registeredCustomer.State, + UserName: registeredCustomer.UserName, + ZipCode: registeredCustomer.ZipCode + } + + // hand it off to the registration summary presenter + self.webPortal.Journey.advance(Microsoft.WebPortal.Feature.RegistrationConfirmation, registrationConfirmationInfo); }) // Failure of Create Customer API Call. .fail(function (result, status, error) { @@ -107,120 +129,18 @@ Microsoft.WebPortal.CustomerRegistrationPresenter = function (webPortal, feature self.isPosting = false; }); } - - // raise the Order passing along the registrationConfirmation data object only if customerId is present. - if (customerId) { - self.raiseOrder(customerNotification, self.customerRegistrationInfo); - } - } else { // the form is invalid } } - this.raiseOrder = function (customerNotification, registeredCustomer) { - /// - /// Called when the customer has been created and hence order can be placed. - /// - - // order notification. - var orderNotification = new Microsoft.WebPortal.Services.Notification(Microsoft.WebPortal.Services.Notification.NotificationType.Progress, - self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.CustomerOrderRegistrationMessage); - self.webPortal.Services.Notifications.add(orderNotification); - - new Microsoft.WebPortal.Utilities.RetryableServerCall(this.webPortal.Helpers.ajaxCall("api/Subscription/RegistrationOrder", Microsoft.WebPortal.HttpMethod.Post, { - Subscriptions: this.getSubscriptions(), - CreditCard: this.getCreditCardInfo(), - CustomerId: registeredCustomer.MicrosoftId // populate the Customer Id. - }, Microsoft.WebPortal.ContentType.Json, 120000), "RegisterCustomerOrder", []).execute() - // Success of Create CustomerOrder API Call. - .done(function (orderConfirmation) { - if (customerNotification) { - customerNotification.dismiss(); - } - orderNotification.dismiss(); - - // Build the RegistrationConfirmation object & pass it off to RegistrationConfirmationPage. - var registrationConfirmationInfo = { - CreatedSubscriptions: orderConfirmation, - AddressLine1: registeredCustomer.AddressLine1, - AddressLine2: registeredCustomer.AddressLine2, - AdminUserAccount: registeredCustomer.AdminUserAccount, - Password: registeredCustomer.Password, - City: registeredCustomer.City, - CompanyName: registeredCustomer.CompanyName, - Country: registeredCustomer.Country, - Email: registeredCustomer.Email, - FirstName: registeredCustomer.FirstName, - Language: registeredCustomer.Language, - LastName: registeredCustomer.LastName, - MicrosoftId: registeredCustomer.MicrosoftId, - Phone: registeredCustomer.Phone, - State: registeredCustomer.State, - UserName: registeredCustomer.UserName, - ZipCode: registeredCustomer.ZipCode - } - - // hand it off to the registration summary presenter - self.webPortal.Journey.advance(Microsoft.WebPortal.Feature.RegistrationConfirmation, registrationConfirmationInfo); - }) - // Failure in Create CustomerOrder API call. - .fail(function (result, status, error) { - // on failure check if customerid is returned (or check using errCode). if returned then do something to set the ClientCustomerId - orderNotification.type(Microsoft.WebPortal.Services.Notification.NotificationType.Error); - orderNotification.buttons([ - // no need for retry button. user should be able to hit submit. - Microsoft.WebPortal.Services.Button.create(Microsoft.WebPortal.Services.Button.StandardButtons.OK, self.webPortal.Resources.Strings.OK, function () { - orderNotification.dismiss(); - }) - ]); - - var errorPayload = JSON.parse(result.responseText); - - if (errorPayload) { - switch (errorPayload.ErrorCode) { - case Microsoft.WebPortal.ErrorCode.InvalidInput: - orderNotification.message(self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.InvalidInputErrorPrefix + errorPayload.Details.ErrorMessage); - break; - case Microsoft.WebPortal.ErrorCode.DownstreamServiceError: - orderNotification.message(self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.DownstreamErrorPrefix + errorPayload.Details.ErrorMessage); - break; - case Microsoft.WebPortal.ErrorCode.CardCVNCheckFailed: - orderNotification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.CardCVNFailedError); - break; - case Microsoft.WebPortal.ErrorCode.CardExpired: - orderNotification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.CardExpiredError + self.webPortal.Resources.Strings.Plugins.CreditCardView.UseAlternateCardMessage); - break; - case Microsoft.WebPortal.ErrorCode.CardRefused: - orderNotification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.CardRefusedError + self.webPortal.Resources.Strings.Plugins.CreditCardView.UseAlternateCardMessage); - break; - case Microsoft.WebPortal.ErrorCode.PaymentGatewayPaymentError: - orderNotification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.UseAlternateCardMessage); - break; - case Microsoft.WebPortal.ErrorCode.PaymentGatewayIdentityFailureDuringPayment: - case Microsoft.WebPortal.ErrorCode.PaymentGatewayFailure: - orderNotification.message(errorPayload.Details.ErrorMessage); - break; - default: - orderNotification.message(self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.OrderRegistrationFailureMessage); - break; - } - } else { - orderNotification.message(self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.OrderRegistrationFailureMessage); - } - - }) - .always(function () { - self.isPosting = false; - }); - } - this.getSubscriptions = function () { var orders = []; for (var i in this.addSubscriptionsView.subscriptionsList.rows()) { orders.push({ - OfferId: this.addSubscriptionsView.subscriptionsList.rows()[i].offer.Id, + OfferId: this.addSubscriptionsView.subscriptionsList.rows()[i].offer.Id, + SubscriptionId: this.addSubscriptionsView.subscriptionsList.rows()[i].offer.Id, Quantity: this.addSubscriptionsView.subscriptionsList.rows()[i].quantity() }); } @@ -248,20 +168,6 @@ Microsoft.WebPortal.CustomerRegistrationPresenter = function (webPortal, feature return customerInformation; } - - this.getCreditCardInfo = function () { - var paymentCard = { - CreditCardType: this.creditCardInputView.viewModel.CardType(), - CardHolderFirstName: this.creditCardInputView.viewModel.CardHolderFirstName(), - CardHolderLastName: this.creditCardInputView.viewModel.CardHolderLastName(), - CreditCardNumber: this.creditCardInputView.viewModel.CardNumber(), - CreditCardExpiryMonth: this.creditCardInputView.viewModel.Month(), - CreditCardExpiryYear: this.creditCardInputView.viewModel.Year(), - CreditCardCvn: this.creditCardInputView.viewModel.CardCvn() - } - - return paymentCard; - } } // inherit BasePresenter @@ -282,8 +188,6 @@ Microsoft.WebPortal.CustomerRegistrationPresenter.prototype.onRender = function this.addSubscriptionsView.render(); this.customerProfileView.render(); - this.creditCardInputView.render(); - } Microsoft.WebPortal.CustomerRegistrationPresenter.prototype.onShow = function () { @@ -292,8 +196,7 @@ Microsoft.WebPortal.CustomerRegistrationPresenter.prototype.onShow = function () /// this.addSubscriptionsView.show(); - this.customerProfileView.show(); - this.creditCardInputView.show(); + this.customerProfileView.show(); } //@ sourceURL=CustomerRegistrationPresenter.js \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/ErrorCode.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/ErrorCode.js index 0dd34c0..9e3f89b 100644 --- a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/ErrorCode.js +++ b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/ErrorCode.js @@ -91,25 +91,10 @@ Microsoft.WebPortal.ErrorCode = { /// PaymentGatewayIdentityFailureDuringPayment: 17, - /// - /// Failure in payment gateway due to card refusal by financial instituion. - /// - CardRefused: 18, - - /// - /// Failure in payment gateway due to use of an expired card during payment. - /// - CardExpired: 19, - - /// - /// Failure in payment gateway due to invalid CVN. - /// - CardCVNCheckFailed: 20, - /// /// Failure in payment gateway during payment. /// - PaymentGatewayPaymentError: 21 + PaymentGatewayPaymentError: 18 } //@ sourceURL=ErrorCode.js \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/OfferListPresenter.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/OfferListPresenter.js index a82d085..3cfa0d7 100644 --- a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/OfferListPresenter.js +++ b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/OfferListPresenter.js @@ -166,7 +166,8 @@ Microsoft.WebPortal.OfferListPresenter.prototype._deleteSelectedOffers = functio // aggregate the partner and microsoft offers for (var i in partnerOffers) { - self.viewModel.Offers.push({ + + var offerToPush = { PartnerOffer: partnerOffers[i], MicrosoftOffer: function () { for (var j in self.microsoftOffers) { @@ -174,23 +175,28 @@ Microsoft.WebPortal.OfferListPresenter.prototype._deleteSelectedOffers = functio return self.microsoftOffers[j]; } } - return null; }() - }); - - self.viewModel.Offers[i].IsOfferAutoRenewableCaption = self.viewModel.Offers[i].MicrosoftOffer.Offer.IsAutoRenewable ? - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferAutomaticallyRenewable : - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferManuallyRenewable; - - self.viewModel.Offers[i].IsAvailableForPurchaseCaption = self.viewModel.Offers[i].MicrosoftOffer.Offer.IsAvailableForPurchase ? - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferAvailableForPurchase : - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferUnavailableForPurchase; - - self.viewModel.Offers[i].AllowedQuantityCaption = self.viewModel.Offers[i].MicrosoftOffer.Offer.MinimumQuantity + - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.To + - self.viewModel.Offers[i].MicrosoftOffer.Offer.MaximumQuantity + - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.Seats; + }; + + // TODO :: Handle Microsoft offer being pulled back due to EOL. + // Temporary fix - Do not display this partner offer for further configuration. Need a better way to handle this. + if (offerToPush.MicrosoftOffer != null) { + offerToPush.IsOfferAutoRenewableCaption = offerToPush.MicrosoftOffer.Offer.IsAutoRenewable ? + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferAutomaticallyRenewable : + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferManuallyRenewable; + + offerToPush.IsAvailableForPurchaseCaption = offerToPush.MicrosoftOffer.Offer.IsAvailableForPurchase ? + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferAvailableForPurchase : + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferUnavailableForPurchase; + + offerToPush.AllowedQuantityCaption = offerToPush.MicrosoftOffer.Offer.MinimumQuantity + + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.To + + offerToPush.MicrosoftOffer.Offer.MaximumQuantity + + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.Seats; + + self.viewModel.Offers.push(offerToPush); + } } self.viewModel.offerList.set(self.viewModel.Offers); @@ -246,7 +252,8 @@ Microsoft.WebPortal.OfferListPresenter.prototype._retrievePartnerOffers = functi // aggregate the partner and microsoft offers for (var i in partnerOffers) { - self.viewModel.Offers.push({ + + var offerToPush = { PartnerOffer: partnerOffers[i], MicrosoftOffer: function () { for (var j in microsoftOffers) { @@ -254,23 +261,28 @@ Microsoft.WebPortal.OfferListPresenter.prototype._retrievePartnerOffers = functi return microsoftOffers[j]; } } - return null; }() - }); - - self.viewModel.Offers[i].IsOfferAutoRenewableCaption = self.viewModel.Offers[i].MicrosoftOffer.Offer.IsAutoRenewable ? - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferAutomaticallyRenewable : - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferManuallyRenewable; - - self.viewModel.Offers[i].IsAvailableForPurchaseCaption = self.viewModel.Offers[i].MicrosoftOffer.Offer.IsAvailableForPurchase ? - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferAvailableForPurchase : - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferUnavailableForPurchase; - - self.viewModel.Offers[i].AllowedQuantityCaption = self.viewModel.Offers[i].MicrosoftOffer.Offer.MinimumQuantity + - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.To + - self.viewModel.Offers[i].MicrosoftOffer.Offer.MaximumQuantity + - self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.Seats; + }; + + // TODO :: Handle Microsoft offer being pulled back due to EOL. + // Temporary fix - Do not display this partner offer for further configuration. Need a better way to handle this. + if (offerToPush.MicrosoftOffer != null) { + offerToPush.IsOfferAutoRenewableCaption = offerToPush.MicrosoftOffer.Offer.IsAutoRenewable ? + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferAutomaticallyRenewable : + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferManuallyRenewable; + + offerToPush.IsAvailableForPurchaseCaption = offerToPush.MicrosoftOffer.Offer.IsAvailableForPurchase ? + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferAvailableForPurchase : + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.OfferUnavailableForPurchase; + + offerToPush.AllowedQuantityCaption = offerToPush.MicrosoftOffer.Offer.MinimumQuantity + + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.To + + offerToPush.MicrosoftOffer.Offer.MaximumQuantity + + self.webPortal.Resources.Strings.Plugins.AdminOfferConfiguration.Seats; + + self.viewModel.Offers.push(offerToPush); + } } self.viewModel.offerList.set(self.viewModel.Offers); diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/ProcessOrderPresenter.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/ProcessOrderPresenter.js new file mode 100644 index 0000000..275f1b7 --- /dev/null +++ b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/ProcessOrderPresenter.js @@ -0,0 +1,147 @@ +/// + +Microsoft.WebPortal.ProcessOrderPresenter = function (webPortal, feature, processOrderViewModel) { + /// + /// Shows the registration confirmation page. + /// + /// The web portal instance. + /// The feature for which this presenter is created. + /// The order processing view model. + this.base.constructor.call(this, webPortal, feature, "Order Processor", "/Template/ProcessOrder/"); + var self = this; + + function QueryStringToJSON() { + var pairs = window.location.hash.slice(1).split('&'); + + var result = {}; + pairs.forEach(function (pair) { + pair = pair.split('='); + result[pair[0]] = decodeURIComponent(pair[1] || ''); + }); + + return JSON.parse(JSON.stringify(result)); + } + var queryStringParams = QueryStringToJSON(); + + self.viewModel = { + paymentId: queryStringParams["paymentId"], + PayerID: queryStringParams["PayerID"], + customerId: queryStringParams["customerId"], + txStatus: queryStringParams["payment"], + PageTitle: ko.observable(""), + Subscriptions: ko.observable(""), + TotalPrice: ko.observable(""), + showDoneButton: ko.observable(false), + showSubscriptions: ko.observable(false), + nextJourney: Microsoft.WebPortal.Feature.Subscriptions + } + + self.apiUrl = ""; + var existingCustomerOrderUrl = "api/Order/Process" + "?paymentId=" + self.viewModel.paymentId + "&payerId=" + self.viewModel.PayerID; + var newCustomerOrderUrl = "api/Order/NewCustomerProcessOrder" + "?customerId=" + self.viewModel.customerId + "&paymentId=" + self.viewModel.paymentId + "&payerId=" + self.viewModel.PayerID; + + if (self.viewModel.customerId != null) { + self.apiUrl = newCustomerOrderUrl; + self.viewModel.nextJourney = Microsoft.WebPortal.Feature.Home; + } else { + self.apiUrl = existingCustomerOrderUrl; + self.viewModel.nextJourney = Microsoft.WebPortal.Feature.Subscriptions; + } + + + this.onDoneClicked = function () { + self.webPortal.Journey.start(self.viewModel.nextJourney); + } +} + +// inherit BasePresenter +$WebPortal.Helpers.inherit(Microsoft.WebPortal.ProcessOrderPresenter, Microsoft.WebPortal.Core.TemplatePresenter); + +Microsoft.WebPortal.ProcessOrderPresenter.prototype.onRender = function () { + /// + /// Called when the presenter is about to be rendered. + /// + + var self = this; + ko.applyBindings(this, $("#ProcessOrderContainer")[0]); + + var processOrder = function() { + // If paypal sent failure then display message and go back to subscriptions page. + if (self.viewModel.txStatus.toLowerCase() == "failure") { + self.viewModel.showDoneButton(true); + if (self.viewModel.customerId != null) { + self.viewModel.PageTitle(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.NewCustomerProcessOrderFailureReceivingPaymentMessage); + } else { + self.viewModel.PageTitle(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.PaymentReceiptFailureNotification); + } + } + else { // success from paypal. + self.viewModel.PageTitle(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.ProcessingOrderNotification); + var thisNotification = new Microsoft.WebPortal.Services.Notification(Microsoft.WebPortal.Services.Notification.NotificationType.Progress, self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.ProcessingOrderMessage); + self.webPortal.Services.Notifications.add(thisNotification); + + new Microsoft.WebPortal.Utilities.RetryableServerCall(self.webPortal.Helpers.ajaxCall(self.apiUrl, Microsoft.WebPortal.HttpMethod.Get, { + }, Microsoft.WebPortal.ContentType.Json, 120000), self.apiUrl, []).execute() + .done(function (result) { + // hand it off to the subscriptions page. + thisNotification.dismiss(); + + if (self.viewModel.customerId != null) { + self.viewModel.showDoneButton(true); + self.viewModel.showSubscriptions(true); + self.viewModel.PageTitle(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.NewCustomerOrderSuccessMessage); + self.viewModel.Subscriptions(result.Subscriptions); + self.viewModel.TotalPrice(result.SummaryTotal); + } else { + // all processed so push to subscriptions page. + self.webPortal.Journey.start(self.viewModel.nextJourney); + } + }) + .fail(function (result, status, error) { + thisNotification.type(Microsoft.WebPortal.Services.Notification.NotificationType.Error); + thisNotification.buttons([ + Microsoft.WebPortal.Services.Button.create(Microsoft.WebPortal.Services.Button.StandardButtons.OK, self.webPortal.Resources.Strings.OK, function () { + thisNotification.dismiss(); + }) + ]); + + self.viewModel.showDoneButton(true); + self.viewModel.PageTitle(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.OrderProcessingFailureNotification); + + var errorPayload = JSON.parse(result.responseText); + if (errorPayload) { + switch (errorPayload.ErrorCode) { + case Microsoft.WebPortal.ErrorCode.AlreadyExists: + thisNotification.message(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.CannotAddExistingSubscriptionError); + break; + case Microsoft.WebPortal.ErrorCode.InvalidInput: + thisNotification.message(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.InvalidInputErrorPrefix + errorPayload.Details.ErrorMessage); + break; + case Microsoft.WebPortal.ErrorCode.DownstreamServiceError: + thisNotification.message(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.DownstreamErrorPrefix + errorPayload.Details.ErrorMessage); + break; + case Microsoft.WebPortal.ErrorCode.PaymentGatewayPaymentError: + thisNotification.message(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.PaymentGatewayErrorPrefix + errorPayload.Details.ErrorMessage); + break; + case Microsoft.WebPortal.ErrorCode.PaymentGatewayIdentityFailureDuringPayment: + case Microsoft.WebPortal.ErrorCode.PaymentGatewayFailure: + thisNotification.message(errorPayload.Details.ErrorMessage); + break; + default: + thisNotification.message(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.OrderFailureMessage); + break; + } + } else { + thisNotification.message(self.webPortal.Resources.Strings.Plugins.ProcessOrderPage.OrderFailureMessage); + } + }) + .always(function () { + self.isPosting = false; + }); + } + }; + + processOrder(); +} + +//@ sourceURL=ProcessOrderPresenter.js \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/RegistrationConfirmationPresenter.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/RegistrationConfirmationPresenter.js index 4923189..fd515fd 100644 --- a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/RegistrationConfirmationPresenter.js +++ b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/RegistrationConfirmationPresenter.js @@ -10,12 +10,16 @@ Microsoft.WebPortal.RegistrationConfirmationPresenter = function (webPortal, fea this.base.constructor.call(this, webPortal, feature, "Home", "/Template/RegistrationConfirmation/"); var self = this; - self.viewModel = registrationConfirmationViewModel; - self.viewModel.Subscriptions = registrationConfirmationViewModel.CreatedSubscriptions.Subscriptions; - self.viewModel.TotalPrice = registrationConfirmationViewModel.CreatedSubscriptions.SummaryTotal; + self.viewModel = registrationConfirmationViewModel; - var addressLine = this.viewModel.AddressLine1; + // object to pass to order API. + self.viewModel.orderToPlace = { + Subscriptions: registrationConfirmationViewModel.SubscriptionsToOrder, + OperationType: Microsoft.WebPortal.CommerceOperationType.NewPurchase, //PurchaseSubscriptions. + CustomerId: registrationConfirmationViewModel.MicrosoftId // populate the Customer Id. + }; + var addressLine = this.viewModel.AddressLine1; if (this.viewModel.AddressLine2) { addressLine += " " + this.viewModel.AddressLine2; } @@ -34,7 +38,67 @@ Microsoft.WebPortal.RegistrationConfirmationPresenter = function (webPortal, fea this.onDoneClicked = function () { // go back to the home page - webPortal.Journey.start(Microsoft.WebPortal.Feature.Home); + // webPortal.Journey.start(Microsoft.WebPortal.Feature.Home); + + // Prepare the order + self.raiseOrder(); + } + + this.raiseOrder = function (customerNotification, registeredCustomer) { + /// + /// Called when the customer has been created and hence order can be placed. + /// + + // order notification. + var orderNotification = new Microsoft.WebPortal.Services.Notification(Microsoft.WebPortal.Services.Notification.NotificationType.Progress, + self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.PreparingOrderAndRedirectingMessage); + self.webPortal.Services.Notifications.add(orderNotification); + + new Microsoft.WebPortal.Utilities.RetryableServerCall(this.webPortal.Helpers.ajaxCall("api/Order/NewCustomerPrepareOrder", Microsoft.WebPortal.HttpMethod.Post, self.viewModel.orderToPlace, Microsoft.WebPortal.ContentType.Json, 120000), "RegisterCustomerOrder", []).execute() + // Success of Create CustomerOrder API Call. + .done(function (result) { + orderNotification.dismiss(); + // we need to now redirect to paypal based on the response from the API. + window.location = result; + }) + // Failure in Create CustomerOrder API call. + .fail(function (result, status, error) { + // on failure check if customerid is returned (or check using errCode). if returned then do something to set the ClientCustomerId + orderNotification.type(Microsoft.WebPortal.Services.Notification.NotificationType.Error); + orderNotification.buttons([ + // no need for retry button. user should be able to hit submit. + Microsoft.WebPortal.Services.Button.create(Microsoft.WebPortal.Services.Button.StandardButtons.OK, self.webPortal.Resources.Strings.OK, function () { + orderNotification.dismiss(); + }) + ]); + + var errorPayload = JSON.parse(result.responseText); + + if (errorPayload) { + switch (errorPayload.ErrorCode) { + case Microsoft.WebPortal.ErrorCode.InvalidInput: + orderNotification.message(self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.InvalidInputErrorPrefix + errorPayload.Details.ErrorMessage); + break; + case Microsoft.WebPortal.ErrorCode.DownstreamServiceError: + orderNotification.message(self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.DownstreamErrorPrefix + errorPayload.Details.ErrorMessage); + break; + case Microsoft.WebPortal.ErrorCode.PaymentGatewayPaymentError: + case Microsoft.WebPortal.ErrorCode.PaymentGatewayIdentityFailureDuringPayment: + case Microsoft.WebPortal.ErrorCode.PaymentGatewayFailure: + orderNotification.message(errorPayload.Details.ErrorMessage); + break; + default: + orderNotification.message(self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.OrderRegistrationFailureMessage); + break; + } + } else { + orderNotification.message(self.webPortal.Resources.Strings.Plugins.CustomerRegistrationPage.OrderRegistrationFailureMessage); + } + + }) + .always(function () { + self.isPosting = false; + }); } } diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/SubscriptionsPresenter.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/SubscriptionsPresenter.js index c09662c..779e52e 100644 --- a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/SubscriptionsPresenter.js +++ b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/SubscriptionsPresenter.js @@ -72,7 +72,7 @@ Microsoft.WebPortal.SubscriptionsPresenter.prototype.onRender = function () { var getSubscriptionsSummary = function () { var getSubscriptionSummaryServerCall = self.webPortal.ServerCallManager.create( - self.feature, self.webPortal.Helpers.ajaxCall("api/Subscription/summary", Microsoft.WebPortal.HttpMethod.Get), "GetSubscriptionSummary"); + self.feature, self.webPortal.Helpers.ajaxCall("api/Order/summary", Microsoft.WebPortal.HttpMethod.Get), "GetSubscriptionSummary"); self.viewModel.IsSet(false); self.viewModel.ShowProgress(true); diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/UpdateSubscriptionsPresenter.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/UpdateSubscriptionsPresenter.js index 00b7eee..86cc960 100644 --- a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/UpdateSubscriptionsPresenter.js +++ b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/UpdateSubscriptionsPresenter.js @@ -8,9 +8,7 @@ Microsoft.WebPortal.UpdateSubscriptionsPresenter = function (webPortal, feature, /// The feature for which this presenter is created. /// The customer subscription item which is being edited. - this.base.constructor.call(this, webPortal, feature, "Update Subscriptions", "/Template/UpdateSubscriptions/"); - this.creditCardInputView = new Microsoft.WebPortal.Views.CreditCardInputView(webPortal, "#CreditCardInputContainer"); - + this.base.constructor.call(this, webPortal, feature, "Update Subscriptions", "/Template/UpdateSubscriptions/"); var self = this; self.isPosting = false; @@ -34,6 +32,12 @@ Microsoft.WebPortal.UpdateSubscriptionsPresenter = function (webPortal, feature, this.viewModel.PageTitle = self.webPortal.Resources.Strings.Plugins.UpdateSubscriptionPage.RenewSubscriptionTitleText; } + this.viewModel.OperationType = 1; + if (this.viewModel.isUpdateSubscription) { + this.viewModel.OperationType = Microsoft.WebPortal.CommerceOperationType.AdditionalSeatsPurchase; // AddSeats. + } else if (this.viewModel.isRenewSubscription) { + this.viewModel.OperationType = Microsoft.WebPortal.CommerceOperationType.Renewal; // RenewSubscription. + } this.viewModel.ShowProgress = ko.observable(true); this.viewModel.IsSet = ko.observable(false); @@ -43,47 +47,27 @@ Microsoft.WebPortal.UpdateSubscriptionsPresenter = function (webPortal, feature, webPortal.Journey.retract(); } - this.onSubmitClicked = function () { - if (self.isPosting) { - return; - } - - self.webPortal.Services.Notifications.clear(); - var subscriptions = self.viewModel; - - if ($("#Form").valid()) { - self.isPosting = true; - this.raiseOrder(); - } else { - // the form is invalid - } - } - - this.raiseOrder = function () { - /// - /// Called to renew or add more seats to the subscription. - /// - var thisNotification = new Microsoft.WebPortal.Services.Notification(Microsoft.WebPortal.Services.Notification.NotificationType.Progress, self.webPortal.Resources.Strings.Plugins.UpdateSubscriptionPage.UpdatingSubscriptionMessage); + this.onFormPostClicked = function () { + // create order payload & post to order api. + // on success, order api will return a redirect uri for payment gateway + // -- redirect UX to payment gateway URI. + + var thisNotification = new Microsoft.WebPortal.Services.Notification(Microsoft.WebPortal.Services.Notification.NotificationType.Progress, self.webPortal.Resources.Strings.Plugins.UpdateSubscriptionPage.PreparingOrderAndRedirectingMessage); self.webPortal.Services.Notifications.add(thisNotification); - if (self.viewModel.isUpdateSubscription) - apiUrl = "api/Subscription/AddSeats"; - - if (self.viewModel.isRenewSubscription) - apiUrl = "api/Subscription/Renew"; - - new Microsoft.WebPortal.Utilities.RetryableServerCall(this.webPortal.Helpers.ajaxCall(apiUrl, Microsoft.WebPortal.HttpMethod.Post, { + new Microsoft.WebPortal.Utilities.RetryableServerCall(this.webPortal.Helpers.ajaxCall("api/Order/Prepare", Microsoft.WebPortal.HttpMethod.Post, { Subscriptions: this.getSubscriptions(), - CreditCard: this.getCreditCardInfo() - }, Microsoft.WebPortal.ContentType.Json, 120000), apiUrl, []).execute() - .done(function () { + OperationType: self.viewModel.OperationType + }, Microsoft.WebPortal.ContentType.Json, 120000), "api/Order/Prepare", []).execute() + .done(function (result) { // hand it off to the subscriptions page. thisNotification.dismiss(); - self.webPortal.Journey.start(Microsoft.WebPortal.Feature.Subscriptions); - }) - .fail(function (result, status, error) { + // we need to now redirect to paypal based on the response from the API. + window.location = result; + }) + .fail(function (result, status, error) { thisNotification.type(Microsoft.WebPortal.Services.Notification.NotificationType.Error); - thisNotification.buttons([ + thisNotification.buttons([ Microsoft.WebPortal.Services.Button.create(Microsoft.WebPortal.Services.Button.StandardButtons.OK, self.webPortal.Resources.Strings.OK, function () { thisNotification.dismiss(); }) @@ -99,32 +83,13 @@ Microsoft.WebPortal.UpdateSubscriptionsPresenter = function (webPortal, feature, case Microsoft.WebPortal.ErrorCode.DownstreamServiceError: thisNotification.message(self.webPortal.Resources.Strings.Plugins.UpdateSubscriptionPage.DownstreamErrorPrefix + errorPayload.Details.ErrorMessage); break; - case Microsoft.WebPortal.ErrorCode.CardCVNCheckFailed: - thisNotification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.CardCVNFailedError); - break; - case Microsoft.WebPortal.ErrorCode.CardExpired: - thisNotification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.CardExpiredError + self.webPortal.Resources.Strings.Plugins.CreditCardView.UseAlternateCardMessage); - break; - case Microsoft.WebPortal.ErrorCode.CardRefused: - thisNotification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.CardRefusedError +self.webPortal.Resources.Strings.Plugins.CreditCardView.UseAlternateCardMessage); - break; - case Microsoft.WebPortal.ErrorCode.PaymentGatewayPaymentError: - thisNotification.message(self.webPortal.Resources.Strings.Plugins.CreditCardView.PaymentGatewayErrorPrefix + self.webPortal.Resources.Strings.Plugins.CreditCardView.UseAlternateCardMessage); - break; - case Microsoft.WebPortal.ErrorCode.PaymentGatewayIdentityFailureDuringPayment: - case Microsoft.WebPortal.ErrorCode.PaymentGatewayFailure: - thisNotification.message(errorPayload.Details.ErrorMessage); - break; default: - thisNotification.message(self.webPortal.Resources.Strings.Plugins.UpdateSubscriptionPage.OrderAddFailureMessage); + thisNotification.message(self.webPortal.Resources.Strings.Plugins.UpdateSubscriptionPage.OrderUpdateFailureMessag); break; } } else { - thisNotification.message(self.webPortal.Resources.Strings.Plugins.UpdateSubscriptionPage.OrderUpdateFailureMessage); + thisNotification.message(self.webPortal.Resources.Strings.Plugins.UpdateSubscriptionPage.OrderUpdateFailureMessage); } - - - }) .always(function () { self.isPosting = false; @@ -135,28 +100,12 @@ Microsoft.WebPortal.UpdateSubscriptionsPresenter = function (webPortal, feature, var orders = []; orders.push({ - SubscriptionId: self.viewModel.SubscriptionId, // required for the add seats & renew calls. + SubscriptionId: self.viewModel.SubscriptionId, // required for the add seats & renew calls. Quantity: self.viewModel.Quantity() // this.addSubscriptionsView.subscriptionsList.rows()[i].quantity() }); return orders; } - - - this.getCreditCardInfo = function () { - var paymentCard = { - CreditCardType: this.creditCardInputView.viewModel.CardType(), - CardHolderFirstName: this.creditCardInputView.viewModel.CardHolderFirstName(), - CardHolderLastName: this.creditCardInputView.viewModel.CardHolderLastName(), - CreditCardNumber: this.creditCardInputView.viewModel.CardNumber(), - CreditCardExpiryMonth: this.creditCardInputView.viewModel.Month(), - CreditCardExpiryYear: this.creditCardInputView.viewModel.Year(), - CreditCardCvn: this.creditCardInputView.viewModel.CardCvn() - } - - return paymentCard; - } - } // inherit BasePresenter @@ -174,8 +123,6 @@ Microsoft.WebPortal.UpdateSubscriptionsPresenter.prototype.onRender = function ( /// ko.applyBindings(this, $("#Form")[0]); - // show credit card control after fetching subscriptions. - this.creditCardInputView.render(); var self = this; @@ -232,11 +179,6 @@ Microsoft.WebPortal.UpdateSubscriptionsPresenter.prototype.onRender = function ( Microsoft.WebPortal.UpdateSubscriptionsPresenter.prototype.onShow = function () { /// /// Called when content is shown. - /// - - this.creditCardInputView.show(); + /// } - - - //@ sourceURL=UpdateSubscriptionsPresenter.js \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/Views/CreditCardInputView.js b/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/Views/CreditCardInputView.js deleted file mode 100644 index 0f7f1b3..0000000 --- a/Source/PartnerCenter.CustomerPortal/Scripts/Plugins/Views/CreditCardInputView.js +++ /dev/null @@ -1,112 +0,0 @@ -/// - -Microsoft.WebPortal.Views.CreditCardInputView = function (webPortal, elementSelector, isShown, animation) { - /// - /// A view that render input fields for Credit Card information. - /// - /// The web portal instance - /// The JQuery selector for the HTML element this view will own. - /// The initial show state. Optional. Default is false. - /// Optional animation to use for showing and hiding the view. - - this.base.constructor.call(this, webPortal, elementSelector, isShown, null, animation); - this.template = "CreditCardInput-template"; - - this.monthsArray = []; - for (counter = 1; counter <= 12; ++counter) { - this.monthsArray.push(counter); - } - - this.currentYear = new Date().getFullYear(); - this.yearsArray = []; - for (counter = this.currentYear; counter < this.currentYear + 10; ++counter) { - this.yearsArray.push(counter); - } - - this.currentMonth = new Date().getMonth() + 1; - - this.viewModel = { - CardHolderFirstName: ko.observable(""), - CardHolderLastName: ko.observable(""), - CardNumber: ko.observable(""), - CardExpiryMonth: ko.observable(""), - CardExpiryYear: ko.observable(""), - CardCvn: ko.observable(""), - Year: ko.observable(this.currentYear), - Month: ko.observable(this.currentMonth) - } - - this.viewModel.Months = this.monthsArray; - this.viewModel.Years = this.yearsArray; - - this.viewModel.CardTypeCaption = ko.computed(function () { - var self = this; - number = this.viewModel.CardNumber(); - - // Regex Borrowed from https://github.com/jzaefferer/jquery-validation/blob/master/src/additional/creditcardtypes.js - // if nothing is entered. - if (number.length < 1) { - self.viewModel.CardType = ko.observable("unknown"); - return ""; - } - // check for Visa card. - else if (/^(4)/.test(number)) { - self.viewModel.CardType = ko.observable("visa"); - return " - " + self.webPortal.Resources.Strings.Plugins.CreditCardView.CreditCardVisaCaption; - } - // check for master card. - else if (/^(5[12345])/.test(number)) { - self.viewModel.CardType = ko.observable("mastercard"); - return " - " + self.webPortal.Resources.Strings.Plugins.CreditCardView.CreditCardMasterCaption; - } - // check for Amex card. - else if (/^(3[47])/.test(number)) { - self.viewModel.CardType = ko.observable("amex"); - return " - " + self.webPortal.Resources.Strings.Plugins.CreditCardView.CreditCardAmexCaption; - } - // check for Discover card. - else if (/^(6011)/.test(number)) { - self.viewModel.CardType = ko.observable("discover"); - return " - " + self.webPortal.Resources.Strings.Plugins.CreditCardView.CreditCardDiscoverCaption; - } - // default is unknown card. - else { - self.viewModel.CardType = ko.observable("unknown"); - return " - " + self.webPortal.Resources.Strings.Plugins.CreditCardView.CreditCardNotSupportedCaption; - } - }, this); - - -} - -// extend the base view -$WebPortal.Helpers.inherit(Microsoft.WebPortal.Views.CreditCardInputView, Microsoft.WebPortal.Core.View); - -Microsoft.WebPortal.Views.CreditCardInputView.prototype.onRender = function () { - /// - /// Called when the view is rendered. - /// - - $(this.elementSelector).attr("data-bind", "template: { name: '" + this.template + "'}"); - ko.applyBindings(this, $(this.elementSelector)[0]); -} - -Microsoft.WebPortal.Views.CreditCardInputView.prototype.onShowing = function (isShowing) { -} - -Microsoft.WebPortal.Views.CreditCardInputView.prototype.onShown = function (isShowing) { -} - -Microsoft.WebPortal.Views.CreditCardInputView.prototype.onDestroy = function () { - /// - /// Called when the journey trail is about to be destroyed. - /// - - if ($(this.elementSelector)[0]) { - // if the element is there, clear its bindings and clean up its content - ko.cleanNode($(this.elementSelector)[0]); - $(this.elementSelector).empty(); - } -} - -//@ sourceURL=CreditCardInputView.js \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/WebPortal/Core/SessionManager.js b/Source/PartnerCenter.CustomerPortal/Scripts/WebPortal/Core/SessionManager.js index 0348905..5874bf1 100644 --- a/Source/PartnerCenter.CustomerPortal/Scripts/WebPortal/Core/SessionManager.js +++ b/Source/PartnerCenter.CustomerPortal/Scripts/WebPortal/Core/SessionManager.js @@ -39,6 +39,7 @@ Microsoft.WebPortal.Core.SessionManager.prototype.initialize = function (eventId this.webPortal.registerFeaturePresenter(Microsoft.WebPortal.Feature.Subscriptions, Microsoft.WebPortal.SubscriptionsPresenter); this.webPortal.registerFeaturePresenter(Microsoft.WebPortal.Feature.AddSubscriptions, Microsoft.WebPortal.AddSubscriptionsPresenter); this.webPortal.registerFeaturePresenter(Microsoft.WebPortal.Feature.UpdateSubscriptions, Microsoft.WebPortal.UpdateSubscriptionsPresenter); + this.webPortal.registerFeaturePresenter(Microsoft.WebPortal.Feature.ProcessOrder, Microsoft.WebPortal.ProcessOrderPresenter); this.webPortal.registerFeaturePresenter(Microsoft.WebPortal.Feature.CustomerAccount, Microsoft.WebPortal.CustomerAccountPresenter); this.webPortal.registerFeaturePresenter(Microsoft.WebPortal.Feature.UpdateContactInformation, Microsoft.WebPortal.UpdateContactInformationPresenter); diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/WebPortal/Core/UrlManager.js b/Source/PartnerCenter.CustomerPortal/Scripts/WebPortal/Core/UrlManager.js index 713ddfc..a0c1b88 100644 --- a/Source/PartnerCenter.CustomerPortal/Scripts/WebPortal/Core/UrlManager.js +++ b/Source/PartnerCenter.CustomerPortal/Scripts/WebPortal/Core/UrlManager.js @@ -32,9 +32,15 @@ Microsoft.WebPortal.Core.UrlManager.prototype.onPortalInitialized = function () if (window.location.hash && window.location.hash.length > 0) { // a hash is specified in the URL, extract the feature and any context object from the URL - var featureHash = window.location.hash.slice(1).split("?Context="); + var featureHash = window.location.hash.slice(1).split("?"); featureToInvoke = Microsoft.WebPortal.Feature[featureHash[0]] || featureToInvoke; - contextToPass = featureHash.length > 1 ? JSON.parse(decodeURI(featureHash[1])) : contextToPass; + try { + featureHash = window.location.hash.slice(1).split("?Context="); + contextToPass = featureHash.length > 1 ? JSON.parse(decodeURI(featureHash[1])) : contextToPass; + } + catch (e) { + contextToPass = null; + } } // start a journey with the feature we just determined @@ -57,7 +63,14 @@ Microsoft.WebPortal.Core.UrlManager.prototype.onPortalInitialized = function () // could not retract, start a new journey with the feature name and context extracted from the URL var queryStringIndex = window.location.hash.indexOf("?"); var featureName = window.location.hash.slice(1, queryStringIndex == -1 ? undefined : queryStringIndex); - var context = queryStringIndex == -1 ? null : JSON.parse(decodeURI(window.location.hash.slice(queryStringIndex))); + + var context = null; + try { + context = queryStringIndex == -1 ? null : JSON.parse(decodeURI(window.location.hash.slice(queryStringIndex))); + } + catch (e) { + context = null; + } if (self.webPortal.Journey.journey()[self.webPortal.Journey.journey().length - 1].feature.name != featureName) { self.webPortal.Journey.start(Microsoft.WebPortal.Feature[featureName], context); diff --git a/Source/PartnerCenter.CustomerPortal/Scripts/_references.js b/Source/PartnerCenter.CustomerPortal/Scripts/_references.js index 70b0295cdb4127f30973b242a0d4f2063b5c5b68..29c2a6df40dad0c8bea943ca74980bc93674d279 100644 GIT binary patch delta 96 zcmX@)zQJQd7L#f|LoN`eG88c+1Ic`b0x&y~p@bn5$jbxLl?(-wFLK&Xe!wWOIfH2n llXwA8aXwgiF+(wsRm6}2(lmJ@WBg_rRwL%kXE?q|0RZ_a8nOTY delta 58 zcmdnsamal`7SrYjOq-ZCbFdjP%R4g^F{Co2Fk~{6FgPm1q`J?k+8|} Mobj8jIKN2&0QxQv7XSbN diff --git a/Source/PartnerCenter.CustomerPortal/Views/Controls/CreditCardInput.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Controls/CreditCardInput.cshtml deleted file mode 100644 index 1d85b38..0000000 --- a/Source/PartnerCenter.CustomerPortal/Views/Controls/CreditCardInput.cshtml +++ /dev/null @@ -1,45 +0,0 @@ - \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Views/Controls/NewCustomerProfile.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Controls/NewCustomerProfile.cshtml index 5bc271b..2afc090 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Controls/NewCustomerProfile.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Controls/NewCustomerProfile.cshtml @@ -101,7 +101,7 @@ - @Resources.CustomerProfilePhoneNumberCaption + @Resources.PhoneHeaderCaption diff --git a/Source/PartnerCenter.CustomerPortal/Views/Framework/Resources.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Framework/Resources.cshtml index a8e5147..c1ff6d4 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Framework/Resources.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Framework/Resources.cshtml @@ -23,7 +23,7 @@ Retry: "@Resources.RetryCapital", Next: "@Resources.Next", Back: "@Resources.Back", - Add: "@Resources.AddCapital", + Add: "@Resources.AddCaption", Delete: "@Resources.Delete", Save: "@Resources.Save", Undo: "@Resources.Undo", @@ -113,7 +113,7 @@ CouldNotRetrievePartnerOffers: "@Resources.CouldNotRetrievePartnerOffers" }, CustomerRegistrationPage: { - CustomerOrderRegistrationMessage: "@Resources.CustomerOrderRegistrationMessage", + PreparingOrderAndRedirectingMessage: "@Resources.PreparingOrderAndRedirectingMessage", CustomerRegistrationSuccessMessage: "@Resources.CustomerRegistrationSuccessMessage", CustomerRegistrationFailureMessage: "@Resources.CustomerRegistrationFailureMessage", OrderRegistrationFailureMessage: "@Resources.OrderRegistrationFailureMessage", @@ -124,21 +124,20 @@ InvalidAddress: "@Resources.InvalidAddress" }, UpdateSubscriptionPage: { - UpdatingSubscriptionMessage: "@Resources.UpdatingSubscriptionMessage", + PreparingOrderAndRedirectingMessage: "@Resources.PreparingOrderAndRedirectingMessage", ProratedPaymentTotalText: "@Resources.ProratedPaymentTotal", OrderUpdateFailureMessage: "@Resources.OrderUpdateFailureMessage", InvalidInputErrorPrefix: "@Resources.InvalidInputErrorPrefix", DownstreamErrorPrefix: "@Resources.DownstreamErrorPrefix", RenewSubscriptionTitleText: "@Resources.RenewSubscriptionTitleCaption", CouldNotRetrieveOffer: "@Resources.CouldNotRetrieveOffer", - AddMoreSeatsTitleText: "@Resources.UpdateSubscriptions", + AddMoreSeatsTitleText: "@Resources.AddMoreLicensesCaption", PaymentTotalText: "@Resources.TotalAmountCaption" }, AddSubscriptionPage: { - AddingSubscriptionMessage: "@Resources.AddingSubscriptionMessage", + PreparingOrderAndRedirectingMessage: "@Resources.PreparingOrderAndRedirectingMessage", OrderAddFailureMessage: "@Resources.OrderAddFailureMessage", InvalidInputErrorPrefix: "@Resources.InvalidInputErrorPrefix", - CannotAddExistingSubscriptionError: "@Resources.CannotAddExistingSubscriptionError", DownstreamErrorPrefix: "@Resources.DownstreamErrorPrefix" }, SubscriptionsSummaryPage: { @@ -147,21 +146,22 @@ CustomerAccountPage: { CouldNotRetrieveCustomerAccount: "@Resources.CouldNotRetrieveCustomerAccount" }, + ProcessOrderPage: { + PaymentGatewayErrorPrefix: "@Resources.PaymentGatewayErrorPrefix", + CannotAddExistingSubscriptionError: "@Resources.CannotAddExistingSubscriptionError", + ProcessingOrderMessage: "@Resources.ProcessingOrderMessage", + OrderProcessingFailureNotification: "@Resources.OrderProcessingFailureNotification", + ProcessingOrderNotification: "@Resources.ProcessingOrderNotification", + PaymentReceiptFailureNotification: "@Resources.PaymentReceiptFailureNotification", + OrderFailureMessage: "@Resources.OrderFailureMessage", + NewCustomerProcessOrderFailureReceivingPaymentMessage: "@Resources.NewCustomerProcessOrderFailureReceivingPaymentMessage", + NewCustomerOrderSuccessMessage: "@Resources.NewCustomerOrderSuccessMessage", + InvalidInputErrorPrefix: "@Resources.InvalidInputErrorPrefix", + DownstreamErrorPrefix: "@Resources.DownstreamErrorPrefix" + }, AddSubscriptionsView: { SelectOfferErrorMessage: "@Resources.SelectOfferErrorMessage", EmptyListCaption: "@Resources.EmptyListCaption" - }, - CreditCardView: { - CreditCardNotSupportedCaption: "@Resources.CreditCardNotSupportedCaption", - CreditCardAmexCaption: "@Resources.CreditCardAmexCaption", - CreditCardVisaCaption: "@Resources.CreditCardVisaCaption", - CreditCardMasterCaption: "@Resources.CreditCardMasterCaption", - CreditCardDiscoverCaption: "@Resources.CreditCardDiscoverCaption", - PaymentGatewayErrorPrefix: "@Resources.PaymentGatewayErrorPrefix", - CardRefusedError: "@Resources.CardRefusedError", - CardExpiredError: "@Resources.CardExpiredError", - UseAlternateCardMessage: " @Resources.UseAlternateCardMessage", - CardCVNFailedError: "@Resources.CardCVNFailedError" } } }, diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/AddOrUpdateOffer.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/AddOrUpdateOffer.cshtml index fd71da2..96a6655 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/AddOrUpdateOffer.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/AddOrUpdateOffer.cshtml @@ -62,7 +62,7 @@ - @Resources.DetailedFeaturesCaption + @Resources.FeaturesCaption @@ -79,7 +79,7 @@ - @Resources.SummaryCaption + @Resources.SummaryListRowCaption diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/AddSubscriptions.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/AddSubscriptions.cshtml index 454d732..c776ea0 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/AddSubscriptions.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/AddSubscriptions.cshtml @@ -3,8 +3,7 @@
-
-
+

@Resources.PlaceOrderCaption @@ -23,9 +22,6 @@ rules: { }, messages: { - CardCvn: { - digits: "@Resources.CvnValidationMessage" - }, }, submitHandler: function (form) { diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/BrandingSetup.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/BrandingSetup.cshtml index 711392f..5354fba 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/BrandingSetup.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/BrandingSetup.cshtml @@ -44,13 +44,13 @@ -
+
-
@Resources.ContactUsSubText
+
@Resources.ShowInContactUsHeaderCaption
@@ -68,13 +68,13 @@ - @Resources.PhoneHeader
+ @Resources.PhoneHeaderCaption
-
@Resources.ContactSalesSubText
+
@Resources.ShowInContactUsHeaderCaption
diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/CustomerRegistration.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/CustomerRegistration.cshtml index f89e9a7..ed329c3 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/CustomerRegistration.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/CustomerRegistration.cshtml @@ -4,8 +4,7 @@
-
-
+
@@ -48,9 +47,6 @@ } }, messages: { - CardCvn: { - digits: "@Resources.CvnValidationMessage" - }, consent: { required: "@Resources.AcceptTermsValidationMessage" } diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/Error.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/Error.cshtml index be55b17..6a279c0 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/Error.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/Error.cshtml @@ -1,9 +1,9 @@ @model System.Web.Mvc.HandleErrorInfo @{ - ViewBag.Title = "Error"; + ViewBag.Title = Resources.ErrorPageTitleCaption; } -

Error.

-

An error occurred while processing your request.

+

@Resources.ErrorPageTitleCaption

+

@Resources.ErrorPageDetailsCaption

diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/OfferList.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/OfferList.cshtml index 9e972c5..34a5317 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/OfferList.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/OfferList.cshtml @@ -41,7 +41,7 @@ - @Resources.FeaturesListRowCaption + @Resources.FeaturesCaption
  • diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/PaymentSetup.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/PaymentSetup.cshtml index 8f3d596..45c582d 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/PaymentSetup.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/PaymentSetup.cshtml @@ -7,7 +7,10 @@ - @Resources.PaymentConfigurationSubTextPrefix @Resources.PayPalWebsiteCaption + @Resources.PaymentConfigurationSubTextPrefix + + @Resources.PayPalWebsiteCaption + @Resources.PaymentConfigurationSubTextPostfix diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/ProcessOrder.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/ProcessOrder.cshtml new file mode 100644 index 0000000..570a444 --- /dev/null +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/ProcessOrder.cshtml @@ -0,0 +1,71 @@ +
    +
    +
    + +
    + + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + @Resources.Subscription + + @Resources.UserLicencesCaption + + @Resources.SubscriptionSummaryRateCaption + + @Resources.AnnualPriceCaption + +
    + + + + + + + + +
    + @Resources.Total + + +
    +
    + + + + + +
    \ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/RegistrationConfirmation.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/RegistrationConfirmation.cshtml index 4d4b058..4f11d5f 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/RegistrationConfirmation.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/RegistrationConfirmation.cshtml @@ -65,64 +65,7 @@
-
- + - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- @Resources.Subscription - - @Resources.UserLicencesCaption - - @Resources.SubscriptionSummaryRateCaption - - @Resources.AnnualPriceCaption - -
- - - - - - - - -
- @Resources.Total - - -
-
- -
\ No newline at end of file diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/StandardSplashScreen.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/StandardSplashScreen.cshtml index b9a0a5c..cae1d7c 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/StandardSplashScreen.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/StandardSplashScreen.cshtml @@ -13,7 +13,7 @@ - + diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/Subscriptions.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/Subscriptions.cshtml index 915587c..40959ce 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/Subscriptions.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/Subscriptions.cshtml @@ -57,7 +57,7 @@ - + diff --git a/Source/PartnerCenter.CustomerPortal/Views/Shared/UpdateSubscriptions.cshtml b/Source/PartnerCenter.CustomerPortal/Views/Shared/UpdateSubscriptions.cshtml index 4c58af6..427ae35 100644 --- a/Source/PartnerCenter.CustomerPortal/Views/Shared/UpdateSubscriptions.cshtml +++ b/Source/PartnerCenter.CustomerPortal/Views/Shared/UpdateSubscriptions.cshtml @@ -69,11 +69,10 @@
-
-
+


- @Resources.PlaceOrderCaption - @Resources.Cancel + @Resources.PlaceOrderCaption + @Resources.Cancel